[Перевод] Управление игровыми состояниями в C++

Здравствуйте, дорогие читатели!

У нас активно расходится третий доптираж крайне успешной книги «Изучаем C++ через программирование игр». Поэтому сегодня вашему вниманию предлагается перевод интересной статьи на одну из узких тем, связанных с программированием игр на C++. Также просим вас поучаствовать в опросе
Я впервые приобрел впечатление о различных состояниях игры много лет назад, когда смотрел одну демку. Это было не «превью готовящейся игры», а нечто олдскульное, «с сайта scene.org». Так или иначе, подобные демки совершенно незаметно переходили от одного эффекта к другому. От каких-нибудь двухмерных вихрей игра могла переключиться сразу на сложный рендеринг трехмерной сцены. Помню, мне казалось, что для реализации этих эффектов требуется сразу несколько отдельных программ.

Множественные состояния важны не только в демках, но и в любых играх. Любая игра начинается с заставки, затем открывает определенное меню, после чего начинается геймплей. Когда вы будете окончательно побеждены, игра переходит в состояние «game over», за которым обычно следует возврат в меню. В большинстве игр можно одновременно находиться в двух и более состояниях. Например, во время геймплея обычно можно открыть меню.

Как правило, множественные состояния обрабатываются при помощи серий инструкций if, переключателей и циклов. Программа начинается с заставки и остается в этом состоянии, пока не будет нажата клавиша. Затем отображается меню, оно остается на экране, пока вы не сделаете выбор. Затем начинается геймплей, работающий в виде цикла, пока игра не закончится. В любой момент в рамках игрового цикла программа должна проверять, что следует сделать — отрисовать меню или просто отобразить следующий кадр. Кроме того, та часть программы, что занята обработкой событий, должна проверять, скажется ли пользовательский ввод на состоянии меню или игры как таковой. Все это складывается в основной цикл, отслеживать который очень сложно, а значит — сложно отлаживать и поддерживать.

Что такое состояние?

Как было указано выше, состояние — это практически отдельная программа в составе игры. В каждом состоянии события обрабатываются по-своему, на экране отрисовываются свои элементы. В каждом состоянии обрабатываются собственные события, обновляется игровой мир, и на экране рисуется следующий кадр. Вот мы и определили три метода, которые должны содержаться в нашем классе состояния.

Кроме того, игровое состояние должно «уметь» загружать графику и само себя инициализировать, а также избавляться от ненужных ресурсов, когда задача будет решена. Случается, когда мы хотим поставить состояние на паузу и возобновить его позднее. Например, мы хотим ненадолго приостановить игру, чтобы вывести меню. Пока у нас вырисовывается такой класс игрового состояния:

class CGameState
{
public:
  void Init();
  void Cleanup();

  void Pause();
  void Resume();

  void HandleEvents();
  void Update();
  void Draw();
};

Подобная схема должна вполне отвечать всем нашим потребностям, связанным с игровым состоянием. Получается красивый базовый класс, от которого мы можем наследовать другие, соответствующие каждому из состояний игры — заставке, меню, геймплею и т.д.

Менеджер состояний

Далее потребуется разработать механизм для управления этими состояниями — менеджер состояний. В моем коде менеджер состояний входит в состав игрового движка. Другой программист мог бы создать для менеджера состояний отдельный класс, но мне показалось, что будет проще добавить его непосредственно к движку. Опять же, можно специально выяснить, что должен делать игровой движок, а затем написать для него такой класс, который будет реализовывать именно эти функции.

В нашем простом примере движок должен всего лишь инициализировать SDL и выполнить очистку, когда все будет сделано. Поскольку мы собираемся использовать движок в основном цикле, также потребуется проверять, продолжает ли он работать, приказывать ему завершить работу, обрабатывать обычные события, возникающие в процессе, обновлять игровой мир, отрисовывать последовательность кадров.

Та часть движка, что связана с менеджером состояний, в сущности, очень проста. Чтобы одни состояния могли существовать поверх других, нужно расположить их в виде стека. Я собираюсь реализовать такой стек при помощи вектора из STL. Кроме того, мне понадобятся методы для смены состояний, а также для перемещения их по стеку вверх-вниз.

Итак, класс игрового движка приобретает следующий вид:

class CGameEngine
{
public:
  void Init();
  void Cleanup();

  void ChangeState(CGameState* state);
  void PushState(CGameState* state);
  void PopState();

  void HandleEvents();
  void Update();
  void Draw();

  bool Running() { return m_running; }
  void Quit() { m_running = false; }

private:
  // стек состояний
  vector states;

  bool m_running;
};

Написать некоторые из этих функций будет очень просто. HandleEvents(), Update() и Draw() — все они просто будут вызывать соответствующую функцию из того состояния, которое находится на верхушке стека. Поскольку для этого зачастую потребуется доступ к данным игрового движка, я вернусь к классу игровых состояний и добавлю указатель на класс игрового движка как параметр каждой из этих функций-членов.

Последний вопрос — как переходить между состояниями. Как движок узнает, когда переходить из одного состояния в другое? Никак. О необходимости перехода в следующее состояние узнает лишь текущее состояние. Итак, мы вновь вернемся к классу игровых состояний и добавим туда функцию для перехода между ними.

В данном случае мы будем создавать абстрактный базовый класс, и сделаем большинство его членов чистыми виртуальными функциями. Так гарантируется, что унаследованный класс будет их реализовывать. С учетом всех этих изменений готовый класс игровых состояний примет такой вид:

class CGameState
{
public:
  virtual void Init() = 0;
  virtual void Cleanup() = 0;

  virtual void Pause() = 0;
  virtual void Resume() = 0;

  virtual void HandleEvents(CGameEngine* game) = 0;
  virtual void Update(CGameEngine* game) = 0;
  virtual void Draw(CGameEngine* game) = 0;

  void ChangeState(CGameEngine* game,
                   CGameState* state) {
    game->ChangeState(state);
  }

  protected: CGameState() { }
};

Теперь добавлять состояния к нашей игре предельно просто: наследуем базовый класс и определяем семь чистых виртуальных функций. Поскольку нам в любом случае потребуется не более одного экземпляра любого конкретного состояния, давайте реализуем их в виде одиночек. Если вы не знакомы с паттерном «одиночка», поясню: он просто позволяет убедиться, что объект существует ровно в одном экземпляре. Для этого конструктор делается защищенным, а затем делается функция, возвращающая указатель на статический экземпляр данного класса.

Чтобы вы могли представить, насколько этот метод может упростить всю игру, обратите внимание на следующий листинг, где находится весь файл main.cpp:

#include "gameengine.h"
#include "introstate.h"

int main ( int argc, char *argv[] )
{
  CGameEngine game;

  // инициализация движка
  game.Init( "Engine Test v1.0" );

  // загрузка заставки
  game.ChangeState( CIntroState::Instance() );

  // основной цикл
  while ( game.Running() )
  {
    game.HandleEvents();
    game.Update();
    game.Draw();
  }

  // очистка движка
  game.Cleanup();
  return 0;
}

Файлы

В этом примере описаны три состояния: заставка, выступающая на черном фоне, геймплей и игровое меню. На время работы с меню геймплей приостанавливается, а после закрытия меню — возобновляется. Каждому состоянию соответствует простое фоновое изображение.

• stateman.zip — Исходный код, графика и файлы проекта для Visual C++
• stateman.tar.gz — Исходный код, графика и файлы проекта для Linux.

В коде примеров используется SDL. Если вы не знакомы с SDL, почитайте мой туториал Getting Started with SDL. Если у вас на компьютере не установлена SDL, то вы не сможете скомпилировать и запустить этот пример.

Ресурсы

Если вы только начинаете изучать C++, то определенно должны познакомиться с книгой «Изучаем C++ через программирование игр». Это замечательное введение в язык программирования C++, в качестве примеров автор использует простые игры. Программистам среднего уровня я рекомендую C++ For Game Programmers. Эта книга поможет вам углубить знания C++. Наконец, чтобы как следует усвоить паттерны, читайте книгу «Паттерны проектирования» под авторством «Банды четырех».

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru