feat: add block model with IBlock flyweight and registry

This commit is contained in:
2026-05-28 11:53:15 +02:00
parent 17503820e6
commit f4d3f9aab4
8 changed files with 156 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
using VoxelEngine.World;
using VoxelEngine.World.Blocks;
using Xunit;
namespace VoxelEngine.Tests;
public class BlockRegistryTests
{
[Fact]
public void Get_ReturnsRegisteredBlockById()
{
var registry = new BlockRegistry();
Assert.IsType<AirBlock>(registry.Get(0));
Assert.IsType<StoneBlock>(registry.Get(1));
Assert.IsType<DirtBlock>(registry.Get(2));
Assert.IsType<GrassBlock>(registry.Get(3));
}
[Fact]
public void Air_IsNotOpaqueOrSolid_OthersAre()
{
var registry = new BlockRegistry();
Assert.False(registry.Get(0).IsOpaque);
Assert.False(registry.Get(0).IsSolid);
Assert.True(registry.Get(1).IsOpaque);
Assert.True(registry.Get(1).IsSolid);
}
[Fact]
public void Grass_HasDifferentTexturesPerFace()
{
var registry = new BlockRegistry();
IBlock grass = registry.Get(3);
Assert.Equal(0, grass.GetTextureIndex(BlockFace.Top));
Assert.Equal(1, grass.GetTextureIndex(BlockFace.Bottom));
Assert.Equal(3, grass.GetTextureIndex(BlockFace.North));
}
[Fact]
public void NamedAccessors_MatchIds()
{
var registry = new BlockRegistry();
Assert.Equal((ushort)0, registry.Air.Id);
Assert.Equal((ushort)1, registry.Stone.Id);
Assert.Equal((ushort)2, registry.Dirt.Id);
Assert.Equal((ushort)3, registry.Grass.Id);
}
}

11
World/BlockFace.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace VoxelEngine.World;
public enum BlockFace
{
Top, // +Y
Bottom, // -Y
North, // -Z
South, // +Z
East, // +X
West // -X
}

26
World/BlockRegistry.cs Normal file
View File

@@ -0,0 +1,26 @@
using VoxelEngine.World.Blocks;
namespace VoxelEngine.World;
public sealed class BlockRegistry
{
private readonly IBlock[] _byId;
public AirBlock Air { get; } = new();
public StoneBlock Stone { get; } = new();
public DirtBlock Dirt { get; } = new();
public GrassBlock Grass { get; } = new();
public BlockRegistry()
{
// Index by id. Order does not matter; slot = block.Id.
var blocks = new IBlock[] { Air, Stone, Dirt, Grass };
_byId = new IBlock[blocks.Length];
foreach (IBlock block in blocks)
{
_byId[block.Id] = block;
}
}
public IBlock Get(ushort id) => _byId[id];
}

10
World/Blocks/AirBlock.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace VoxelEngine.World.Blocks;
public sealed class AirBlock : IBlock
{
public ushort Id => 0;
public string Name => "Air";
public bool IsOpaque => false;
public bool IsSolid => false;
public int GetTextureIndex(BlockFace face) => 0;
}

10
World/Blocks/DirtBlock.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace VoxelEngine.World.Blocks;
public sealed class DirtBlock : IBlock
{
public ushort Id => 2;
public string Name => "Dirt";
public bool IsOpaque => true;
public bool IsSolid => true;
public int GetTextureIndex(BlockFace face) => 1;
}

View File

@@ -0,0 +1,16 @@
namespace VoxelEngine.World.Blocks;
public sealed class GrassBlock : IBlock
{
public ushort Id => 3;
public string Name => "Grass";
public bool IsOpaque => true;
public bool IsSolid => true;
public int GetTextureIndex(BlockFace face) => face switch
{
BlockFace.Top => 0, // grass top
BlockFace.Bottom => 1, // dirt
_ => 3 // grass side
};
}

View File

@@ -0,0 +1,10 @@
namespace VoxelEngine.World.Blocks;
public sealed class StoneBlock : IBlock
{
public ushort Id => 1;
public string Name => "Stone";
public bool IsOpaque => true;
public bool IsSolid => true;
public int GetTextureIndex(BlockFace face) => 2;
}

21
World/IBlock.cs Normal file
View File

@@ -0,0 +1,21 @@
namespace VoxelEngine.World;
public interface IBlock
{
ushort Id { get; }
string Name { get; }
// Drives face culling: a face is emitted only if its neighbor is not opaque.
bool IsOpaque { get; }
// Reserved for future collision (not used in the baseline).
bool IsSolid { get; }
// Atlas tile index for the given face.
int GetTextureIndex(BlockFace face);
// Interaction hooks — capability present now, system built post-baseline.
void OnInteract() { }
void OnPlaced() { }
void OnBroken() { }
}