Предыдущие части:
В прошлый раз мы закончили с Моделью тетриса. Однако чтобы получить игру, одной логики мало. Требуется взаимодействие с пользователем и координация действий приложения. Первую задачу решает Представление, а вторую - Контроллер. Ими мы и займемся.
Представление находится на передовой приложения. Именно с ним взаимодействует пользователь. Представление обеспечивает:
Чтобы соблюдать чистоту паттерна MVC, придерживайтесь простого правила: Представление не меняет состояние Модели. Поэтому если заметили, что Представление использует не-const
функцию-член Модели, то что-то не так.
В качестве базового класса Представления используем обычный QWidget
. Не самое удачное решение для игрового приложения, но вполне уместное для демонстрации принципов. Неплохим выбором на этот случай является QGLWidget
. Возможно, в будущем мы до него еще дойдем.
Отрисовку Модели выполняем с помощью QPainter
в обработчике paintEvent()
:
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()
. Однако не будем привязываться к нему жестко и создадим свою обертку:
void TetrisView::refresh() {
repaint();
}
В нашем случае такое решение является избыточным, но для мест стыка между компонентами MVC это необходимо. Ведь на то он и MVC, что интерфейс Представления должен быть независимым (конечно, для этого нужно ввести базовый абстрактный класс, что мы не делали для краткости). Кроме того, требования постоянно меняются и реализация функции refresh()
в какой-то момент может усложниться даже при наличии единственного класса Представления.
Вызов refresh()
в нужные моменты обеспечит Контроллер (альтернатива - паттерн Наблюдатель в рамках Модели). При этом мы не мелочимся, а перерисовываем все поле каждый раз.
Тетрис - не самая сложная игра в плане управления. Свяжем события нажатий нужных клавиш с соответствующими обработчиками Контроллера:
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 представляет собой прослойку между Представлением и Моделью, когда речь заходит о действиях пользователя. При правильном проектировании реализация функций Контроллера представляется тривиальной задачей:
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++. Нельзя сказать, что мы реализовали законченную игру. Ей не хватает еще многих элементов: таблицы лучших результатов, графических и звуковых эффектов, и т.д. Однако то, что у нас получилось, является хорошей основой. А принятые проектные решения обеспечивают относительно простое расширение функционала.