Einführung ins Triggersystem
Geschrieben von ThiemoEnglish abstract: A short introduction to the event trigger system.
Was unser bis 2007 federführender Programmierer Holger Meisel in etwas mehr als einem Jahr geschaffen hat, fasziniert mich immer wieder aufs Neue. Glow basiert auf einer vollständig selbst entwickelten Engine. Es kommt tatsächlich keine besondere Grafikengine zum Einsatz – kein XNA, kein Allegro, keine SDL, kein Box2D. Heutzutage würden wir diesen Punkt vermutlich anders angehen und vielleicht keine vollständige 3D-Engine aber zumindest eine leichtgewichtige Grafik- und Physik-Bibliotheken nutzen. Aber darum soll es hier nicht gehen. Die Glow-Engine baut auf OpenGL und Direct3D auf, wie es von den Herstellern ausgeliefert wird, und erlaubt bemerkenswerterweise das freie Umschalten zwischen beiden Ausgabearten. Die einzige Ausnahme ist die OpenAL-Bibliothek für die Ausgabe von Sounds und Musik.
Als Informatiker begeistert mich die unglaublich sauber implementierte Objektsstruktur am meisten. Jedes Objekt im Spiel hat im C++-Quelltext ein Äquivalent im Sinne der Objektorientierung. Stevie wird verletzt, indem seine Methode Player->Hurt(2) mit der Stärke der Verletzung aufgerufen wird. Ein Gegner stirbt? Enemy->Die(). Eine Tür ist zu öffnen? Einfach das passende Door->Unlock() absetzen.
Diese Methodenaufrufe schreien geradezu nach einem Event-System und tatsächlich wurde damals damit begonnen, ein solches zu implementieren. Leider war gerade das ein Punkt, der gegen Ende der geplanten Entwicklungszeit abgekürzt werden musste. Deshalb war im Spiel von 2007 nicht viel von dieser Ereignisbehandlung zu spüren. Stevie konnte Hebel umlegen und der Hebel öffnete eine Tür oder schaltete eine Blitzfalle aus. Der Tod eines Bossgegners beendete den aktuellen Level. Und das war es schon fast. Wie schade, dachte ich mir, und begann damit, an der Umsetzung weiterer Ideen zu arbeiten. Zum Beispiel:
- An einer bestimmten, vom Leveldesigner festgelegten Stelle tauchen hinter Stevie plötzlich neue Gegner auf und nehmen ihn überraschend in die Zange.
- Das Einsammeln eines Schlüssels löst einen Alarm aus und ruft einen besonders hartnäckigen Gegner herbei.
- Eine Tür schließt sich hinter dem Spieler wieder, um ihm dabei zu helfen, in größeren Labyrinthen nicht unendlich im Kreis zu laufen.
- Vor Stevies Nase knallt plötzlich eine Kiste oder sogar eine große Statue auf den Boden.
- Ein längerer Fahrstuhl wird erst durch die Anwesenheit des Spielers aktiviert und fährt genau im richtigen Moment los.
- Harmlos aussehende, inaktive Fallen werden bei Annäherung plötzlich aktiv und erschrecken den Spieler.
- In bestimmten Situationen, zum Beispiel wenn ein größerer Gegner stirbt, sterben auch alle verbliebenen kleinen Gegner im Umkreis.
Und so weiter. Der Fantasie der Leveldesigner (ihr erinnert euch, wir werden den Editor mitliefern) sind nur wenige Grenzen gesetzt. Es ist sogar möglich, solche Ereignisse an die Spielfigur Stevie Speed zu senden, so dass er zum Beispiel von selbst ein Stück läuft oder automatisch die Waffe wechselt.
Ein Trigger, der einen Gegner direkt hinter Stevie erscheinen lässt, sieht im Editor beispielsweise so aus:
Auslöser können sein:
- Hebel werden bewusst betätigt. Der Spieler muss Stevie vor den Hebel bewegen und ihn mit der Alt-Taste umlegen. Den Hebel, nicht Stevie. Hebel können allerdings nur einmal aktiviert werden. Das ist eine Designentscheidung von uns, um die Sache für den Spieler nicht unnötig kompliziert zu machen.
- Radius-Auslöser wie der oben im Bild sind im Spiel unsichtbar und werden ausgelöst, sobald sich Stevies Mittelpunkt innerhalb des Kreises befindet. Funktioniert ansonsten wie ein Hebel.
- Bei jedem anderen Objekt können an bestimmte Auslöser-Kommandos zusätzliche Kommando-Ketten gehangen werden. Zum Beispiel können zwei Gegner so verkettet werden, dass im Todesfall des einen Gegners sofort ein zweites
Die()-Kommando an einen anderen Gegner gesendet wird.
Mögliche Kommandos sind:
- Die() sorgt dafür, dass das Zielobjekt, an das dieses Kommando gerichtet war, verschwindet. Die meisten Objekte verschwinden in diesem Fall ganz unspektakulär. Sie sind einfach weg. Gegner und auch unser Stevie sterben natürlich wesentlich spektakulärer und lösen sich in Feuer und Rauch auf.
- Hurt() verletzt einen Gegner. Da die Verletzungsstärke im Editor nicht als Parameter übergeben werden kann, wird dem Gegner in diesem Fall immer ein Lebenspunkt abgezogen.
- Activate() gilt nur für Objekte, die auf das Drücken der Alt-Taste reagieren, zum Beispiel Hebel. Das Kommando löst das selbe Ereignis aus wie das Drücken der Taste.
- Lock() schließt Türen und schaltet Blitzbarrieren ein.
- Unlock() öffnet dementsprechend Türen und Barrieren und entsperrt Lifte.
- ToggleInventory() und einige weitere Toggle-Kommandos öffnen das Inventar, das Menü, schalten das Spiel in den Pause-Modus und einiges mehr.
- Außerdem gibt es noch einen ganzen Satz Kommandos, mit denen sich die Spielfigur steuern lässt: Loslaufen, stoppen, springen, zur nächstbesseren Waffe wechseln. Der Spieler hat allerdings das Recht, jederzeit einzugreifen. Es gibt bewusst kein Kommando, um dem Spieler die Kontrolle zu entziehen.
Eine Art Enable()-Kommando (ich bin mir bei der Benennung noch nicht ganz sicher) fehlt aktuell noch. Es soll wie oben beschrieben dafür sorgen, dass Kisten und Gegner aus dem Nichts erscheinen können. Meine aktuellen Versuche sehen noch so aus, dass ich versucht habe, das schon existierende Unlock-Kommando für Gegner (die bisher nichts mit Unlock anfangen konnten) zu implementieren. Mit durchwachsenem Erfolg. Was passiert in einer hardwarenahen Sprache wie C++, wenn ein Objekt zerstört wurde (ein Gegner ist gestorben), aber andere Objekte verweisen noch immer auf diesen nun ungültig gewordenen Pointer (der Tod eines Bossgegners soll den Tod aller übrig gebliebenen kleinen Gegner auslösen)? Das Spiel stürzt mit Zugriffsverletzungen ab. Abfangen wie in C# oder Java kann man diese Fehler nicht, dafür ist C++ nicht konstruiert. Also was tun?
Mein Plan: Zuerst einmal schaffe ich ein neues Kommando für das Erscheinen von bis dahin unsichtbaren Objekten. Dann muss in einem zweiten Schritt das Eventsystem so umgebaut werden, dass es nicht mit C-Pointern arbeitet, die wie erlebt schwer zu beherrschen sind, sondern mit Objekt-IDs. Sprich: Mit den eindeutigen, in Textform vorliegenden Kennzeichnern der Objekte (z.B. EnemyRinger_7). Diese IDs werden zum Glück automatisch vergeben. Der Vorteil dieser Methode ist gleichzeitig der einzige Nachteil: In dem Moment, in dem ein Ereignis ausgelöst wurde, muss erst aufwendig nach dem Objekt mit der angegebenen ID gesucht werden. Das kostet im Vergleich wesentlich mehr Rechenzeit als das simple Auswerten eines Pointers, geht aber trotzdem noch so schnell, dass es der Spieler nicht merken sollte.
Und wozu der Aufwand? Wir wollen euch den Editor geben und ihr sollt damit so kreativ wie möglich sein können, ohne dass euch das Spiel abstürzt, nur weil ihr etwas kreativer mit den Triggern wart als wir. Deswegen der Aufwand.

14. Oktober 2009 um 05:07
Schön geschrieben. An dieser Stelle kann man auch mal erwähnen, dass es nicht nur zum Spiel eine gute, ausführliche Anleitung geben wird sondern auch zum Editor. Das bringen nicht einmal viele AAA-Titel auf die Reihe.
14. Oktober 2009 um 13:16
Ist schon erstaunlich was du immer wieder auf die Beine stellst, es freut mich wirklich was aus dem Projekt geworden ist, auch wenn es wahrscheinlich nicht so rüberkommen mag… ;-)
15. Oktober 2009 um 16:38
Hört sich schonmal ganz gut an. Naja, hätte man manches im Voraus geplant, hätte man bestimmte Dinge wohl eleganter lösen können („Da die Verletzungsstärke im Editor nicht als Parameter übergeben werden kann, wird dem Gegner in diesem Fall immer ein Lebenspunkt abgezogen.“, statt der IDs kann man auch Smartpointer Objekte nehmen, ähnlich wie in Java), aber im großen und ganzen hört es sich schon ganz nett an.
Wobei mir die verschwindenden Objekte in meinem Spiel auch Sorgen machen, vor allen weil es keine gescheite Lösung gibt. Benutzt man den Pointer und löscht das Objekt, knallt es beim Zugriff, benutzt man Smartpointer oder Garbage Collection (wie in Java), wird das Objekt entweder nicht entfernt, wenn es das soll, oder aber, es scheint zu verschwinden, existiert im Hintergrund aber heimlich weiter. Beides führt zu seltsamen Ergebnissen.
Einfach Befehle an ein nicht mehr existierendes Objekt zu ignorieren führt ebenfalls zu Problemen, also muss man wohl oder übel an jeder Stelle im Programm darauf vorbereitet sein, dass das Objekt einmal weg sein könnte, was wirklich nicht sehr schön ist :(
22. Oktober 2009 um 09:55
Eine perfekte Zusammenfassung. Danke. Ich staune, dass zwei unterschiedliche Spiele von ganz verschiedenen Entwicklern vor den wirklich haargenau gleichen Problemen stehen können. Interessant. Die Glow-Engine verwendet übrigens schon Smartpointer, aber das löst mein oben beschriebenes Problem merkwürdigerweise nicht. Ich denke, da stimmt irgendwo etwas mit der Referenzzählung nicht.