Для лучшего понимания материала рекомендую сначала познакомиться с первыми двумя частями:
В прошлый раз мы оставили нашу игру в таком состоянии, когда в Модели уже можно скомпоновать любое корректное состояние. Хорошее начало, учитывая, что для этого нам потребовалось всего лишь прибегнуть к двум двумерным массивам для игрового поля и активного элемента, а также определить простую систему координат с началом в левом верхнем углу игрового поля.
Например, допустим, что игровое поле представлено матрицей:
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 }, }; |
А активный элемент имеет координаты (18; 24) (соответствует центру элемента) в условных точках, и определяется следующей матричной формой:
1 2 3 4 5 |
Matrix itemMatrix = { { 0, 3, 0 }, { 0, 3, 0 }, { 0, 3, 3 }, }; |
Представление для такого состояния Модели приведено на рисунке слева (в предположении, что 1 блок = 12 точек, как в предложенной реализации на C++).
Для активного элемента может произойти три принципиально отличных действия:
- Движение по оси абсцисс (влево-вправо);
- Движение по оси ординат (только вниз);
- Вращение против часовой стрелки.
Хоть они и разные, но их обработка отличается друг от друга не сильно. Однако сначала у вас может возникнуть естественное желание предотвратить выход элемента за границы игрового поля. Эта идея выглядит достаточно заманчиво, поскольку границы поля легко определить и все проверки сведутся к простым неравенствам. Однако если глубже проанализировать задачу, которая перед нами стоит, то становится понятно, что это бессмысленная затея. Ведь нам в любом случае придется думать не только о границах игрового поля, но и о занятых позициях, которые могут образовывать множество различных игровых ситуаций.
Лучше сразу принять следующий универсальный алгоритм: изменение положения элемента в рамках игрового поля допустимо лишь тогда, когда его новая позиция является корректной, то есть все его непустые блоки будут расположены на пустых блоках игрового поля. В такой интерпретации вокруг игрового поля появляется некая условная граница, для обозначения элементов которой в прошлый раз мы выбрали символы -1. Таким образом мы плавно перешли к задаче обнаружения столкновений.
На верхнем уровне алгоритм проверки активного элемента на столкновение с занятыми блоками игрового поля выглядит довольно просто: столкновение имеется, если хотя бы для одного из блоков активного элемента нашлось пересечение с занятым блоком игрового поля, иначе столкновений нет:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool TetrisModel::hasCollisions( const TetrisItem& item ) const { for( int xBlocks = 0; xBlocks < item.getSizeBlocks(); ++xBlocks ) { for( int yBlocks = 0; yBlocks < item.getSizeBlocks(); ++yBlocks ) { if( item.getBlockType( xBlocks, yBlocks ) > 0 && hasCollisions( item.getBlockXPoints( xBlocks ), item.getBlockYPoints( yBlocks ) ) ) { return true; } } } return false; } |
Проверка на пересечение блоков выполняется не намного сложнее. Вспомним, что мы делали допущение, по которому элемент всегда ходит влево-вправо точно по сетке игрового поля ровно на один блок, однако это допущение не распространяется на падение элемента вниз, где элемент (и каждый его блок) двигается уже не по сетке (смещение происходит не на блок, а на какое-то количество точек в зависимости от текущей скорости падения). Для заданных координат центра любого блока (указанных в точках) имеем следующий алгоритм:
- Переводим переданную x-координату из точек в блоки, однозначно получая нужную координату игрового поля по построению (достигается простым целочисленным делением);
- Находим y-координату верхней границы переданного блока в точках, которую по аналогии с x-координатой выше переводим в координату в блоках игрового поля;
- Если блок игрового поля, расположенный в найденной выше позиции, занят, то столкновение имеется;
- Иначе находим y-координату нижней границы переданного блока в точках, которую переводим в координату в блоках игрового поля;
- Если y-координата верхней или нижней границы расположена не по сетке (не делится без остатка на размер блока в точках) и блок игрового поля, расположенный в позиции, соответствующей нижней границе блока элемента, занят, то столкновение есть;
- Иначе столкновений нет.
На C++ этот алгоритм можно записать следующим образом (код получился не самым оптимальным, но более или менее однородным, поэтому можете упростить его, если захотите):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
bool TetrisModel::hasCollisions( int xPoints, int yPoints ) const { int xBlocks = ( xPoints < 0 ) ? -1 : xPoints / BLOCK_SIZE; int yTopPoints = yPoints - HALF_BLOCK_SIZE; if( getBlockType( xBlocks, yTopPoints / BLOCK_SIZE ) ) { return true; } int yBottomPoints = yPoints + HALF_BLOCK_SIZE; if( yTopPoints % BLOCK_SIZE != 0 && getBlockType( xBlocks, yBottomPoints / BLOCK_SIZE ) ) { return true; } return false; } |
Теперь в нашем распоряжении имеется мощная функция hasCollisions(), которая всегда поможет узнать о том, допустимо ли некое состояние элемента или нет. А то, как ей пользоваться и реализовать с ее помощью динамическую модель тетриса, мы обсудим в следующий раз…