Механика казуальных игр
Раздел:
Software /
Игры
@
20.07.2011 |
Ключевые слова: игровой движок казуальные игры qt
Автор: sergrt Источник: habrahabr
На хабрахабре периодически предпринимаются попытки описания процесса игроделания с самых разных сторон — от воплощения 3D-графики до создания сетевых протоколов. Эти темы, безусловно, важны, однако довольно узкие. В данной статье я попробую использовать более широкий подход — рассмотрю принцип создания игрового движка для т.н. казуальных игр. Описываемая механика вполне подойдет для создания всяческих пакманов, арканоидов, платформеров и пр. Описание процесса будет на примере примитивного scrolldown шутера (из ностальгических чувств к Zybex и Xevious) — летаем по полю, сбиваем метеориты. Инструмент — Qt.
Сразу оговорюсь, что никаких красот и законченности в коде нет. Классы примитивны и повторяют код, функции неоптимальны, графика некрасивая никакая, зато это всё пыхтит и ворочается. Это — база, с которой можно работать дальше. Опытным программистам — пролистать за чашечкой чего-нибудь горячего, начинающим или вливающимся в тему возможно даст пищу для размышления. Начинаем.
Цикл приложения
Для того, чтобы правильно выбрать способ организации программы, нужно определиться с участниками главного цикла. В любой казуальной (и большинстве неказуальных) игре их минимум три:
- Тактовый генератор игрового процесса
- Симулятор непрерывности
- Рендеринг сцены
Возможны и другие пункты — менеджер анимации, искусственный интеллект и пр. Для примера нам вполне хватит этих трех.
Что это за такие участники?
Тактовый генератор игрового процесса — это привязанный к таймеру… эм… тактовый генератор игрового процесса. В нем контролируются перемещения объектов по игровому полю. Основное его назначение — обеспечение целостности игрового процесса и его одинаковость. Это очень важно — не только для того, чтобы скорость игры не зависела от производительности компьютера, но еще и для обеспечения синхронизации при игре по сети.
Симулятор непрерывности — это вспомогательные функции, основное назначение которых — следить, чтобы между вызовами генератора игрового процесса не произошло что-нибудь важное. Например, рассмотрим такой игровой момент:
Слева и справа изображены два последовательных вызова тактового генератора игрового процесса. Предположим, что скорость желтого круга = 3. Расстояние между кругом и прямоугольником, как видно из рисунка, = 2. Получается, что круг и прямоугольник так и не столкнутся, если им не помочь. Эту помощь и оказывает симулятор непрерывности.
Рендеринг сцены — тут вроде бы всё понятно. Он отдельным пунктом, так как:
- не должен зависеть от скорости игрового процесса;
- должен обеспечить плавность изображения (при скорости объекта = 10 точкам на экране и частоте игры = 30 будут видны рывки движущегося объекта, если рендерить кадры только в момент вызова генератора игрового процесса)
Возможные способы организации циклов
Сразу в голову приходит мысль сделать по отдельному потоку для каждого из участников. Однако такой подход не является оптимальным, так как:
- не обеспечивает по умолчанию синхронизацию между участниками цикла. Вдруг придется жертвовать рендерингом и анимацией ради поддержания синхронности сетевой баталии?
- значительно усложняет разработку и как следствие повышает количество ошибок. Разные потоки = разные ресурсы, вопросы синхронизации и совместного доступа, и прочие прелести многопоточных программ.
Поэтому потоки делать будем, но несколько иначе — отдельный поток на обработку сообщений от ОС (отрисовка, опрос клавиатуры), и отдельный поток на игровой процесс, в котором вызываются все три участника. С отрисовкой и опросом клавиатуры всё понятно — это просто поток главной формы приложения. Разберемся с потоком игрового процесса.
Поток игрового процесса
Структура потока показана на рисунке:
Теперь немного кода с пояснениями. Для начала устанавливаем частоты. Грубо говоря, сколько мс должно пройти между вызовами обработки логики, рендеринга:
Copy Source | Copy HTML
- // Тактовый генератор: FREQ - логика, FPS - рендеринг
- const int FREQ = 1000 / 40; // 1000 / FPS
- const int MAX_FPS = 1000 / 180;
А вот код основного цикла — с вызовом всех участников, проверками по времени и пр.
Copy Source | Copy HTML
- while (true)
- {
- qint64 time_cur_tick = QDateTime::currentMSecsSinceEpoch();
- int numLoops = 0;
-
- bool ft = true;
- while ( time_prev_tick < time_cur_tick && numLoops < MAX_LOOPS )
- {
- // Вызов логики
- w->UpdateLogic( 1 / FREQ );
- numLoops++;
-
- if ( ft )
- {
- ft = false;
- last_freq = time_cur_tick;
- }
- time_prev_tick += FREQ;
-
- // Обновляем time_cur_tick для более точного тактирования
- time_cur_tick = QDateTime::currentMSecsSinceEpoch();
- }
-
- time_tmp = QDateTime::currentMSecsSinceEpoch();
- w->SimulateConsistLogic( (float)( time_tmp - last_freq )/FREQ );
-
- time_tmp = QDateTime::currentMSecsSinceEpoch();
- if ( time_tmp - time_lastrender >= MAX_FPS &&
w->paint_mx.tryLock() )
- {
- time_lastrender = time_tmp;
- float freq_bit = 0;
- if ( time_tmp != last_freq )
- freq_bit = (float)( time_tmp - last_freq )/FREQ ;
-
- emit signalGUI( freq_bit );
- w->paint_mx.unlock();
- }
- }
(прим. — если будете смотреть исходный код — там всё несколько сложнее. Идет подсчет кадров в секунду, вывод дебажной информации и прочее)
Наверняка возник вопрос — зачем функциям рендеринга и симулятора непрерывности знать время, которое прошло с момента последнего обновления игровой логики? Всё просто — для того, чтобы рассчитать моментальное состояние сцены, и верным образом его обработать и вывести на экран. Для экономии ресурсов вызывая симулятор непрерывности можно передавать также время его прошлого вызова.
Как всё это работает
В нашем примере три вида объектов:
- корабль игрока
- пули
- метеориты
Для них сделаны соответствующие классы (CShip, CBullet, CMeteorite). Для пуль и метеоритов заданы контейнеры QVector для хранения. Для обработки пользовательского ввода создан массив «направлений движения» и переопределены функции keyReleaseEvent и keyPressEvent: keyReleaseEvent проверяет, есть ли в массиве нажатых клавиш отпускаемая клавиша, и удаляет ее при наличии. keyPressEvent соответственно заносит нажатую клавишу в массив нажатых клавиш (если ее там нет). Обработка этого массива происходит в функции тактового генератора игрового процесса. Там же происходят перемещения игровых объектов, обсчет инерции при движении корабля, создание метеоритов:
Copy Source | Copy HTML
- void MainWindow::UpdateLogic( float ftime )
- {
- float speed = 2;
- for ( int i = 0; i < m_dir.size(); i++ )
- {
- if ( m_dir[i] == MainWindow::UP )
- actor1.adjustDirection( QVector2D( 0, -speed ) );
- if ( m_dir[i] == MainWindow::DOWN )
- actor1.adjustDirection( QVector2D( 0, speed ) );
- if ( m_dir[i] == MainWindow::LEFT )
- actor1.adjustDirection( QVector2D( -speed, 0 ) );
- if ( m_dir[i] == MainWindow::RIGHT )
- actor1.adjustDirection( QVector2D( speed, 0 ) );
-
- if ( m_dir[i] == MainWindow::SPACE &&
m_allowbullet == 0 )
- {
- m_bullets.push_back( CBullet( actor1.getX(), actor1.getY() - 1, QVector2D( 0, -15) ) );
- qDebug( QString("Added bullet. Pos %1").arg( m_bullets.size() - 1 ).toAscii() );
- m_allowbullet = 5;
- fired++;
- }
- }
- actor1.stepDirection();
- bool dir_touched = false;
- for ( int i = 0; i < m_dir.size(); i++ )
- {
- if (m_dir[i] != MainWindow::SPACE )
- {
- dir_touched = true;
- break;
- }
- }
-
- if ( !dir_touched )
- {
- m_allowmove= 0;
- float inertia = 0.5;
- if ( actor1.getSpeed() < 0.5 )
- inertia = 1;
- actor1.adjustSpeed( inertia);
- }
-
-
- for ( int i = 0; i < m_bullets.size(); i++ )
- m_bullets[ i ].stepDirection();
-
- for ( int x = 0; x < m_enemies1.size(); x++ )
- m_enemies1[ x ].stepDirection();
-
- CheckGameRules();
- if ( m_enemies1.size() < max_enemies )
- {
- CMeteorite meteo( mrand( field_ident + CMeteorite::meteo_size,
field_ident + field_w - CMeteorite::meteo_size ),
- -mrand( 0, 20 ),
- QVector2D( 0, 1 ) );
- while( true )
- {
- int i = 0;
- while( i < m_enemies1.size() )
- {
- if( meteo.getBoundsT().intersects( m_enemies1[ i ].getBoundsT() ) )
- break;
-
- i++;
- }
-
- if ( i == m_enemies1.size() )
- break;
-
- meteo = CMeteorite( mrand( 1, 100 ), -mrand( 0, 20 ),
- QVector2D( 0, 1 ) );
-
- }
- m_enemies1.push_back( meteo );
- }
- UpdateBullet();
- }
Функция CheckGameRules проверяет игровые правила — кто в кого врезался, кто за рамки чего вышел и прочее. Кстати, в 2D это всё очень удобно делается функциями классов QPolygon, QRect и иже с ними.
Copy Source | Copy HTML
- void MainWindow::CheckGameRules( const float ftime )
- {
- QRect field_rect( field_ident, field_ident,
field_w, field_h );
-
- for ( int i = 0; i < m_bullets.size(); i++ )
- {
- CBullet blt = m_bullets[ i ];
- float tx = 0, ty = 0;
- blt.getTickCoords( ftime, tx, ty );
-
- blt.setX( tx );
- blt.setY( ty );
-
- if ( !field_rect.contains( m_bullets[ i ].getX(), m_bullets[ i ].getY() ) )
- {
- m_bullets.remove( i-- );
- }
- else
- {
- for ( int j = 0; j < m_enemies1.size(); j++ )
- {
- CMeteorite enm = m_enemies1[ j ];
- float etx = 0, ety = 0;
- enm.getTickCoords( ftime, etx, ety );
- enm.setX( etx );
- enm.setY( ety );
-
- if ( blt.checkCollision( enm.getBodyT() ) )
- {
- m_enemies1.remove( j-- );
- m_bullets.remove( i-- );
- score++;
- break;
- }
- } //for
- }
- }
-
- for ( int j = 0; j < m_enemies1.size(); j++ )
- {
-
- CMeteorite enm = m_enemies1[ j ];
- if ( !field_rect.contains( enm.getBoundsT() ) &&
- field_rect.bottomRight().y() < enm.getBoundsT().topLeft().y() )
- {
- m_enemies1.remove( j-- );
- }
-
- if ( actor1.checkCollision( enm.getBodyT() ) )
- {
- m_enemies1.remove( j-- );
- hits++;
- }
- }
-
-
- if ( !field_rect.contains( actor1.getBoundsT(), true ) )
- {
- while ( field_rect.x() >= actor1.getBoundsT().left() )
- actor1.setX( actor1.getX() + 1 );
-
- while ( field_rect.x()*2 + field_rect.width() <= actor1.getBoundsT().x() + actor1.getBoundsT().width())
- actor1.setX( actor1.getX() - 1 );
-
- while ( field_rect.top() >= actor1.getBoundsT().top() )
- actor1.setY( actor1.getY() + 1 );
-
- while ( field_rect.y()*2 + field_rect.height() <= actor1.getBoundsT().y() + actor1.getBoundsT().height())
- actor1.setY( actor1.getY() - 1 );
-
- actor1.stop();
-
- }
- }
Соответственно вызов симулятора непрерывности прост до безобразия. Всего лишь с небольшим шагом проверяем игровую логику:
Copy Source | Copy HTML
- void MainWindow::SimulateConsistLogic( float ftime )
- {
- for ( float bt = 0; bt < ftime; bt = bt + 0.1 )
- {
- CheckGameRules( bt );
- }
- }
Рендеринг отрисовывает игровое поле и вызывает Draw() всех объектов с параметром текущего отступа от последнего вызова тактового генератора игрового процесса. Плюс вывод служебной информации:
Copy Source | Copy HTML
- void MainWindow::Render()
- {
- QPainter qpainter(this);
-
- const int bgw = 2;
- qpainter.setPen (QPen(Qt::black, bgw));
- qpainter.setBrush( QBrush( Qt::darkGray ) );
- qpainter.drawRect( field_ident, field_ident,
field_w + field_ident, field_h + field_ident );
-
-
- for ( int i = 0; i < m_bullets.size(); i++ )
- {
- CBullet blt = m_bullets[ i ];
- blt.Draw( qpainter, freq_bit );
- }
-
- for ( int i = 0; i < m_enemies1.size(); i++ )
- {
- CMeteorite enm = m_enemies1[ i ];
- enm.Draw( qpainter, freq_bit );
- }
-
- actor1.Draw( qpainter, freq_bit );
-
- QPalette pal;
- qpainter.setBrush( pal.brush( QPalette::Window ) );
- qpainter.setPen (QPen(pal.color( QPalette::Window), 1));
- qpainter.drawRect( field_ident - bgw/2, 0,
field_w + field_ident + bgw/2, field_ident - bgw );
-
- qpainter.setPen (QPen(Qt::black, bgw));
- qpainter.setBrush( QBrush( Qt::darkGray, Qt::NoBrush ));
- qpainter.drawRect( field_ident, field_ident,
field_w + field_ident, field_h + field_ident );
-
- ui->label_freq->setText( QString("%1").arg( freq ).toAscii() );
- ui->label_fps->setText( QString("%1").arg( fps ).toAscii() );
- ui->label_speed->setText( QString("%1").arg( actor1.getSpeed() ).toAscii() );
-
- ui->label_score->setText( QString("%1").arg( score ).toAscii() );
- ui->label_fired->setText( QString("%1").arg( fired ).toAscii() );
- ui->label_hits->setText( QString("%1").arg( hits ).toAscii() );
-
- }
Собственно, остальное — тривиальное программирование. Скелет приложения разобран, а детали реализации можно посмотреть в прилагаемых исходных кодах. В качестве итога — внешний вид того, что у меня получилось:
Исходники тут. Летаем стрелками, стреляем пробелом.
Вернуться в раздел:
Software /
Игры
|