You have two (common) problems with this design.
First, you don't need a Cube
class at all. Things like a cube's position are implicit if you store your data appropriately. Having a separate class is a waste of memory and, due to how C# gives you no control on memory layout of class objects, is going to hurt performance when you iterate over all your cubes.
Second, don't use inheritance and classes unless you really need them. Always try to reserver inheritance for logical interface extension, not "is-a" relationships between object. Yes, a StoneBlock
is a Block
, but that's a real-world taxonomy issue, not something the code needs to at all care about.
First, change your representation of blocks. You need a BlockDescriptor
that describes a type of block, similar to your sub-classes but data-drive. The actual type can be a free-form integer and all the data loaded from a file for easy extensibility and modding. To start, you can hard-code a table.
public static class Blocks {
public enum EBlockType { Air, Stone, Grass, Wood };
public class BlockDescriptor {
public EBlockType BlockType;
public string DescriptiveName;
public Texture2D Texture;
}
public static BlockDescriptor[] TypeTable = {
{ EBlockType.Air, "open space", null },
{ EBlockType.Stone, "granite", LoadTexture("stone.png") },
{ EBlockType.Grass, "dirt & grass", LoadTexture("grass.png") },
{ EBlockType.Wood, "oak", LoadTexture("wood.png") },
};
public static DrawBlock(Device device, Vector3 position, EBlockType type)
{
Texture2D texture = TypeTable[type];
if (texture != nullptr)
your_draw_cube(device, position, texture);
}
}
Now you can easily add rows to this table. You can do it at runtime, from a file, however you want. Add new fields to BlockDescriptor
, like IsMinable
, as needed.
The other problem is that you only need a single integer and an array to store your world (or a chunk in your world).
class Chunk {
public const int WIDTH = 512;
public const int HEIGHT = 512;
public const int DEPTH = 512;
private int[] _Blocks;
public Chunk() {
_Blocks = int[WIDTH * HEIGHT * DEPTH];
public int GetAt(int x, int y, int z) {
return _Blocks[y * WIDTH * DEPTH + z * WIDTH + x];
}
public void Draw(Device device) {
for (int y = 0; y < HEIGHT; ++y)
for (int z = 0; z < DEPTH; ++z)
for (int x = 0; x < WIDTH; ++x)
Blocks.DrawBlock(device, new Vector3(x, y, z), Blocks.EBlockType(GetAt(x, y, z)));
}
}
To store more data than just the type, you can use bit manipulation to pack all the data into an int (or long or short, whichever you need and no larger). You could also use a C# struct type if you want easy mode.
The idea of this data structure is that the entire chunk is tightly packed into a single contiguous chunk of memory. The CPU doesn't get stuck in a wait state while chasing down a bazillion Cube
instances.
Also note that at no point did I store a refernce to Device
. Blocks themselves have absolutely nothing to do with graphics; they're data. Neither the block instances nor the chunk have any reason at all to hold on to the device. You easily make the argument that Blocks.DrawBlock
should be split out into a separate place, too.
Really, you'll end up doing that anyway. You do not want to just draw a cube for each block. That is quite inefficient. First, it means one draw call per cube. That will be slooow and heavily underutilize your GPU which is designed for massive parallelism and drawing thousands of triangles at once. Also, if you have two blocks next to each other, why spend time processing the triangles between the blocks that you can't possibly see anyway? You'll want a separate pass that iterates over the blocks, finds outward faces (the faces of blocks that are adjacent to empty space), and push the triangles for those blocks into a separate mesh array and call this any time a block changes (which won't be too often). Note you'll need to switch to using texture atlasing rather than individual textures for each block for this to work.
Then your nice separate rendering code can just draw that mesh. One single draw call, no excess cube faces, super fast and efficient if nothing is changing (which, the vast majority of the frames, nothing will be).