Irrlicht - from Noob to Pro: Gamestates


Allgemeines

Bearbeiten

Gamestates helfen eine logische Struktur in ein Spiel zu bekommen. Auch wenn sie nicht zwingend notwendig sind, wird dringend empfohlen sie vor allem bei größeren Projekten zu verwenden.

Was sind Gamestates

Bearbeiten

Ein Gamestate ist ein Zustand, in dem sich ein Spiel befinden kann (Spiel, Menu, Optionen). Dabei sind die Zustände über Transitionen miteinander verknüpft. Das heißt, man kann nur von einem Zustand in bestimmte Zustände umschalten. Es wäre sehr unsinnig, wenn man beispielsweise vom Menu sofort in den Pausemodus des Spiels kommen könnte. Ein Gamestate hat eine separate Eingabe und Ausgabe. Über einen Gamestate-Manager wird dem aktuellen Gamestate, die Verwaltung, der Eingabegeräte (Maus, Tastatur, Joysticks) zugesprochen, sodass nur dieser auf Benutzereingabe reagieren kann.

Nachfolgende Grafik zeigt ein grobes Gamestatesytem.

 

Die Unterteilung ist sehr intuitiv und grob gehalten. Natürlich könnte man das noch feiner untergliedern. Beispielsweise die Optionen in Video-, Audio- und Spieloptionen unterteilen. Dabei sollet man aber darauf achten, dass man sinnvoll unterteilt.

Vorteile

Bearbeiten
  • Logische Struktur (Für den Programmierer und jeden, der den Code liest)
  • Transitionen verhindern unsinninges Wechseln in falsche Zustände
  • Verbesserung der Qualität des Codes (Wiederverwendbarkeit, Wartung)

Irrlicht und Gamestates

Bearbeiten

Wir müssen einem Gamestate ermöglichen Benutzereingaben abfangen zu können. In Irrlicht verwendet man dafür den EventReceiver.

Der EventReceiver

Bearbeiten

Der EventReceiver ist ein Objekt, welches Events, die vom Anwender produziert werden, indem er beispielsweise die Tastatur oder Maus benutzt oder die GUI manipuliert, abfängt. Genauer wird der EventReceiver bei dem Thema Grafische Benutzeroberfläche besprochen.

Beispiel

Bearbeiten

Hier soll nun ein Beispiel gegeben werden, wie man Gamestates mit Irrlicht umsetzen kann. dazu schauen wir uns ersteinmal ein Interface eines Gamestates an:

 //gamestate.h
 
 #if !defined(__gamestate_h__)
 #define __gamestate_h__
 
 class GameState
 {
 protected:
 	stringc		name;
 	bool		active;
 	bool		firstEnter;
 
 public:
 	GameState(void);
 	GameState(stringc newName);
 	virtual ~GameState(void);
 
 	//get Name of gamestate
 	stringc getName(void);
 
 	//is gamestate currently running?
 	bool  isActive(void);
 
 	//set to true if currently running otherwise false
 	void  setActive(bool);
 
 	//initialize the gamestate, setup gui or something
 	virtual void  initialize(void);
 
 	//clean up the gamestate
 	virtual void  flush(void);
 
 	//this is called everytime the gamestate is entered
 	//So load the map when the game starts or something like this
 	virtual void  OnEnter(void) = 0;
 
 	//this is called everytime when another gamestate is entered
 	//so first leave this (e.g. destroy the map)
 	virtual void  OnLeave(void) = 0;
 
 	//here we get the user input
 	//this method is derived by IEventReceiver
 	virtual bool  OnEvent(const SEvent &event) = 0;
 
 	//render all the stuff (gui, scene...)
 	virtual void  render(void) = 0;
 };
 
 #endif






 //gamestate.cpp
 #include "GameState.h"
 
 GameState::GameState(void)
 {
 	name = stringc("unknown");
 		
 	active = false;
 	firstEnter = true;
 }
 
 GameState::GameState(stringc newName) : name(newName)
 {
 	active = false;
 	firstEnter = true;
 }
 
 GameState::~GameState(void)
 {	
 }
 
 stringc GameState::getName(void)
 {
 	return(name);
 }
 
 bool GameState::isActive(void)
 {
 	return(active);
 }
 
 void GameState::setActive(bool isactive)
 {
 	active = isactive;
 }
 
 void GameState::initialize(void)
 {
 	active = true;
 	OnEnter();
 }
 
 void GameState::flush(void)
 {
 	active = false;
 	OnLeave();
 }

Ein Gamestate hat einen Namen, über den wir ihn suchen und finden können. Zudem kann er initialisiert und bereinigt werden. Und er stellt dem Programmierer Methoden bereit die Start-Up und Aufräumaktivitäten übernehmen. Er bekommt außerdem via OnEvent die Benutzereingaben und rendert alles über die render-Methode.

Diese Beispiel beschränkt sich sehr auf die Grundzüge. Natürlich könnte man den Gamestate sehr erweitern (XML-gesteuertes Laden von Daten, z.B. GUI, Settings, etc...).

Nun erstellen wir einen Manager, der alle Gamestates verwaltet. Dazu bekommt der Manager eine Liste von allen bisher eingetragenen Gamestates und die Möglichkeit zwischen diesen hin und her zu schalten.

 //gamestatemanager.h
 #if !defined __gamestatemanager_h__
 #define __gamestatemanager_h__
 
 #define manager			GamestateManager::getInstance()
 #define device			GamestateManager::getInstance().getDevice()
 
 class GamestateManager
 {
 private:
 	IrrlichtDevice*				irrlichtDevice;
 	vector<IGameState*>			gameStates;
 
 private:
 	GamestateManager(void);
 	GamestateManager(const GamestateManager&);
 	~GamestateManager(void);
 
 	GamestateManager operator =(const GamestateManager&);
 
 public:
 	static GamestateManager& getInstance(void)
 	{
 		static GamestateManager instance;
 		return(instance);
 	}
 
 public:
 	bool initialize(int width, int height, bool fullscreen);
 	bool finalize(void);
 
 	IrrlichtDevice* getDevice(void);
 
 	bool isRunning(void);
 
 	void addGameState(IGameState* gamestate, bool active = false);
 	void changeGameState(const char* name);
 	IGameState* getActiveGameState(void);
 	IGameState* getGameState(const char* name);
 
 	void beginRender(void);
 	void endRender(void);
 
 	void render(void);
 };
 
 #endif






 //gamestatemanager.cpp
 #include "GamestateManager.h"
 
 GamestateManager::GamestateManager(void)
 {
 }
 
 GamestateManager::GamestateManager(const GamestateManager&)
 {
 }
 
 GamestateManager::~GamestateManager(void)
 {
 }
 
 bool GamestateManager::initialize(int windowWidth, int windowHeight, bool fullscreen)
 {
 	irrlichtDevice = createDevice(EDT_OPENGL, dimension2d<u32>(windowWidth, windowHeight), 32, fullscreen);
 
 	if(irrlichtDevice)
 	{
 		return(true);
 	}
 
 	return(false);
 }
 
 bool GamestateManager::finalize(void)
 {
  	irrlichtDevice->closeDevice();
 	irrlichtDevice->drop();
 
 	return(true);
 }
 
 IrrlichtDevice* GamestateManager::getDevice(void)
 {
 	return(irrlichtDevice);
 }
 
 bool GamestateManager::isRunning(void)
 {
 	return(irrlichtDevice->run());
 }
 
 void GamestateManager::addGameState(IGameState* gamestate, bool active)
 {
 	gameStates.push_back(gamestate);
 
 	if(active)
 	{
 		gamestate->setActive(true);
 		device->setEventReceiver(gamestate);
 		gamestate->initialize();		
 	}
 }
 
 void GamestateManager::changeGameState(const char* name)
 {
 	for(unsigned int i = 0; i < gameStates.size(); i ++)
 	{
 		if(stringc(name) == stringc(gameStates[i]->getName()))
 		{
 			if(getActiveGameState() != NULL)
 			{
 				getActiveGameState()->flush();
 			}			
 
 			device->setEventReceiver(gameStates[i]);
 			gameStates[i]->initialize();
 		}
 	}
 }
 
 IGameState* GamestateManager::getActiveGameState(void)
 {
 	for(unsigned int i = 0; i < gameStates.size(); i ++)
 	{
  		if(gameStates[i]->isActive()) 
 		{
 			return(gameStates[i]);
 		}
 	}
  
  	return(NULL);
 }
 
 IGameState* GamestateManager::getGameState(const char* name)
 {
   	for(unsigned int i = 0; i < gameStates.size(); i ++)
 	{
  		if(stringc(name) == gameStates[i]->getName())
  		{
  			return(gameStates[i]);
  		}
  	}
  
 	return(NULL);
 }
 
 void GamestateManager::beginRender(void)
 {
 	irrlichtDevice->getVideoDriver()->beginScene(true, true, SColor(255,255,255,255));
 }
 
 void GamestateManager::endRender(void)
 {
 	irrlichtDevice->getVideoDriver()->endScene();
 }
 
 void GamestateManager::render(void)
 {
 	getActiveGameState()->render();
 }

Der Gamestatemanager übernimmt in diesem Beispiel auch mehr Corefunktionalitäten wie das Verwalten des IrrlichtDevices usw. Im realen Fall sollte man das natürlich auslagern. Über addGameState werden Gamestates hinzugefügt und auf Bedarf sofort aktiviert. Ansonsten kann man jederzeit via changeGameState in einen beliebigen Gamestate umschalten. In diesem Beispiel sind keine Transitionen umgesetz oder anders gesagt, kann man hier von einem Gamestate in jeden anderen umschalten.

Wenn man nun bestimmte Gamestates erstellen will, leitet man von der Klasse Gamestate ab und implementiert die entsprechenden Methoden.

 //intro.cpp
 #include "Intro.h"
 
 Intro::Intro(void)
 {
 }
 
 Intro::Intro(stringc name) : GameState(name)
 {
 }
 
 Intro::~Intro(void)
 {
 }
 
 void Intro::OnEnter(void)
 {
 	//set up gui
 }
 
 void Intro::OnLeave(void)
 {
 	//restore playerdata
 }
 
 bool Intro::OnEvent(const SEvent &event)
 {
 	//handle user input
 	return(false);
 }
 
 void Intro::render(void)
 {
 	//draw gui and scene
 }

Danach erstellt man den Gamestate (genauer: einen Zeiger, der auf ein Objekt vom Typ Gamestate zeigt) und lädt diesen in den GamestateManager und schon hat man den ersten Gamestate.

 //main.cpp
 #include "GamestateManager.h"
 #include "GameState.h"
 #include "Intro.h"
 
 int main(int argc, char **argv)
 {
 	if(manager.initialize(640, 480, false))
 	{
 		manager.addGameState(new Intro("intro"));		
 		manager.changeGameState("intro");
 
 		while(device->run())
 		{
 			device->getVideoDriver()->beginScene(true,true,SColor(255,255,255,255));
 				manager.render();
 			device->getVideoDriver()->endScene();
 		}
 
 		manager.finalize();
 	}	
 
 	return(0);
 }

Wenn man nun weitere Gamestates haben möchte. Leitet man sich immer wieder neue Klassen von GameState ab und implementiert diese nach belieben.