Teil 11: Geordnetes Chaos II

Wir wollen also unsere Spielobjekte nicht mehr alle “von Hand” in CGSGame deklarieren. Es würde auch mühsam werden, schliesslich müssen wir sehr viele Objekte zur Laufzeit anlegen!
Hunderte Schüsse, Feinde, Powerups, die zu unterschiedlichen Zeiten in unterschiedlicher Menge auftauchen. Wir werden uns also stattdessen wieder einer Factory-Methode bedienen, wie wir sie schon bei den GameStates verwendet haben. Bisher haben wir nur ein einziges Spielobjekt, nämlich den Spielergleiter, dementsprechend einfach fällt der Code der Factory aus:
Datei /game/EntityFactory.h

#ifndef ENTITYFACTORY_H
#define ENTITYFACTORY_H
#include <objects/Entity.h>
// ---------------------------------------------------------------------------
class CEntityFactory
{
public:
  enum eIdentifier
  {
     EN_Glider
  };
  static CEntity* Create( eIdentifier eWhichEntity );
};
#endif

Zukünftig werden wir alle neuen Spielobjekte hier in die Enumeration einpflegen und im untenstehenden Code in den switch einhängen:
Datei /game/EntityFactory.cpp:

#include "EntityFactory.h"
#include <objects/Glider.h>
// ---------------------------------------------------------------------------
CEntity* CEntityFactory::Create( eIdentifier eWhichEntity )
{
  CEntity* pEntity = 0;
  switch( eWhichEntity )
  {
  case EN_Glider: pEntity = new CGlider; break;
  }
  if( pEntity )
  {
     pEntity- >Initialize();
  }
  return pEntity;
}

Hiermit können wir jederzeit mit der statischen Methode CEntityFactory::Create( CEntityFactory::EN_Glider ) einen Spielergleiter erstellen. Das werden wir natürlich nur einmal machen, aber für andere Objekte wie Laserschüsse etc. wird das ganze später recht praktisch.
Weiterhin benötigen wir eine Klasse, die unsere Objekte verwaltet. Schließlich haben wir für sie schon einen Listentyp definiert, die Liste müssen wir jetzt irgendwo unterbringen. Unseren Objekten haben wir ja die beiden Methoden Draw und Idle zugedacht, genau diese soll unsere Verwaltungsklasse auch für alle Objekte aufrufen können. Seit dem letzten Teil gibt es weiterhin das Flag m_bDestroy in CEntity, die Managerklasse muß also auch dafür sorgen, daß Objekte wieder gelöscht werden, wenn sie nicht mehr benötigt werden.
Wieder stellt sich beim Anlegen der Managerklasse die Frage: Wo wird sie benötigt? Reicht es, sie als Member von CGSGame einzufügen, oder sollten wir vielleicht wieder ein Singleton daraus machen und den Zugriff von überall gewähren? Können wir dann überhaupt ein Singleton verwenden oder benötigen wir vielleicht mehrere Instanzen? Ich habe mich für das bewährte Singleton entschieden. Mehrere Instanzen werden wir wohl nicht brauchen, dafür muß aber z.B. auch der Gleiter in der Lage sein, Objekte anzulegen (nämlich die Schüsse). Wer sich nun den untenstehenden Code anschaut und sich etwas besser mit C++ auskennt, wird sich vielleicht fragen: Das ist schon das 3. Singleton, warum löst Du das nicht über eine Template-Basisklasse? Ich bin gegen diese Lösung, weil der C’tor der von der abgeleiteten Klasse dann öffentlich sein müsste, was dem Singleton-Konzept widerspricht und einen Mißbrauch möglich macht.
Datei /game/EntityManager.h

#ifndef ENTITYMANAGER_H
#define ENTITYMANAGER_H
// ---------------------------------------------------------------------------
#include <Types.h>
#include <objects/Entity.h>
// ---------------------------------------------------------------------------
class CEntityManager
{
public:
  static CEntityManager* GetInstance();
  static void DestroyInstance();
  CEntity* Add( CEntity* pEntity );
  void CleanUp();
  void Idle( DWORD dwDeltaTime );
  void Draw();
private:
  CEntityManager();
  ~CEntityManager();
  vecEntityPtr m_vecEntities;
  static CEntityManager* m_pInstance;
};
#define ENTITYMANAGER CEntityManager::GetInstance()
#endif

Noch nicht besprochen haben wir hier die Methoden Add und CleanUp. Add erhält einen Zeiger auf ein Entity und wird dieses dann einfach zur Liste hinzufügen. Es gibt denselben Zeiger wieder zurück. Das sieht komisch aus, macht aber Sinn, damit wir später diese Schreibweise anwenden können: CEntity* pAnyEntity = ENTITYMANAGER- >Add( CEntityFactory::Create( CEntityFactory::EN_AnyEntity ) );
Die Methode CleanUp löscht alle vorhandenen Objekte in der Liste. Das ist z.B. wichtig, wenn nach ‘nem Game Over der Level neu gestartet wird oder das Spiel beendet wird. Der Code sieht wie folgt aus:
Datei game/EntityManager.cpp

#include "EntityManager.h"
// ---------------------------------------------------------------------------
CEntityManager* CEntityManager::m_pInstance = 0;
// ---------------------------------------------------------------------------
CEntityManager* CEntityManager::GetInstance()
{
  if( !m_pInstance )
  {
     m_pInstance = new CEntityManager;
  }
  return m_pInstance;
}
// ---------------------------------------------------------------------------
void CEntityManager::DestroyInstance()
{
  if( m_pInstance )
  {
     delete m_pInstance;
     m_pInstance = 0;
  }
}
// ---------------------------------------------------------------------------
CEntityManager::CEntityManager()
{
}
// ---------------------------------------------------------------------------
CEntityManager::~CEntityManager()
{
  CleanUp();
}
// ---------------------------------------------------------------------------
CEntity* CEntityManager::Add( CEntity* pEntity )
{
  m_vecEntities.push_back( pEntity );
  return pEntity;
}
// ---------------------------------------------------------------------------
void CEntityManager::CleanUp()
{
  for( vecEntityPtr::iterator it = m_vecEntities.begin(); it != m_vecEntities.end(); ++it )
  {
     CEntity* pEntity = *it;
     pEntity- >Uninitialize();
     delete pEntity;
  }
  m_vecEntities.clear();
}
// ---------------------------------------------------------------------------
void CEntityManager::Idle( DWORD dwDeltaTime )
{
  vecEntityPtr::iterator it;
  CEntity* pEntity;
  it = m_vecEntities.begin();
  while( it != m_vecEntities.end() )
  {
     pEntity = *it;
     if( pEntity- >m_bDestroy )
     {
        pEntity- >Uninitialize();
        delete pEntity;
        it = m_vecEntities.erase( it );
     }
     else
     {
        ++it;
     }
  }
  for( it = m_vecEntities.begin(); it != m_vecEntities.end(); ++it )
  {
     pEntity = *it;
     pEntity- >Idle( dwDeltaTime );
  }
}
// ---------------------------------------------------------------------------
void CEntityManager::Draw()
{
  CEntity* pEntity;
  for( vecEntityPtr::iterator it = m_vecEntities.begin(); it != m_vecEntities.end(); ++it )
  {
     pEntity = *it;
     pEntity- >Draw();
  }
}

Damit das ganze funktioniert, müssen wir nun noch ein paar Anpassungen machen, die ich hier nur stichpunktartig auflisten möchte:

  • Die Singleton-Instanz muß irgendwo zerstört werden, ein guter Punkt hierfür ist die Uninitialize-Funktion des GameManagers, dort fügen wir also noch ENTITYMANAGER- >DestroyInstance(); ein. Hierzu includen wir EntityManager.h in GameManager.cpp.
  • Der Gleiter wird nun nicht mehr automatisch erstellt, sondern über die Factory-Methode. Wir ändern die Deklaration in CGSGame.h zu CGlider* m_pGlider und das Erstellen in CGSGame::Enter zu m_pGlider = static_cast< CGlider* >( ENTITYMANAGER- >Add( CEntityFactory::Create( CEntityFactory::EN_Glider ) ) ); – damit das funktioniert, müssen natürlich EntityManager.h und EntityFactory.h in GSGame.cpp eingebunden werden.
  • Der Aufruf der Idle– und Draw-Funktionen des Gleiters fällt natürlich weg, stattdessen rufen wir diese Funktionen im EntityManager auf: ENTITYMANAGER- >Idle( dwDeltaTime ); ENTITYMANAGER->Draw();
  • In CGSGame::Leave muß der Gleiter nicht mehr deinitialisiert werden, stattdessen lassen wir den EntityManager den Level komplett abräumen: ENTITYMANAGER->CleanUp();

Download

Vor den Kulissen funktioniert also noch alles wie vorher, hinter den Kulissen haben wir aber einen kleines Management aufgezogen, mit dem wir nun endlich den ersten Feind einbauen können.

Leave a Reply

Your email address will not be published. Required fields are marked *