Statische Klassen als Hilfsmittel zur zentralen Datenspeicherung


Bei der Entwicklung eines Spieles gibt es viele verschiedene Daten zu speichern, welche zwar eigentlich zu bestimmten Objekten gehören, auf der anderen Seite aber auch eine zentrale Speicherung wünschenswert wäre. In unserem Projekt sind dies beispielsweise Materialien, Balanceinformationen oder auch Sounds. Wir haben festgestellt, dass es für diese Art von Daten praktisch ist, wenn diese zentral an einer einzigen Stelle abgespeichert werden. Daher soll es in diesem Blogeintrag um ein einheitliches Konzept gehen, welches wir verwendet haben, um diese Art von Daten zu speichern.

Eine zentrale Speicherung (im Gegensatz zu einer verteilten in X Klassen) hat den entscheidenden Vorteil, dass man sofort weiß, wo man nachsehen muss, falls man einen Datensatz ändern möchte. Will man beispielsweise ein bestimmtes Material austauschen, so gibt es bei uns nur eine Datei, welche dafür angepasst werden muss. Ein weiterer Vorteil ist die einfachere Zusammenarbeit im Team. Wenn es für bestimmte Daten (z. B. Materialien) nur eine zentrale Anlaufstelle für die Speicherung gibt, weiß jedes Teammitglied auch sofort, wo es suchen muss. Gerade im Fall von Materialien können in unserem Projekt die Modellierer ihre Modelle mit Testtexturen versehen und Einträge für ihre Materialien hinterlegen. Darauf aufbauend können die Texturierer die Materialien durch die richtigen ersetzen. Und das alles über eine zentrale Anlaufstelle.

Neben den bereits angesprochenen Materialien verwenden wir diese Technik zur Zeit auch noch für Balanceinformationen und für Sounds. Unter dem Balancing sind vor allem Daten relevant, die eine starke Auswirkung auf den Spielfluss haben (und natürlich auch sehr stark vom jeweiligen Spiel abhängen). Bei uns ist dies beispielsweise das Bevölkerungswachstum der Stadt in Einwohner pro Sekunde. Die zentrale Stelle für Sounds sammelt bei uns alle Soundeffekte und die verwendete Musik.

Die technische Realisierung läuft bei uns über statische Klassen ab. Das hat den großen Vorteil, dass von überall heraus sehr einfach auf die Daten dieser Klasse zugegriffen werden kann. Werden die Daten dagegen nur in einem bestimmten Objekt abgespeichert, muss man immer dafür sorgen, dass eine entsprechende Referenz auf dieses Objekt auch in allen Klassen zur Verfügung steht. Das ist etwas, was mit der Zeit sehr lästig werden kann. Zusätzlich ist die Verwendung einfacher. Man benötigt nur den Klassennamen und die entsprechende Methode, um z. B. einen Soundeffekt abzuspielen. Wenn man mit Objekten arbeitet, besteht zudem gerade in C++ immer die Gefahr, dass durch falsche Verwendungen bei der Parameterübergabe (ungewollte) Kopien von Objekten erzeugt werden.

Im Folgenden ist die Grundstruktur für eine solche statische Klasse am Beispiel unseres Soundloaders gegeben (Headerdatei, kompletter Inhalt kann auch auf Github eingesehen werden):


class VSoundLoader
{
private:
	VSoundLoader() = delete;
	VSoundLoader(const VSoundLoader&) = delete;
	VSoundLoader(const VSoundLoader&&) = delete;
	VSoundLoader& operator=(const VSoundLoader&) = delete;
	VSoundLoader& operator=(const VSoundLoader&&) = delete;
	~VSoundLoader() = delete;

private:
	DEBUG_EXPRESSION(static bool initDone);
	// More variable declarations...

public:
	static void init(/** Some parameter **/);
	static void playBackgroundMusicIngame();
};

Als erstes wird unterbunden, dass ein Objekt dieser Klasse erzeugt werden kann (schließlich soll die Klasse ja komplett statisch sein). Dazu werden sämtliche Implementierungen, welche der Compiler normalerweise automatisch erzeugt, abgeschaltet. Es würde reichen, nur den Konstruktor zu verbieten, aber der Vollständigkeit halber wurde hier alles ausgeschlossen.
Als nächstes gibt es eine Variable, welche anzeigt, ob die statische Klasse initialisiert wurde. Dies ist praktisch als Fehlerüberprüfung (im Debug Modus), wie in der Implementierungsdatei noch zu sehen.
Dann kommen noch die Methoden, welche auf dieser Klasse ausgeführt werden können. Bei allen unseren Klassen gibt es immer eine init()-Methode, welche die Initialisierung der einzelnen Variablen übernimmt. In diesem Fall können dort die einzelnen Sounddateien eingelesen werden. Es muss dafür gesorgt werden, dass diese Methode einmal während des Programmstartes (möglichst am Anfang) aufgerufen wird. Danach kann die Klasse frei verwendet werden.

Abschließend noch ein kleiner Blick in die zugehörige Implementierungsdatei:


DEBUG_EXPRESSION(bool VSoundLoader::initDone = false);
DEBUG_EXPRESSION(static const char* const assertMsg = "SoundLoader is not initialized");
//More variable definitions...

void VSoundLoader::init(/** Some parameter **/)
{
	// Some init stuff

	DEBUG_EXPRESSION(initDone = true);
}

void VSoundLoader::playBackgroundMusicIngame()
{
	ASSERT(initDone, assertMsg);

	// Some magic
}

Hier wird jetzt auch klar, warum es eine Variable gibt, welche den Initialisierungsstatus überprüft. Am Ende der init()-Methode wird diese Variable auf true gesetzt. Bei allen anderen Methoden wird nun zuerst überprüft, ob die Initialisierung auch wirklich durchgeführt wurde. Dadurch wird die korrekte Verwendung dieser Klasse sichergestellt. Wenn man jetzt von irgendwo im Programmcode die Hintergrundmusik abspielen möchte, braucht man nur VSoundLoader::playBackgroundMusicIngame() aufzurufen und schon gibt es musikalische Untermalung.

Wie bereits erwähnt, haben wir bis jetzt insgesamt drei solcher Klassen und soweit hat sich dieses Vorgehen auch als eine gute Idee herausgestellt.