В прошлый раз мы закончили с Моделью тетриса. Однако чтобы получить игру, одной логики мало. Требуется взаимодействие с пользователем и координация действий приложения. Первую задачу решает Представление, а вторую — Контроллер. Ими мы и займемся.
Предыдущие части:
- Тетрис на C++: Введение
- Тетрис на C++: Статическая модель
- Тетрис на C++: Обнаружение столкновений
- Тетрис на C++: Динамическая модель
- Тетрис на C++: Соблюдение правил
Представление для тетриса
Представление находится на передовой приложения. Именно с ним взаимодействует пользователь. Представление обеспечивает:
- Визуализацию данных Модели;
- Перенаправление действий пользователя Контроллеру.
Чтобы соблюдать чистоту паттерна MVC, придерживайтесь простого правила: Представление не меняет состояние Модели. Поэтому если заметили, что Представление использует не- const функцию-член Модели, то что-то не так.
Визуализация данных Модели
В качестве базового класса Представления используем обычный QWidget. Не самое удачное решение для игрового приложения, но вполне уместное для демонстрации принципов. Неплохим выбором на этот случай является QGLWidget. Возможно, в будущем мы до него еще дойдем.
Отрисовку Модели выполняем с помощью QPainter в обработчике paintEvent():
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
void TetrisView::paintEvent( QPaintEvent* ) { QPainter painter( this ); // Очищаем игровое поле painter.fillRect( 0, 0, m_width, m_height, BACKGROUND_COLOR ); if( DEBUG ) { // Рисуем сетку игрового поля при включенном режиме отладки painter.setPen( DEBUG_GRID_COLOR ); for( int x = BLOCK_SIZE_PIXELS; x < m_width; x += BLOCK_SIZE_PIXELS ) { painter.drawLine( x, 0, x, m_height ); } for( int y = BLOCK_SIZE_PIXELS; y < m_height; y += BLOCK_SIZE_PIXELS ) { painter.drawLine( 0, y, m_width, y ); } } // Рисуем блоки игрового поля for( int x = 0; x < m_model.getWidthBlocks(); ++x ) { for( int y = 0; y < m_model.getHeightBlocks(); ++y ) { drawBlock( blocksToPoints( x ) + HALF_BLOCK_SIZE, blocksToPoints( y ) + HALF_BLOCK_SIZE, m_model.getBlockType( x, y ), &painter ); } } // Рисуем активный элемент const TetrisItem& item = m_model.getItem(); for( int x = 0; x < item.getSizeBlocks(); ++x ) { for( int y = 0; y < item.getSizeBlocks(); ++y ) { drawBlock( item.getBlockXPoints( x ), item.getBlockYPoints( y ), item.getBlockType( x, y ), &painter ); } } } void TetrisView::drawBlock( int xPoints, int yPoints, int type , QPainter* painter ) { static const std::vector< QColor > COLOR_TABLE = { Qt::white, Qt::yellow, Qt::green, Qt::red, Qt::cyan, Qt::magenta, Qt::lightGray }; if( type <= 0 || COLOR_TABLE.size() < type ) { return; } int xPixels = modelPointsToPixels( xPoints ) - HALF_BLOCK_SIZE_PIXELS; int yPixels = modelPointsToPixels( yPoints ) - HALF_BLOCK_SIZE_PIXELS; painter->fillRect( xPixels, yPixels, BLOCK_SIZE_PIXELS, BLOCK_SIZE_PIXELS, COLOR_TABLE[ type - 1 ] ); } |
Реализация Представления, отвечающая за визуализацию, решает 3 подзадачи:
- Рисует сетку игрового поля, полезную в режиме отладки;
- Рисует каждый блок игрового поля;
- Рисует каждый блок активного элемента.
Для рисования блоков используется отдельная функция drawBlock(), которая принимает координаты середины блока в точках, тип блока и указатель на QPainter. Каждому типу блока ставится в соответствие некоторый цвет. Проще всего для этого использовать таблицу цветов, представленную вектором. В этом случае для типа блока type цвет выбирается следующим образом: COLOR_TABLE[ type - 1 ].
Одним из способов вызова paintEvent() является слот repaint(). Однако не будем привязываться к нему жестко и создадим свою обертку:
1 2 3 |
void TetrisView::refresh() { repaint(); } |
В нашем случае такое решение является избыточным, но для мест стыка между компонентами MVC это необходимо. Ведь на то он и MVC, что интерфейс Представления должен быть независимым (конечно, для этого нужно ввести базовый абстрактный класс, что мы не делали для краткости). Кроме того, требования постоянно меняются и реализация функции refresh() в какой-то момент может усложниться даже при наличии единственного класса Представления.
Вызов refresh() в нужные моменты обеспечит Контроллер (альтернатива — паттерн Наблюдатель в рамках Модели). При этом мы не мелочимся, а перерисовываем все поле каждый раз.
Перенаправление действий пользователя Контроллеру
Тетрис — не самая сложная игра в плане управления. Свяжем события нажатий нужных клавиш с соответствующими обработчиками Контроллера:
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 27 28 29 30 31 32 33 34 |
void TetrisView::keyPressEvent( QKeyEvent* e ) { switch( e->key() ) { case Qt::Key_Left: m_controller->onMoveLeft(); break; case Qt::Key_Right: m_controller->onMoveRight(); break; case Qt::Key_Up: m_controller->onRotate(); break; case Qt::Key_Down: m_controller->onDropEnabled( true ); break; case Qt::Key_Space: m_controller->onTogglePause(); break; case Qt::Key_Escape: m_controller->onStart(); break; default: QWidget::keyPressEvent( e ); } } void TetrisView::keyReleaseEvent( QKeyEvent* e ) { switch( e->key() ) { case Qt::Key_Down: m_controller->onDropEnabled( false ); break; default: QWidget::keyReleaseEvent( e ); } } |
Все изменения в Модели должны происходить только через Контроллер.
Контроллер для тетриса
Контроллер в рамках MVC представляет собой прослойку между Представлением и Моделью, когда речь заходит о действиях пользователя. При правильном проектировании реализация функций Контроллера представляется тривиальной задачей:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
TetrisController::TetrisController( TetrisModel* model , TetrisView* view, QObject* parent ) : QObject( parent ), m_model( model ), m_view( view ) { connect( &m_timer, SIGNAL( timeout() ), SLOT( onStep() ) ); } void TetrisController::onStart() { m_model->resetGame(); onResume(); } void TetrisController::onStep() { m_model->doStep(); m_view->refresh(); if( m_model->isGameOver() ) { qDebug() << m_model->getScore(); m_model->resetGame(); } } void TetrisController::onPause() { m_timer.stop(); } void TetrisController::onResume() { m_timer.start( STEP_TIME_INTERVAL ); } void TetrisController::onMoveLeft() { onAction( &TetrisModel::moveItemLeft ); } void TetrisController::onMoveRight() { onAction( &TetrisModel::moveItemRight ); } void TetrisController::onRotate() { onAction( &TetrisModel::rotateItem ); } void TetrisController::onDropEnabled( bool enabled ) { onAction( enabled ? &TetrisModel::startDrop : &TetrisModel::stopDrop ); } void TetrisController::onTogglePause() { m_timer.isActive() ? onPause() : onResume(); } void TetrisController::onAction( void ( TetrisModel::*action )() ) { if( !m_timer.isActive() ) { return; } ( m_model->*action )(); m_view->refresh(); } |
Здесь следует обратить внимание на следующие моменты:
- Вызов onStep() происходит по событию таймера QTimer. В onStep() выполняется обновление состояния модели с помощью doStep(). Таким образом, в Модели отсутствует понятие паузы. Пауза достигается путем остановки таймера. С одной стороны, это не очень хорошо, поскольку Контроллеру не желательно иметь собственного состояния. С другой стороны, паттерны проектирования не являются жесткими решениями, поэтому всегда в первую очередь руководствуйтесь здравым смыслом;
- Также в onStep() проверяется не окончена ли игра. Если игра окончена, то на консоль выводится счет, и игра перезапускается;
- Обработка игровых действий выполняется с помощью закрытой функции onAction(), которая принимает указатель на функцию-член Модели. Это удобно, поскольку в обработке действий много общего. Например, если игра приостановлена, то действие выполнять не требуется. К тому же, после выполнения действия не помешает обновить Представление, чтобы оно отражало актуальное состояние Модели.
Заключение
На этом мы завершаем цикл статей, посвященных разработке тетриса на C++. Нельзя сказать, что мы реализовали законченную игру. Ей не хватает еще многих элементов: таблицы лучших результатов, графических и звуковых эффектов, и т.д. Однако то, что у нас получилось, является хорошей основой. А принятые проектные решения обеспечивают относительно простое расширение функционала.