Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

Im currently making a voxel game like Minecraft for fun with DirectX11. Game works with chunk system like any other voxel game, but my current algorithm for generating chunk mesh is not expandable. Block class has a few attributes like is block full and mesh type.

class Block
{
public:
    bool isFull = true;
    MeshType type = MeshType::FullBlock;
    Vector2i texture = { 9, 1 };
    Vector2i topTexture = { 9, 1 };
    const char* sound;

    Block(){}
    Block(bool isFull, MeshType type, Vector2i texture, Vector2i topTexture, const char* sound): isFull(isFull), type(type), texture(texture), topTexture(topTexture), sound(sound){}
    Block(bool isFull, MeshType type, Vector2i texture, const char* sound) : isFull(isFull), type(type), texture(texture), topTexture(texture), sound(sound) {}
    Block(bool isFull, MeshType type, Vector2i texture) : isFull(isFull), type(type), texture(texture), topTexture(texture) {}

};

Every block is then initialised in a vector

    blocks.reserve(64);

    Block air(false, MeshType::Empty, {0 ,0});
    blocks.emplace_back(air);

    Block grass(true, MeshType::FullBlock, { 3, 0 }, { 0, 0 }, "Audio/grass1.ogg");
    blocks.emplace_back(grass);

    Block stone(true, MeshType::FullBlock, { 1, 0 }, "Audio/stone1.ogg");
    blocks.emplace_back(stone);

    Block rose(false, MeshType::Cross, { 12 ,0 }, "Audio/grass1.ogg");
    blocks.emplace_back(rose);

    Block wheat(false, MeshType::Hash, { 8 ,3 });
    blocks.emplace_back(wheat);

    //and so on...

I have a function that accepts a vector of vertices and chunk data. That function loops through all the blocks with a few optimisations and emplaces data back into the vector that gets sent to the buffer.

for (int x = 0; x < ChunkWidth; x++)
    {
        for (int y = 0; y < ChunkHeight; y++)
        {
            for (int z = 0; z < ChunkWidth; z++)
            {
                if (IsDrawable[x][y][z] == 1)
                {
                    switch (blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]].type)
                    {
                    case MeshType::FullBlock:
                        BuildBlock(chunk, vertices, x, y, z);
                        break;
                    case MeshType::Cross:
                        FillCross(vertices, blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]], x + chunk->x * ChunkWidth, y, z + chunk->z * ChunkWidth);
                        break;
                    case MeshType::Hash:
                        FillHash(vertices, blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]], x + chunk->x * ChunkWidth, y, z + chunk->z * ChunkWidth);
                        break;
                    }
                }
            }
        }
    }

With every new mesh type the switch statement gets bigger and I think that is not the way to go. Im asking if there are better ways of doing this. I thank you in advance.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
112 views
Welcome To Ask or Share your Answers For Others

1 Answer

I think making different derived classes with a common parent class Block is the way to go here. You add a virtual method in it whose behaviour is overridden in the derived classes. Then you place them in a polymorphic vector of std::shared_ptr<Block> and call them. If you are afraid that for some reason this might be too slow you might replace the virtual functions with the Curiously Recurring Template Pattern (CRTP) to achieve static polymorphism. So something like:

Implementatin of the base class Block: Can stay roughly the same bout you add a virtual method draw(...) which is the common interface for all derived classes:

class Block {
  public:
    bool isFull = true;
    Vector2i texture = { 9, 1 };
    Vector2i topTexture = { 9, 1 };
    const char* sound;

    Block() {
      return;
    }
    Block(bool isFull, Vector2i const& texture, Vector2i const& topTexture, const char* sound)
    : isFull(isFull), texture(texture), topTexture(topTexture), sound(sound) {
      return;
    }
    Block(bool isFull, Vector2i const& texture, const char* sound)
    : isFull(isFull), texture(texture), topTexture(texture), sound(sound) {
      return;
    }
    Block(bool const& isFull, Vector2i const& texture)
    : isFull(isFull), texture(texture), topTexture(texture) {
      return;
    }

    // Virtual method that every derived class should override
    // Could contain default behaviour but here I declared it as pure virtual method (therefore the = 0)
    // Didn't know the data types for chunk and vertices so I used Chunk and Vertices respectively
    virtual void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) = 0;
};

The different types of blocks are introduced as derived classes that inherit the constructor (or you might implement a new one as well) and override the behaviour of the draw(...) class. If you are not planning to inherit from this derived class then you can mark it as final or if you won't be overriding draw in a derived class you can mark only draw as final

class Empty: public Block {
  public:
    using Block::Block; // Use the constructor of the base class
    
    // Overwrite behaviour of the base class here
    void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
      return;
    }
 };

 class FullBlock: public Block {
  public:
    using Block::Block;

    void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
      // Move contents of BuildBlock here
      BuildBlock(chunk, vertices, x, y, z);
      return;
    }
 };

 class Cross final: public Block {
  public:
    using Block::Block;

    void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
      // Move contents of FillCross here! No need to pass blocks[i] or rewrite FillCross to take something else than a Block, e.g. a std::shared_ptr<Block>
      FillCross(vertices, *this, x + chunk->x * chunkWidth, y, z + chunk->z * chunkWidth);
      return;
    }
 };

 class Hash final: public Block {
  public:
    using Block::Block;

    void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
      // Same here
      FillHash(vertices, *this, x + chunk->x * chunkWidth, y, z + chunk->z * chunkWidth);
      return;
    }
 };

Then you add all the blocks as std::shared_ptr or better std::unique_ptr if the resources are not shared! (a wrapper for a plain pointer from #include <memory>)

// Consider using std::unique_ptr if you are not using the individual objects outside of the std::vector
std::vector<std::shared_ptr<Block>> blocks = {};
blocks.reserve(64);

auto air = std::make_shared<Empty>(false, {0 ,0});
blocks.emplace_back(air);

auto grass = std::make_shared<FullBlock>(true, { 3, 0 }, { 0, 0 }, "Audio/grass1.ogg");
blocks.emplace_back(grass);

auto stone = std::make_shared<FullBlock>(true, { 1, 0 }, "Audio/stone1.ogg");
blocks.emplace_back(stone);

auto rose = std::make_shared<Cross>(false, { 12 ,0 }, "Audio/grass1.ogg");
blocks.emplace_back(rose);

auto wheat = std::make_shared<Hash>(false, { 8 ,3 });
blocks.emplace_back(wheat);

You can call then the implementation of the different derived classes as follows:

for (int x = 0; x < chunkWidth; x++) {
  for (int y = 0; y < chunkHeight; y++) {
    for (int z = 0; z < chunkWidth; z++) {
      if (IsDrawable[x][y][z] == 1) {
        blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]]->draw(chunk, vertices, x, y, z, chunkWidth);
      }
    }
  }
}

Here I put together a simplified working example to play around with in an online compiler.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...