Алгоритм генерации сетки для воксельной игры

В настоящее время я делаю воксельную игру, такую ​​​​как Minecraft, для развлечения с DirectX11. Игра работает с системой фрагментов, как и любая другая воксельная игра, но мой текущий алгоритм создания сетки фрагментов не расширяем. Класс блока имеет несколько атрибутов, таких как полный блок и тип сетки.

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) {}

};

Затем каждый блок инициализируется вектором

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

У меня есть функция, которая принимает вектор данных вершин и фрагментов. Эта функция перебирает все блоки с несколькими оптимизациями и помещает данные обратно в вектор, который отправляется в буфер.

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;
                    }
                }
            }
        }
    }

С каждым новым типом меша оператор switch становится больше, и я думаю, что это не тот путь. Я спрашиваю, есть ли лучшие способы сделать это. Я благодарю вас заранее.


person VoidRune    schedule 25.04.2021    source источник


Ответы (1)


Я думаю, что создание разных производных классов с общим родительским классом Block — это правильный путь. Вы добавляете в него метод virtual, поведение которого переопределяется в производных классах. Затем вы помещаете их в полиморфный вектор std::shared_ptr<Block> и вызываете их. Если вы опасаетесь, что по какой-то причине это может быть слишком медленным, вы можете заменить виртуальные функции на Любопытно повторяющийся шаблон шаблона (CRTP) для достижения статического полиморфизма. Итак, что-то вроде:

Реализация базового класса Block: может оставаться примерно такой же, если вы добавите virtual метод draw(...), который является общим интерфейсом для всех производных классов:

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;
};

Различные типы блоков представлены как производные классы, которые наследуют конструктор (или вы также можете реализовать новый) и override поведение класса draw(...). Если вы не планируете наследовать от этого производного класса, вы можете пометить его как final или, если вы не будете переопределять draw в производном классе, вы можете пометить только draw как 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;
    }
 };

Затем вы добавляете все блоки как std::shared_ptr или лучше std::unique_ptr, если ресурсы не являются общими! (обертка для простого указателя из #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);

Затем вы можете вызвать реализацию различных производных классов следующим образом:

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);
      }
    }
  }
}

Здесь я собрал упрощенный рабочий пример, чтобы поиграть с ним в онлайн-компиляторе.

person 2b-t    schedule 25.04.2021
comment
Получил свой голос, но предпочитаю std::unique_ptr, если на самом деле не требуется совместное владение. - person Ted Lyngmo; 25.04.2021