Component-Based Entity Systems in Spielen
Das Design und die Regeln eines Spiels ändern sich im Laufe der Entwicklung kontinuierlich und können so täglich die Projektplanung zunichte machen. Component-Based Entity Systems sind ein hervorragender Ansatz, den vielen Nachteilen Vererbung nutzender Softwarearchitekturen aus dem Weg zu gehen.
- Nick Prühs
Das Design und die Regeln eines Spiels ändern sich im Laufe der Entwicklung kontinuierlich und können so täglich die Projektplanung zunichte machen. Component-Based Entity Systems sind ein hervorragender Ansatz, den vielen Nachteilen Vererbung nutzender Softwarearchitekturen aus dem Weg zu gehen.
Zu Beginn eines Spieleprojekts steht in der Regel ein ausführliches Gespräch mit dem Game Designer. Entwickler erfragen dabei unter anderem, worum es im Spiel geht und mit welchen Dingen der Spieler interagieren können soll. Die mehr oder weniger präsize Antwort umfasst in der Regel eine Fülle an Objekten wie furchteinflößende Kreaturen, verschlossene Türen, mächtige Waffen oder gewaltige Raumschiffe.
Danach beginnt die Planungsphase, in der Software-Ingenieure versuchen, eine Architektur auszuarbeiten, die alle Eventualitäten abdeckt. Um der damit einhergehenden Komplexität Herr zu werden, kommen gewaltige Diagramme, Abstraktionsschichten und häufig teure UML-Tools zum Einsatz.
Bevor es sich lohnt, die einzelnen Vor- und Nachteile der verschiedenen Architekturansätze für Spiele zu diskutieren, muss man sich auf einige Begriffe einigen. Der wichtigste ist dabei der des Entity, das jedes beliebige Objekt der Spielwelt beschreibt. Entities können sichtbar oder unsichtbar sein, sich durch die Welt bewegen, angreifen, explodieren, einem vordefinierten Weg folgen, als Ziel erfasst oder vom Spieler ausgewählt werden. Der Begriff findet quer durch sämtliche Spielgenres Verwendung.
Vererbung als Architekturgrundlage
Der erste Ansatz, ein Spiel zu realisieren, besteht aus verschiedenen Ebenen der Vererbung: Am Anfang steht eine Entity-Basisklasse, die gemeinsam mit ihren Subklassen den Hauptteil der Spiellogik kapseln soll. Als Beispiel lässt sich hierfür die Unreal Engine 3 anführen: Dort waren circa 90 Prozent aller Objekte im Spiel von der Klasse Actor abgeleitet. Sie enthielt Funktionen für das Rendering, das Abspielen von Animationen und Sounds sowie für Physiksimulation. Abgeleitete Klassen fügten weitere Spielfunktionen hinzu. So hatte etwa ein "Pawn" zusätzliche Variablen für Lebensenergie und konnte Schaden nehmen, ein "Projectile" hingegen konnte detonieren und dabei Effekte erzeugen.
Während dieser Ansatz zunächst nahe liegt, führt er früher oder später unweigerlich zu Problemen: Da die meisten objektorientierten Programiersprachen keine Mehrfachvererbung anbieten, hat fast jeder Spieleentwickler schon einmal mit dem sogenannten Diamond of Death zu tun gehabt.
Der Fall lässt sich folgendermaßen verdeutlichen: Angenommen, ein Entwickler arbeitet an einem Strategiespiel und leitet von der Entity-Klasse ab. Er legt eine neue Klasse Moveable an, deren Instanzen sich durch die Spielwelt bewegen können. Danach erzeugt er eine weitere Klasse Attackable für Gebäude und Bäume, die zwar zerstört werden, sich aber nicht bewegen können. Will der Entwickler nun einen tapferen Ritter im Spiel implementieren, der sich sowohl bewegen als auch Schaden nehmen kann, entsteht ohne Mehrfachvererbung automatisch ein Problem.
Wohin mit dem Code?
Dieses zugegebenermaßen etwas konstruierte Beispiel illustriert die Schwächen von Spielarchitekturen, die auf Vererbung aufgebaut sind. Keiner der gängigen Ansätze hierfür ist wirklich zufriedenstellend: Einerseits ließe sich der relevate Code einfach kopieren, was in den meisten Fällen keine gute Idee ist, da etwaige Bugs dann mehrmals anzugehen sind. Andererseits könnte man den Code entlang der Vererbungshierarchie in Richtung Wurzel verschieben. Auf diese Weise wächst die Entity-Klasse immer mehr – und damit der Speicher-Overhead, der mit fast jedem einzelnen Spielobjekt entsteht, das der Entwickler im Code instanziiert: Projektile reservieren Speicher für Lebenspunkte, obwohl sie gar nicht angegriffen werden können, Bäume für Bewegungsgeschwindigkeit, obwohl sie statisch sind. Die Klasse Actor.uc der Unreal Engine 3 beispielsweise kommt so auf 3773 Zeilen.
Doch es gibt noch weitere, auf den ersten Blick nicht ganz so offensichtliche Nachteile dieser Architektur: Neue Mitglieder im Entwicklerteam schrecken zunächst vor Änderungen zurück, da ihre Auswirkungen im Code entlang der gesamten Klassenhierarchie schwer zu erfassen sind. Zudem ist es in den meisten Sprachen nicht möglich, in überschriebenen Methoden den Aufruf der Methode der Basisklasse zu erzwingen. Das führt zu subtilen Bugs, deren Auffinden selbst gestandene Entwickler schon mal einen Nachmittag kosten kann.
Schließlich kann sogar die Aufrufreihenfolge abgeleiteter Methoden zur Herausforderung werden: Vielleicht möchte der Entwickler, dass der Ritter zunächst die Update-Methode der direkten Basisklassen Moveable aufruft, im Anschluss eigenen Code ausführt und erst danach den der Update-Methode der Wurzelklasse Entity. Auch das ist in den meisten Sprachen nicht möglich.
Architekturen, die auf Vererbung setzen, sind schwer zu entwickeln, zu erweitern und zu warten. Allerdings gibt es eine Alternative.