Войти
ПрограммированиеСтатьиОбщее

Смена экранов игры

Внимание! Этот документ ещё не опубликован.

Автор:

Введение


Многие при разработке игр сталкиваются с проблемой — как сделать удобное переключение экранов в игре. Под экранами я имею ввиду главное меню, окно игры, настройки новой игры, заставки и т.д. Начинающие обычно делают так:
switch(GameState)
{
case INTRO:
  Intro();
  break;
case MAINMENU:
  MainMenu();
  break;
case EDITOR:
  Editor();
  break;
case CONFIG:
  Config();
  break;
case ABOUT:
  About();
  break;
case GAME:
  Game();
  break;
case SERVER:
  Server();
  break;
...
}
Данный код не очень гибок и плохо поддается расширению. Далее я опишу способ переключения, который использую сам.

Цели и условия


Давайте определимся, что мы хотим получить.
В игре у нас может быть неопределенное число экранов. Их может быть два (меню и игра) или сотня (например, если это сборник игр).
Мы должны свободно переключаться между этими экранами.
Разработка игры — это часто очень много дебага и тестирования, а, значит, мы должны иметь возможность в любой момент отключать ненужные экраны.
Каждый экран у нас самостоятелен. Не важно, что игрок делал в главном меню — это мало влияет на игру. Графика из меню не нужна в игре.
Архитектурно независима. Проблема свитча (код выше) в том, что в этом месте образуется узел из несвязанных между собой сущностей.

Немножко теории


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

Код

IModule.h

#pragma once

class ModuleMgr;

class IModule
{
public:
  IModule();
  virtual ~IModule(){}

  void Init();
  bool Frame();
  void Close();

  void SetMgr(ModuleMgr *mgr){m_mgr = mgr;}

  bool IsExit() const {return !m_isexit;}
  void Exit(){m_isexit=true;}

  virtual void doInit() = 0;
  virtual bool doRun() = 0;
  virtual void doClose() = 0;

protected:  
  ModuleMgr *m_mgr;  // указатель на менеджер сцен
  bool m_isinit;    // флаг сообщающий о инициализации 
  bool m_isexit;    // флаг оповещающий о выходе  
};

IModule.cpp

#include "IModule.h"

IModule::IModule() :
  m_isinit(false),
  m_isexit(false),
  m_mgr(nullptr)
{
}

void IModule::Init()
{
  if (m_isinit)
    Close();

  m_isinit = true;
  m_isexit = false;

  doInit();
}

bool IModule::Frame()
{
  if (!m_isinit || m_isexit)
    return false;
  else
    return doRun();
}

void IModule::Close()
{
  if (m_isinit)
  {
    doClose();
    m_isinit = false;
  }  
}

ModuleMgr.h

#pragma once

#include <string>
#include <map>
#include "IModule.h"


class ModuleMgr
{
public:
  ModuleMgr();

  void AddModule(const std::string &name, IModule *module);
  void SetActiveModule(const std::string &name);

  bool Frame();
  void Close();

private:
  std::map<std::string, IModule*> m_moduleList;
  IModule *m_currmodule;
};

ModuleMgr.cpp

#include "ModuleMgr.h"

ModuleMgr::ModuleMgr() :
  m_currmodule(nullptr)
{
}

void ModuleMgr::AddModule(const std::string &name, IModule *module)
{
  m_moduleList.insert(std::pair<std::string,IModule*>(name, module));
}

void ModuleMgr::SetActiveModule(const std::string &name)
{
  if (m_currmodule)
    m_currmodule->Close();

  auto it = m_moduleList.find(name);
  if (it == m_moduleList.end())
    return;

  m_currmodule = it->second;
  if (m_currmodule)
  {
    m_currmodule->SetMgr(this);
    m_currmodule->Init();
  }
}

bool ModuleMgr::Frame()
{
  if (!m_currmodule)
    return false;

  return m_currmodule->Frame();
}

void ModuleMgr::Close()
{
  if (m_currmodule)
    m_currmodule->Close();
  
  m_moduleList.clear();
}


Как это работает


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

Пример


Теперь реализуем все это на практике. Для этого воспользуемся простой консолью для вывода (сам метод на практике показал, что работает с любым движком и любым видом графики).
В примере мы сделаем следующие окна: заставка, главное меню и игра. Из игры мы сможем вернуться в меню.

ModuleName.h

#pragma once

#define HELLOSCREEN "Hello"
#define MAINMENU "Menu"
#define GAME "Game"

HelloScreen.h

#pragma once

#include "IModule.h"
#include <string>

class HelloScreen : public IModule
{
public:
  virtual void doInit();
  virtual bool doRun();
  virtual void doClose();

private:
  std::string m_text; // типа данные:)
};

HelloScreen.cpp

#include "HelloScreen.h"
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "ModuleName.h"
#include "ModuleMgr.h"

void HelloScreen::doInit()
{
  m_text = "Hello World";
}

bool HelloScreen::doRun()
{
  system("CLS");  // очищаем экран

  std::cout << m_text << std::endl;

  system("PAUSE");// ожидаем реакции игрока

  m_mgr->SetActiveModule(MAINMENU);// переключаем на следующий модуль

  return true;
}

void HelloScreen::doClose()
{
  m_text.clear();
}

MainMenu.h

#pragma once

#include "IModule.h"
#include <string>

class MainMenu : public IModule
{
public:
  virtual void doInit();
  virtual bool doRun();
  virtual void doClose();
};

MainMenu.cpp

#include "MainMenu.h"
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "ModuleName.h"
#include "ModuleMgr.h"
#include <conio.h>

void MainMenu::doInit()
{
}

bool MainMenu::doRun()
{
  system("CLS");  // очищаем экран

  std::cout << "Main Menu\n\n";
  std::cout << "Select:\n";
  std::cout << "1 - New Game\n";
  std::cout << "2 - Exit Game\n";

  if (kbhit())
  {
    if (getch()==49)  // нажал на 1
      m_mgr->SetActiveModule(GAME);// переключаем на следующий модуль
    else if (getch()==50)  // нажал на 2
      return false;  // выходим
  }

  return true;
}

void MainMenu::doClose()
{
}

Game.h

#pragma once

#include "IModule.h"
#include <string>

class Game : public IModule
{
public:
  virtual void doInit();
  virtual bool doRun();
  virtual void doClose();

public:
  long m_i;
};

Game.cpp

#include "Game.h"
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <limits.h>
#include <conio.h>
#include "ModuleName.h"
#include "ModuleMgr.h"

void Game::doInit()
{
  m_i = 0;
}

bool Game::doRun()
{
  system("CLS");  // очищаем экран

  m_i++;
  if (m_i >= LONG_MAX)
    m_i = 0;

  std::cout << m_i;

  // если пользователь нажмет Esc, вернемся в главное меню
  if (kbhit() && getch() == 27)
    m_mgr->SetActiveModule(MAINMENU);

  return true;
}

void Game::doClose()
{
}

main.cpp

#include "ModuleMgr.h"
#include "HelloScreen.h"
#include "MainMenu.h"
#include "Game.h"
#include "ModuleName.h"

int main()
{
  ModuleMgr mgr;
  
  HelloScreen hello;
  MainMenu menu;
  Game game;

  // добавляем модули
  mgr.AddModule(HELLOSCREEN, &hello);
  mgr.AddModule(MAINMENU, &menu);
  mgr.AddModule(GAME, &game);

  // указываем текущий модуль
  mgr.SetActiveModule(HELLOSCREEN);

  // запускаем игровой цикл
  while(mgr.Frame());

  mgr.Close();

  return 0;
}

Для простоты вот диаграмма классов данного примера (спасибо Zefick):
module | Смена экранов игры

Несколько замечаний


Никто вам не мешает в модуле держать еще один менеджер модулей и организовать таким образом подмодули (например, в игре это могут быть окна инвентаря, статистики игрока и прочее).
Второй частый вопрос — как быть, если хочется сделать переход с каким-нибудь эффектом? Тоже элементарно: рендерим экран в текстуру, производим операции. Для этого можно выделить отдельный дополнительный модуль, который будет отвечать за работу перехода между модулями.stdlib.h

#игровой цикл, #экраны игры

22 ноября 2012 (Обновление: 27 янв. 2013)

Комментарии [135]