0
\$\begingroup\$

I'm looking into making 4 types of cubes; grass, stone, dirt, and empty. I have a cube class that is just for storing the location of the cube.

    public Cube(GraphicsDevice Device, Vector3 Position, Texture2D Texture)

I want the Texture2D to be changed to something like CubeType. In my cube class, I have a grass and stone class which both inherit cube, but I don't know how to make that work for what I want.

    public class Grass : Cube
    {
        public Grass()
        {
            isMineable = false;
        }
    }

    public class Stone : Cube
    {
        public Stone()
        {
            isMineable = false;
        }
    }

The only thing that makes each cube differ from each other is the texture and if theyre mineable or not. What's the easiest way to implement what I want? I guess one way of doing it is just adding another parameter to Cube(bool isMineable), but for learning purposes I want to see if there's a better way to do it.

\$\endgroup\$
2
  • \$\begingroup\$ Eeeem, when you mine the cubes, check the type and then do something or nothing? \$\endgroup\$ Commented Jul 16, 2013 at 8:23
  • \$\begingroup\$ Well only the stone type can be mined or built. Grass and dirt can be destroyed by attacks \$\endgroup\$ Commented Jul 16, 2013 at 8:30

3 Answers 3

2
\$\begingroup\$

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).

\$\endgroup\$
4
  • \$\begingroup\$ I like it. The GetAt method will be very very very very very useful for when I want to cull faces. Can you explain the TypeTable to me? I've never seen one before. It looks like it just stores the kind of information for each cube obviously, but what would the string be used for? \$\endgroup\$ Commented Jul 16, 2013 at 19:58
  • \$\begingroup\$ Another thing, where am I putting the code to create my vertices and indices? Would that just be in Draw in the Chunk class? \$\endgroup\$ Commented Jul 16, 2013 at 20:16
  • \$\begingroup\$ It could be a GenerateFaceVertices or something along those lines. Or an entirely separate BlockMeshBuilder that takes a Chunk and a VertexBuffer. \$\endgroup\$ Commented Jul 16, 2013 at 20:33
  • \$\begingroup\$ Sounds good. I'll give it a shot \$\endgroup\$ Commented Jul 16, 2013 at 20:40
4
\$\begingroup\$

I think are doing over engineering the cubes...

it would be easier to use an enum, it is better because let's you indexing cube properties in arrays or sorting the cubes easily,

You don't need to store the texture and graphicsdevice for each cube... you can implement a CubeCollectionRenderer or similar where you manage that...

  enum CubeTypes { Grass, Stone, ..}

  public class Cube
  {
      public Vector3 cubePosition { get; set; }
      public bool isMineable { get; private set; }
      public CubeTypes CubeType {get; private set;}

      public Cube(CubeTypes CubeType)
      {
         this.CubeType = CubeType;
         this.isMineable = (Cubetype == Cubetypes.Stone);
      }
  }

  public class CubeCollectionRenderer {
      // Approach 1: One texture atlas that contains all the cube type textures,
                     the texture portion sources can be included in the shader and 
                     pass the cubetype to index the right texture portion in the 
                     atlas. 
                     This is quite good and can be used with intancing techniques.
      Texture2D  Atlas;
      Rectangle[] Sources;
      List<Cube> Cubes;

      // Approach 2: Single textures for each cube type, to avoid state changes
                     drawing cubes is recomended to keep the cubes ordered by
                     CubeType.
      Texture2D[] Textures; 
      SortedSet Cubes = new SortedSet( 
          delegate (Cube a, Cube b) { return (int) a.CubeType - (int) b.cubeType; }
      );

      public CubeRenderer(GraphicsDevice device) { .. }



  }
\$\endgroup\$
3
  • \$\begingroup\$ Well the top part makes sense but the bottom doesn't. I'd rather go with approach 2, but what's the point of the array for textures and sorted set? \$\endgroup\$ Commented Jul 16, 2013 at 18:43
  • \$\begingroup\$ Changing the graphics context has a penalty when you are drawing, so if you use single textures you should avoid to change them often unnecesarily... if you sort the cubes by texture (type) then you are sure that texture changes are going to be minimal \$\endgroup\$
    – Blau
    Commented Jul 16, 2013 at 21:53
  • \$\begingroup\$ ah.. the array of textures.. if you load the array with the textures for grass, stone,.. etc... in the same order that CubeTypes is defined... you can index the cube textures by the cubetype: Textures[(int) cube.CubeType] \$\endgroup\$
    – Blau
    Commented Jul 16, 2013 at 21:59
0
\$\begingroup\$

Well I figured that out easy enough. My brain wasn't working for a bit so here it is.

    public class Cube
{
    GraphicsDevice graphicsDevice;
    Vector3 cubePosition { get; set; }
    Texture2D texture { get; set; }

    public bool isMineable { get; set; }

    public Cube(GraphicsDevice Device, Vector3 Position, Texture2D Texture)
    {
        Device = graphicsDevice;
        Position = cubePosition;
        Texture = texture;
    }

    public class Grass : Cube
    {
        public Grass(GraphicsDevice Device, Vector3 Position, Texture2D Texture)
            : base(Device, Position, Texture)
        {
            Device = graphicsDevice;
            Position = cubePosition;
            Texture = texture;
            isMineable = false;
        }
    }

    public class Stone : Cube
    {
        public Stone(GraphicsDevice Device, Vector3 Position, Texture2D Texture)
            : base(Device, Position, Texture)
        {
            Device = graphicsDevice;
            Position = cubePosition;
            Texture = texture;
            isMineable = true;
        }
    }

Added to the list with

    cubes.Add(new Cube.Grass(device, new Vector3(x - 50, map[x, z] - y, z - 50), grass));
\$\endgroup\$
5
  • \$\begingroup\$ I'd recommend against this. Inheritance is totally the wrong approach here. Use data, not code, to define what blocks are, how they look, and how they behave. See @Blau's answer for a better approach. \$\endgroup\$ Commented Jul 16, 2013 at 18:21
  • \$\begingroup\$ huh I did't even notice that answer lol \$\endgroup\$ Commented Jul 16, 2013 at 18:39
  • \$\begingroup\$ Is there a downside to using my method? Performance wise or anything like that \$\endgroup\$ Commented Jul 16, 2013 at 18:44
  • 2
    \$\begingroup\$ yes. for one, it's way more code and complexity than you need, which is always bad. two, it couples code to things that don't need it, making it really hard to change and extend later; it's just the usual bad OOP abuse you see all too often from bad learning materials. third, yes, having a Cube object at all is massively wasteful, though @Blau's answer didn't address that specifically, I may write one to explain this better than I can in a comment. \$\endgroup\$ Commented Jul 16, 2013 at 18:50
  • \$\begingroup\$ I've learned over the past few weeks that the Cube class is only representing the data drawn by my indices and vertices, and isn't actually needed for anything other than representing that point on the map. So maybe I wouldn't need a cube class, but for now it helps while I'm trying to get my BoundingBox stuff working \$\endgroup\$ Commented Jul 16, 2013 at 18:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .