Рекомендую сначала ознакомиться с вводной заметкой, посвященной реализации тетриса на C++, если вы еще этого не сделали.
Игровое поле тетриса
Игровое поле тетриса представляет собой простой двумерный массив или матрицу. На C++ для этого можно использовать следующую структуру:
1 |
typedef std::vector< std::vector< int > > Matrix; |
Каждый элемент матрицы, представляющей игровое поле, определяет состояние соответствующего «блока» на некоторой позиции (x; y). Под блоками будем понимать клетки игрового поля. Для самих состояний достаточно иметь всего 3 условных обозначения:
- Пустой блок: подойдет значение ;
- Занятый блок: 1, 2, и т.д.;
- Блок за пределами игрового поля: -1.
Конечно, использование «волшебных чисел» не поощряется, и правильнее было бы использовать именованные константы, однако мы разрабатываем конкретную игру, в которой вполне уместно обойтись простыми численными значениями. Нет смысла усложнять и нагромождать реализацию.
Как видно на рисунке слева, каждому числовому обозначению мы можем назначить некоторый цвет. Однако этот аспект не касается Модели, а полностью зависит от Представления, поэтому сейчас мы на нем останавливаться не будем.
Изображенному игровому полю на C++ соответствует следующая матрица:
1 2 3 4 5 6 7 8 9 |
Matrix fieldMatrix = { { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 }, { 0, 0, 0, 7, 0 }, { 0, 0, 0, 7, 7 }, { 0, 0, 0, 5, 7 }, { 0, 0, 5, 5, 5 }, }; |
Для вывода на экран текущего состояния игрового поля нам требуется лишь доступ к его размерности и элементам. Обратите внимание, что если запрос элементов организовать на манер Представления, то есть по координатам (а не рядам и столбцам), то возникает небольшая путаница. Элемент fieldMatrix[ x ][ y ] на самом деле соответствует элементу из x-го ряда (ходим вверх-вниз) и y-го столбца (ходим влево-вправо). То есть координаты оказываются перепутаны, поэтому для исправления ситуации приходится использовать конструкцию вида fieldMatrix[ y ][ x ].
Игровые элементы тетриса
На этом можно было бы считать, что статическая модель тетриса практически готова, но не так быстро. До сих пор мы не учитывали, что существует еще и «активный элемент», которым может управлять игрок. Организация внутренней системы координат элемента ничем не отличается от того, что мы уже разобрали для игрового поля. Но в отличие от блоков, относящихся к полю, он имеет логику движения. Здесь нам приходится немного забегать вперед и учитывать динамику поведения элементов, поскольку мы должны уметь зафиксировать любое промежуточное состояние игры, чтобы Представление могло корректно провести отрисовку.
По оси x элемент может быть сдвинут лишь на расстояние, эквивалентное величине блока игрового поля. Однако при своем падении, для обеспечения плавности перемещения, элемент смещается не на целый блок, а на какую-то его долю. Поэтому возникает необходимость ввести еще одну единицу измерения. Условно назовем ее «точкой». И скажем, что блок состоит из точек.
Таким образом, активный элемент должен позиционироваться с помощью точек, а не блоков для получения приемлемой точности. Например, для представления L-образного элемента вновь используем матричную структуру:
1 2 3 4 5 |
Matrix itemMatrix = { { 0, 3, 0 }, { 0, 3, 0 }, { 0, 3, 3 }, }; |
Понятно, что таким же образом можно определить любой стандартный (и не очень) элемент тетриса. Позиция элемента привязывается к его середине. Поэтому координаты остальных блоков элемента выражаются как смещение относительно этой центральной позиции. В предложенной мной реализации для класса TetrisItem это выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Получение x-координаты в точках для блока с внутренней координатой по оси x int TetrisItem::getBlockXPoints( int innerXBlocks ) const { int innerXPoints = blocksToPoints( innerXBlocks ) + HALF_BLOCK_SIZE; int innerXCenterPoints = blocksToPoints( getSizeBlocks() ) / 2; return m_xPoints - innerXCenterPoints + innerXPoints; } // Получение y-координаты в точках для блока с внутренней координатой по оси y int TetrisItem::getBlockYPoints( int innerYBlocks ) const { int innerYPoints = blocksToPoints( innerYBlocks ) + HALF_BLOCK_SIZE; int innerYCenterPoints = blocksToPoints( getSizeBlocks() ) / 2; return m_yPoints - innerYCenterPoints + innerYPoints; } // Извлечение типа блока по внутренним координатам int TetrisItem::getBlockType( int innerXBlocks, int innerYBlocks ) const { if( innerXBlocks < 0 || getSizeBlocks() <= innerXBlocks || innerYBlocks < 0 || getSizeBlocks() <= innerYBlocks ) { return 0; } return m_matrix[ innerYBlocks ][ innerXBlocks ]; } |
Заключение
На уровне статики можно считать, что игровое поле и активный элемент живут в параллельных вселенных. Для Представления это не имеет никакого значения, поскольку ему достаточно уметь рисовать блоки по заданным координатам в точках, что мы уже обеспечили. Модель должна сама гарантировать, что состояние игры остается корректным в каждый момент времени. А это достигается путем введения обнаружения столкновений, о чем мы поговорим в следующий раз…