Я пытаюсь сделать игровую доску из двумерного массива Tile
объектов, класса, который я создал. Это выглядит примерно так:
class Board {
private:
Tile m_board[9][9]
public:
Board();
//Constructors, functions, etc
};
class Tile {
private:
char m_type;
//other class members, etc
public:
Tile();
char getType();
void activate(); //This does nothing.
};
class BreakTile: public Tile {
public:
BreakTile();
void activate(); //This does something.
Я хочу, чтобы я мог подойти к любому Tile
на доске, проверить, является ли это BreakTile
, и если да, то активировать его. У меня есть функция в классе Board
, которая выглядит примерно так:
void Board::activate(int y, int x)
{
if (m_board[y][x].getType() == 'B')
{
m_board[y][x].activate();
}
}
И это может активировать BreakTile
s. Однако все, что он делает, это вызывает функцию activate()
из класса Tile
- предположительно потому, что это массив Tile
- несмотря на то, что в конструкторе Board
я определяю некоторые плитки как BreakTile
, поэтому я знаю, какие плитки ищу. здесь BreakTile
s. Я просто не знаю, как заставить компьютер это распознать.
Я пытался посмотреть, смогу ли я каким-то образом преобразовать Tile
в BreakTile
, но понятия не имею, как мне вообще начать это делать. Я новичок в C++.
Это не минимальный воспроизводимый пример .
Если вы сделаете activate()
virtual
, вам даже не нужно будет проверять, BreakTile
ли это. Просто позвоните activate()
. В классе Tile
activate()
ничего не будет делать, а в BreakTime
он будет делать все, что вы захотите.
Если вы присвоите BreakTile
элементу в m_board
, он будет нарезан. Вам необходимо использовать указатели (или интеллектуальные указатели) в качестве элементов для хранения любых производных элементов. Это также позволит полиморфизм, как упоминалось выше.
Создайте виртуальный базовый класс class TileItf { virtual ~TileItf() = default; virtual void activate() = 0; };
(это хорошая практика для модульного тестирования и внедрения зависимостей позже). Затем извлеките из этого Tile и добавьте void activate() override
к своему Tile. (Также ознакомьтесь с основным правилом C++ 128: виртуальные функции должны иметь значения virtual, override или Final
После этого внутренняя структура данных вашей доски может стать std::array<std::array<std::unique_ptr<TileItf>,9>,9>
, и вы сможете создавать плитки и добавлять их с помощью m_board[1][1] = std::make_unique<BreakTile>();
(при условии, что BreakTile также является производным от TileItf). Также вы можете создать класс «EmptyTile», который ничего не делает при вызове активации, и инициализировать вашу доску с его помощью (шаблон нулевой стратегии, таким образом вам не понадобятся if
операторы, чтобы проверить, пусто ли поле)
Обратите внимание: то, о чем говорит @PepijnKramer, чаще называют «абстрактным базовым классом», а не «виртуальным базовым классом». Если вы хотите узнать о них больше, это, вероятно, даст лучшие результаты.
@PepijnKramer: На самом деле это не поправка - я не думаю, что «виртуальный базовый класс» неправильный. Но я почти уверен, что видел «абстрактный базовый класс» чаще.
Создайте виртуальный базовый класс TileItf
(это хорошая практика для модульного тестирования и внедрения зависимостей позже). Затем извлеките Tile
из этого и добавьте к нему виртуальный activate()
(также ознакомьтесь с C++ Core Guideline 128, виртуальные функции должны иметь virtual, override или Final).
После этого внутренняя структура данных вашей доски может стать двумерным массивом std::unique_ptr<TileItf>
объектов, и вы можете создавать плитки и добавлять их, используя:
m_board[1][1] = std::make_unique<BreakTile>();
(при условии, что BreakTile
также является производным от TileItf
).
Кроме того, вы можете создать класс EmptyTile
, который ничего не делает при вызове activate()
, и инициализировать свою доску с его помощью (шаблон нулевой стратегии, таким образом вам не понадобятся операторы if
для проверки, пусто ли поле).
Вот пример:
#include <array>
#include <memory>
#include <iostream>
// First define an abstract base class (or "interface")
class TileItf
{
public:
virtual ~TileItf() = default;
virtual void activate() = 0;
};
// then create overloads for any kind of tile you can have
class EmptyTile : public TileItf
{
void activate() override { std::cout << "."; }
};
class OTile : public TileItf
{
void activate() override { std::cout << "O"; }
};
class XTile : public TileItf
{
void activate() override { std::cout << "X"; }
};
// The board now will contain a 2D array of interfaces to tiles.
// in C++ we don't use new/delete by default anymore, but use containers or smart pointers
class Board
{
public:
Board()
{
// intitialize the board with empty tiles
for (auto& row : m_tiles)
{
for (auto& tile : row)
{
tile = std::make_unique<EmptyTile>();
}
}
// this next line looks simple but it does a few things
// it will destroy the tile at the current position and assign a new tile to it.
m_tiles[0][1] = std::make_unique<OTile>();
m_tiles[1][1] = std::make_unique<XTile>();
}
void activate()
{
// range based for loops (cannot go out of scope)
for (const auto& row : m_tiles)
{
for (const auto& tile : row)
{
tile->activate(); // <== here the derived activate function will be called depending on the concrete tile type
}
std::cout << "\n";
}
}
private:
// Because tiles are polymorphic we need pointers to them (or you get the object slicing that was talked about before)
std::array<std::array<std::unique_ptr<TileItf>, 3>, 3> m_tiles;
};
int main()
{
Board board;
board.activate();
}
Используйте
virtual
. Кроме того, полиморфизм работает только через указатели/ссылки.