Abenteuer Spieleentwicklung – Teil 4: Hüpfen statt Taktieren

Nach einem erfolgreichen Miniprojekt und einem auf Eis gelegten SRPG habe ich mich Ende 2016 endlich an mein Lieblings-Genre gewagt. Eines ist dabei ganz klar, dass ein 3D-Spiel überhaupt erst in Reichweite gelangen konnte, ist ganz vorrangig der modernen Engine-Entwicklung und dem Indie-freundlichen Geschäftsmodell von Unity zu verdanken.

Natürlich habe ich mir auch in der Vergangenheit bereits Gedanken gemacht, ob und wenn ja wie ich ein 3D-Spiel umsetzen könnte, 3D Grafik bietet aber unzählige Fallstricke, die bis dato immer eine zu hohe Hürde für mich bedeutet hätten: Die notwendige Recherche und der Programmieraufwand hätten zur Folge gehabt, dass nicht anzunehmen gewesen wäre, dass ich jemals fertig würde. Eine 3D Umgebung auf theoretischer Ebene zu modellieren ist natürlich erstmal kein Problem: Man wählt einen dreidimensionalen Vektorraum – wahlweise mit feinen Ganzzahlschritten oder aber vorzugsweise mit Gleitkommaeinträgen und modelliert 3D Objekte anhand ihrer Position im Raum und ihrer Ausmaße. So kann man eine Kugel beispielsweise durch Angabe des Mittelpunkts und des Radius definieren oder einen Quader durch Angabe von Mittelpunkte, Höhe, Breite und Tiefe.

Die erste Fassung des Modells von Regina und Mac: Noch mit einer sehr hohen Polygonzahl und einem Shader, der eher an ein Xbox 360- als ein Nintendo 64-Spiel erinnert.

Allerdings ist es mit einem Modell natürlich nicht getan, der Spieler muss die 3D-Welt natürlich auch sehen können. Hierzu müssen dann alle 3D-Objekte perspektivisch korrekt dargestellt werden. Das ist komplizierter als man zunächst denkt, denn um die Tiefe zu simulieren, müssen Abstände und Formen auf einer Kugel um den Blickpunkt skaliert werden. Fein säuberlich muss entschieden werden, welches Objekt über welches gezeichnet wird und welche Objekte gar nicht erst im Frustrum der Kamera erscheinen. Und eine Perspektivkorrektur der Texturen möchte man sicher auch noch vornehmen, sonst ergibt sich ein äußerst fragwürdiges Bild, wie man es von frühen PS1-Spielen kennt. Wenn ich all dies von Hand hätte programmieren müssen, hätte ich sehr viel recherchieren und sehr viel herumprobieren müssen, zum Design des eigentlichen Spiels wäre ich als Einzelkämpfer in Sachen Programmierung wohl nie gekommen. Zum Glück ist es aber so, dass alle die Punkte, die ich bis hierher aufgezählt habe, von Unity übernommen werden…

Ein 3D Objekt auf den Bildschirm zu bekommen und per Controller-Eingabe zu bewegen ist also zunächst einmal tatsächlich so einfach, dass man das sogar als Programmieranfänger relativ schnell hinbekommen dürfte. Nachdem ich sicher war, eine gute Sprungkurve gefunden zu haben (ungefähr eine gestauchte Parabel) gab es eine weitere Hürde zu überwinden, ohne die ich das Projekt schon drangegeben hätte, bevor es richtig gestartet wäre: Die Erstellung eines Charakters. Von Beginn an war für mich zwar klar, dass ich im Leveldesign auf einfache geometrische Figuren setzen möchte, da das meines Erachtens besser mit dem Spieler kommuniziert, wenn man schwierige Hüpfpassagen einbauen möchte, als komplexe Strukturen, die sich in Sachen Kollisionserkennung aus Spielersicht nicht ganz vorhersehbar verhalten. Ich wollte zumindest einen Charakter erschaffen, der auf dem Nintendo 64 nicht allzu negativ aufgefallen wäre – was angesichts meines künstlerischen Talents alles andere als garantiert ist.

Das allererste Testareal: Eine rote Hölle.

Zum Glück habe ich aber schnell das Programm Sculptris gefunden, das es ermöglicht, ausgehend von einer (oder mehreren) Kugel(n) wie mit Knetmasse seine Figur zu formen. Ein erster Entwurf des Dinosauriers, den ich gerne im Spiel verwenden wollte, hat allerdings im Vergleich zu den Skizzen ein kleines Problem gehabt: Dadurch, dass die Grundform immer eine Kugel war, war der Charakter runder geworden als geplant. Insbesondere die länglich gedachte Form der Nase kam nur schwerlich herüber. Nach einigen Stunden Feinarbeit standen der Dinosaurier und der Vogel allerdings endlich in einer zunächst noch sehr großen Polygonzahl auf dem Bildschirm. Nach einigen Reduktionsschritten hatten die beiden meines Erachtens endlich den gewünschten N64-Look und wurden zudem auch von meinem Freund abgesegnet, so dass es tatsächlich richtig losgehen konnte.

Ein Name für das Spiel war auch schnell gefunden: Regina & Mac. Da man einen Tyrannosaurus Rex (weibliche Form von Rex: Regina) und einen roten Ara (englisch: Macaw) spielt, habe ich die beiden Namen ausgewählt. Überdies ist der Name Regina auch eine Widmung an meine kurz zuvor verstorbene Mutter, die ebenfalls Regina hieß. Ursprünglich hieß der Dinosaurier Regina und der Ara Mac, aber um zu vermeiden, dass ein männlicher frecher Vogel einen stummen weiblichen Charakter herumkommandiert, habe ich im Laufe der Entwicklung die Namen der beiden getauscht.Die wichtigste Aufgabe aus meiner Sicht ist es im Folgenden gewesen, das Spiel mit einem Kollisionssystem auszustatten. Mindestens so leicht gesagt wie getan, habe ich mir gedacht, denn Unity kommt von Haus aus mit einem Kollisionssystem daher. Man kann also seine Spielobjekte mit sogenannten Collidern ausstatten, die in regelmäßigen Abständen überprüfen, ob sie mit anderen Collidern in Berührung geraten. Kollidieren zwei Collider, so wird ein so genanntes Event ausgelöst und dadurch eine Funktion aufgerufen, die ich programmieren kann. Klingt großartig und ist auch einfach zu verwenden. Problem also gelöst?

Farbe macht nicht alles gut, aber doch vieles besser.

Leider ganz und gar nicht, denn es gibt gleich mehrere Probleme mit dem Kollisionssystem, die es für mich unpraktikabel gemacht haben, es zu verwenden. Zunächst einmal wäre da die Form des Charakters. Unity bietet einige Standardkollisionsobjekte an, die sich algorithmisch gut erfassen lassen, das wären Quader (genannt Box), Pille (das ist ein Zylinder mit Halbkugeln oben und unten), Kugel sowie Mesh-Collider. Der Mesh-Collider verwendet die Polygone des 3D Objekts und entscheidet auf der Basis, ob es zu einer Kollision kommt. Das ist für einen Spielcharakter aber völlig unangemessen, denn beispielsweise ist es alles andere als wünschenswert, dass die exakte Fußposition, oder im Fall von Mac gar die Nase entscheidend dafür sind, ob der Charakter kollidiert oder nicht. Zudem ist ein Mesh-Collider auch sehr teuer (sprich: verbraucht viel Rechenzeit, die wichtigste Ressource in der Spieleprogrammierung), weil potentiell alle Dreiecke aus denen Regina und Mac bestehen, überprüft werden müssen.

Ein Quader ist keine brauchbare Approximation für Regina & Mac, weil er immer dann, wenn die Charaktere sich auf der Stelle drehen – oder aber sich bewegende Plattformen mit den Charakteren interagieren, die Ecken zu einem Problem werden. Nichtsdestotrotz wäre ein Quader immernoch die beste Wahl, denn eine Kugel und eine Kapsel – letzteres wird beispielsweise bei Crash Bandicoot N-Sane Trilogy für Crash verwendet – geben dem Charakter eine runde Unterseite, was zu haarigem Verhalten an der Ecke von Plattformen führt. Kurzum: Die Standardformen sind keine gute Option. Was ich gerne verwendet hätte, wäre ein Zylinder und wenngleich ein Zylinder in Unity als geometrische Form existiert, als Kollisionsobjekt existiert er nicht (Unity scheint eine Kapsel als Ersatz zu empfehlen).

Regina & Mac zusammen mit ihrer Kollisions-Repräsentation. Der halbtransparente Zylinder zeigt, wie Regina und Mac in der Spiellogik aussehen.

Hinzu kommt, dass das Eventsystem mir wenig Spielraum bietet, zu kontrollieren, wann auf eine Kollision reagiert wird und insbesondere nicht erlaubt, explorativ aus einem Raum an möglichen Positionen für den Charakter ohne einen Frame Verzögerung die richtige Wahl zu treffen. Kurzum: Das Kollisionssystem von Unity ist nach einigem Herumexperimentieren leider ausgeschieden. Im Ergebnis habe ich als nächstes mein eigenes Kollisionssystem geplant und umgesetzt. Die Idee war, dass ich ebenfalls auf geometrische Grundformen setze, allerdings keine Mesh-basierte Kollision vorsehe. Alle Objekte im Spiel sollten ohnehin aus Gründen der Klarheit aus einfachen geometrischen Figuren aufgebaut sein, so dass Mesh-Kollision keinen Sinn ergeben hätte. Daher habe ich mich auf einen einfachen Satz an Kollisionsobjekten festgelegt: Kugel, Quader, achsen-orientierter Quader (d.h. ein Quader, der parallel zu den Koordinatenachsen verläuft), Pille, in der Mitte aufgeschnittener Quader und, selbstverständlich, Zylinder.

Die Grundformen für die Kollisionserkennung: Kapsel (grün), Quader (rot), Zylinder (blau), Kugel (braun), halber Quader (grau).

Die Unterscheidung zwischen achsen-orientiertem Quaders und Quader dient der Performance-Optimierung, da die Kollision zwischen zwei achsenorientierten Quadern besonders simpel ist, wohingegen beliebig rotierte Quader nur mit trigonometrischen Funktionen handzuhaben ist. Da Regina & Mac auf der veralteten Wii U Hardware mit 60 Bildern in der Sekunde laufen sollte, war mir von Beginn an klar, dass ich möglichst optimalen Code schrieben werden müsste. Die schwache CPU der Wii U und der Umstand, dass Unity sehr CPU-lastig ist, gerade bei selbstgeschriebenem Code, forcierte die Entscheidung von Anfang an jeden Schritt mit einem Optimierungsschritt zu begleiten. Anfangs noch nicht vorgesehen, aber gegen Ende der Entwicklungszeit hinzugekommen ist zudem die Unterscheidung zwischen statischen Kollisionsobjekten und beweglichen Kollisionsobjekten, weil man bei statischen Kollisionsobjekten bestimmte Informationen nur einmalig berechnen muss; beispielsweise die Position der Ecken eines Quaders.

Für die Formen, die auch in Unity als Standardformen vorhanden sind, gibt es (paarweise) gute Buchquellen um sich algorithmisch gute Lösungen anzueignen. Die wesentlichen Ideen sind, dass man für die Kollision mit einem Quader eine Transformation des zu vergleichenden Objekts in den Raum des Quaders vornehmen muss und dann schauen muss, ob ein Punkt des zweiten Objekts innerhalb der Grenzen des Quaders liegt (die teure Transformation entfällt, wenn der Quader achsenorientiert ist), für Kollisionen mit Kugeln muss man nur den Abstand vom Mittelpunkt betrachten und für Kapseln muss man den Abstand von einer Strecke im Raum betrachten. Die konkreten Paarungen müssen dann natürlich die geometrischen Betrachtungen beider Einzelobjekte in Zusammenhang setzen, die mathematischen Details dahinter würden hier aber den Rahmen sprengen. Ich empfehle das exzellent geschriebene Buch „Real Time Collision Detection“ von Christer Ericson, der lange Zeit bei Sony gearbeitet hat und mittlerweile bei Activision arbeitet. In diesem Buch werden die genauen mathematischen und algorithmischen Überlegungen hinter den gerade genannten Kollisionsobjekten (und natürlich noch viel mehr) auf anschauliche Weise diskutiert. Eine mathematisch-informatische Grundbildung ist allerdings empfehlenswert.

Was allerdings blieb war das Problem der Kollisionserkennung für Zylinder und anders als die oben genannten geometrischen Objekte, werden Zylinder in der Literatur eher spärlich behandelt. Ich habe zwar einen Algorithmus gefunden, der wissenschaftlich publiziert wurde, bevor ich ihn implementiert habe, habe ich aber eine nach kurzer Überschlagsrechnung effizientere Lösung gefunden: die Und-Verknüpfung von Kapsel und Quader. Ein Zylinder kann verstanden werden als der Schnitt zwischen einem Quader und einer Kapsel, so dass die Kapsel die Kanten des Quaders abschneidet und der Quader die Halbkugeln des Quaders. Der Vorteil ist, dass ich für den Zylinder demnach keine aufwendige Mathematik betreiben muss, sondern auf bestehende optimierte Lösungen zurückgreifen kann. Denkt man jetzt an die Unity-Implementation zurück, könnte man natürlich einwenden, dass man einfach eine Und-Verknüpfung der beiden Collider für Kapsel und Quader von Unity verwenden könnte, allerdings ist das Problem dann die Event-Struktur, die keine Verundung zulässt. Mit einer geschickten Verwendung und Rücksetzung von Variablen könnte man zwar dennoch eine Zylinder-Kollision hinbekommen, doch potentiell verliert man dann durch das Warten auf das gepaarte Event zusätzliche Zeit, könnte eine Kollision also womöglich erst mit einem oder sogar noch mehr Frames Verzögerung erkennen. Für die angepeilte maximale Reaktionsgeschwindigkeit des Spiels wäre das ein massives Problem.

Die Kollision des Zylinders kann als Schnitt der Kollision mit einem Quader und einer Kapsel verstanden werden.

Die Implementation des Kollisionssystem hat mehrere Wochen in Anspruch genommen, weitere Optimierungen sind fast über die gesamte Entwicklungszeit hinzugekommen, im Endeffekt denke ich aber, dass die Entscheidung, ein eigenes Kollisionssystem zu schreiben, jedenfalls unter der Voraussetzung, dass das Spiel ohnehin auf geometrische Grundfiguren setzen würde und daher Terrain- und Mesh-Collider nicht notwendig sind, eine gute war. Im Vergleich zu anderen Unity 3D Jump & Runs wie FreezeME ist es meiner Beobachtung nach gelungen, die Kollisionslatenz zu minimieren.

Nach Umsetzung des Kollisionssystems stand endlich ein erster spielbarer Protoyp mit einem Debug-Raum, in dem nach und nach verschiedene Plattformtypen ausprobiert und konfiguriert werden konnte. Das eigentliche Design des Spiels und der Level konnte also beginnen. In der nächsten Ausgabe von Abenteuer Spieleentwicklung werden wir daher auf die mechanischen Designentscheidungen für Regina & Mac, sowie das Leveldesign eingehen.