Предыдущие части:
Правила тетриса довольно просты:
А теперь посмотрим, как все эти проверки можно записать с помощью кода C++.
При инициализации модели активный игровой элемент находится в нейтральном состоянии с пустой внутренней матрицей (см. Паттерн Null Object). Такая проверка выполняется с помощью функции-члена isNull()
:
bool TetrisItem::isNull() const {
return m_matrix.empty();
}
Формирование нового элемента выполняем в начале функции-члена doStep()
, которая вызывается для каждого игрового шага (по таймеру из Контроллера):
void TetrisModel::doStep() {
if( m_activeItem.isNull() ) {
m_activeItem = TetrisItem::generateRandom();
int xPoints = blocksToPoints( getWidthBlocks() / 2 );
if( m_activeItem.getSizeBlocks() % 2 == 1 ) {
// Если элемент состоит из нечетного числа блоков, то выравниваем его по сетке:
xPoints += HALF_BLOCK_SIZE;
}
}
// А это осталось еще с прошлого раза:
TetrisItem item = m_activeItem;
item.setPosition( m_activeItem.getXPoints(), m_activeItem.getYPoints() + m_speed );
if( !hasCollisions( item ) ) {
m_activeItem = item;
} else {
while( hasCollisions( item ) ) {
item.setPosition( item.getXPoints(), item.getYPoints() - 1 );
}
m_activeItem = item;
}
}
Формирование случайного элемента осуществляется в функции TetrisItem::generateRandom()
:
TetrisItem TetrisItem::generateRandom() {
// Для краткости не будем записывать все возможные элементы
static const std::vector< TetrisItem > ITEMS = {
TetrisItem( {
{ 1, 1 },
{ 1, 1 },
} ),
// И т.д.
};
int type = rand() % ITEMS.size();
return ITEMS[ type ];
}
В функцию doStep()
удобно добавить и проверку на окончание игры:
void TetrisModel::doStep() {
if( m_activeItem.isNull() ) {
// Убрано для краткости…
if( hasCollisions( m_activeItem ) ) {
// Если элемент сразу имеет столкновения, то игра окончена
m_gameOver = true;
}
}
if( isGameOver() ) {
// Если игра окончена, то продолжать нет смысла
return;
}
// Убрано для краткости…
}
Спросить у модели о том, не окончена ли игра, можно с помощью isGameOver()
:
bool TetrisModel::isGameOver() const {
return m_gameOver;
}
Когда нижняя часть активного элемента во что-то упирается, то мы позволяем ему немного "поползать":
void TetrisModel::doStep() {
if( m_activeItem.isNull() ) {
// Сбрасываем счетчик касаний при появлении нового элемента
m_itemBottomTouchCounter = 0;
m_activeItem = TetrisItem::generateRandom();
int xPoints = blocksToPoints( getWidthBlocks() / 2 );
if( m_activeItem.getSizeBlocks() % 2 == 1 ) {
// Если элемент состоит из нечетного числа блоков, то выравниваем его по сетке:
xPoints += HALF_BLOCK_SIZE;
}
m_activeItem.setPosition( xPoints, 0 );
if( hasCollisions( m_activeItem ) ) {
m_gameOver = true;
}
}
if( isGameOver() ) {
return;
}
TetrisItem item = m_activeItem;
item.setPosition( m_activeItem.getXPoints(), m_activeItem.getYPoints() + m_speed );
if( !hasCollisions( item ) ) {
m_activeItem = item;
// Если столкновений нет, то сбрасываем счетчик касаний
m_itemBottomTouchCounter = 0;
} else {
while( hasCollisions( item ) ) {
item.setPosition( item.getXPoints(), item.getYPoints() - 1 );
}
if( MAX_TOUCH_COUNT < m_itemBottomTouchCounter ) {
// Если количество касаний превысило некий разумный предел, то элемент фиксируется
m_activeItem = TetrisItem();
} else {
m_activeItem = item;
// Иначе значение счетчика касаний наращивается
++m_itemBottomTouchCounter;
}
}
}
Для поддержки функции перемещения по дну нам пришлось добавить счетчик касаний. Каждый раз когда элемент касается непустого блока своей нижней частью, значение счетчика увеличивается на единицу. Как только значение счетчика достигает определенной границы, элемент фиксируется (точнее, пока что просто исчезает). Если же игрок перемещает элемент (влево или вправо) таким образом, что он вновь начинает падать, то значение счетчика сбрасывается.
Нас не устраивает, что сейчас активный элемент просто исчезает после падения. Сделаем так, чтобы все его блоки стали частью игрового поля:
void TetrisModel::doStep() {
// Убрано для краткости…
if( !hasCollisions( item ) ) {
// Убрано для краткости…
} else {
// Убрано для краткости…
if( MAX_TOUCH_COUNT < m_itemBottomTouchCounter ) {
m_activeItem = TetrisItem();
for( int xBlocks = 0; xBlocks < item.getSizeBlocks(); ++xBlocks ) {
for( int yBlocks = 0; yBlocks < item.getSizeBlocks(); ++yBlocks ) {
// Для каждого блока элемента
int blockType = item.getBlockType( xBlocks, yBlocks );
if( blockType != 0 ) {
// Если блок не пустой
int xPoints = item.getBlockXPoints( xBlocks );
int yPoints = item.getBlockYPoints( yBlocks );
// Фиксируем тип блока в ячейке матрицы игрового поля
m_fieldMatrix[ yPoints / BLOCK_SIZE ][ xPoints / BLOCK_SIZE ] = blockType;
}
}
}
} else {
// Убрано для краткости…
}
}
}
Код достаточно очевиден. Замечу лишь, что он работает потому, что активный элемент гарантировано оказывается расположен по сетке игрового поля. В результате деление координат для каждого его блока в точках на размер блока дают нужные координаты игрового поля.
Модель практически закончена. Остается добавить всего несколько заключительных штрихов. Вот алгоритм очистки игрового поля от заполненных рядов:
void TetrisModel::clean() {
for( int i = m_heightBlocks - 1; i >= 0; --i ) {
// Для каждого ряда начиная с последнего
// Считаем количество заполненных блоков
int counter = std::accumulate(
m_fieldMatrix[ i ].begin(),
m_fieldMatrix[ i ].end(),
0,
[]( int a, int b ) { return ( b == 0 ) ? a : a + 1; }
);
if( counter == 0 ) {
// Если все блоки пустые, то выше идти смысла нет
return;
} else if( counter == getWidthBlocks() ) {
// Иначе если все блоки заполнены, то удаляем ряд…
m_fieldMatrix.erase( m_fieldMatrix.begin() + i );
std::vector< int > v( getWidthBlocks() );
// … и вставляем пустой в самый верх
m_fieldMatrix.insert( m_fieldMatrix.begin(), v );
// А еще наращиваем количество набранных очков
incScore();
++i;
}
}
}
Вызывать clean()
нужно сразу же после кода фиксирования элемента:
void TetrisModel::doStep() {
// Убрано для краткости…
if( !hasCollisions( item ) ) {
// Убрано для краткости…
} else {
// Убрано для краткости…
if( MAX_TOUCH_COUNT < m_itemBottomTouchCounter ) {
// Убрано для краткости…
// Чистим игровое поле
clean();
} else {
// Убрано для краткости…
}
}
}
За каждый убранный ряд количество набранных очков увеличивается на единицу. За каждые набранные 10 очков сложность игры возрастает (путем наращивания скорости на единицу):
void TetrisModel::incScore() {
++m_score;
if( m_score % SCORE_COUNT_FOR_NEXT_LEVEL == 0 ) {
incSpeed();
}
}
void TetrisModel::incSpeed() {
if( m_speed < MAX_SPEED ) {
++m_speed;
}
}
На самом деле, функция incScore()
получилась не самой удачной (см. Принцип единой ответственности): она имеет побочный эффект (увеличивает скорость). Однако для такой простой игры это оказалось не критично. Возможно, что в более сложном проекте пришлось бы ее переработать.
Скорость падения элемента может меняется не только в результате вызова incSpeed()
, но и по воле игрока. Для этого усовершенствуем нашу модель следующим образом:
// Включает ускоренный режим
void TetrisModel::startDrop() {
m_dropEnabled = true;
}
// Отключает ускоренный режим
void TetrisModel::stopDrop() {
m_dropEnabled = false;
}
void TetrisModel::doStep() {
// Убрано для краткости…
TetrisItem item = m_activeItem;
int speed = m_dropEnabled ? MAX_SPEED : m_speed;
item.setPosition( m_activeItem.getXPoints(), m_activeItem.getYPoints() + speed );
// Убрано для краткости…
}
На этом мы завершаем знакомство с Моделью игры тетрис. А в следующий раз переходим к краткому обзору реализаций Представления и Контроллера…