Skip to content

Eine offene Welt im Browser bauen, Teil 29: Ein Controller, jeder Körper

Von Oleg Sidorkin, CTO und Mitgründer von Cinevva

Neu hier? Nutze den Serienleitfaden. Er erklärt, was ein Spike ist, und verlinkt alle Teile.

Teil 28 behandelte Gras und Okklusion. Achtundzwanzig Teile haben eine Welt geschaffen, in der man stehen kann: lesbares Terrain, Wasser, in dem man schwimmen kann, Vegetation, die bis zum Horizont überzeugt, und einen Server, der sich an deine Änderungen erinnert. In diesem Teil geht es um das Wesen, das sich durch all das hindurchbewegt. Und genau hier zahlt sich die gesamte Engine aus, denn das Ziel ist nicht einfach ein Spieler-Controller. Es ist ein Controller, dem egal ist, welchen Körper er steuert, woher dessen Animationen stammen oder ob ein Mensch oder eine KI am Steuer sitzt. Spike 58 baut Bewegung als Stapel austauschbarer Verhaltensweisen über einer Physik-Engine auf, die nichts über Fortbewegung weiß, und beweist die Architektur anschließend, indem drei verschiedene Körper mit jeweils einer Instanz davon gesteuert werden. Spike 59 setzt einen echten retargeteten Avatar auf diesen Controller, ohne eine einzige Zeile daran zu ändern. Spike 60 ergänzt ein anderes Animationspaket, das überhaupt kein Retargeting benötigt, und lagert die Logik zur Clip-Auswahl in eine Komponente aus, die sich tatsächlich testen lässt.

Eine Physik-Engine, die nichts über Gehen weiß

Spike 58 in einem neuen Tab öffnen ↗ · Quellcode ansehen

Die Designregel ist kompromisslos, und genau darum geht es: Die Kapsel-Engine enthält keinerlei Fortbewegungslogik. Kein Gehen, kein Laufen, kein Springen, keine Reibung, keine Begrenzung der Höchstgeschwindigkeit, nicht einmal Schwerkraft. Sie integriert eine kinematische Kapsel gegen das Terrain und nichts weiter. Jedes Fortbewegungsverhalten – Gehen, Rutschen, Gleiten, Klettern, Schwimmen, Ducken, Ausdauer – lebt in einem eigenständigen Controller, der bei der Engine registriert wird. In jedem Frame aktualisiert die Engine alle Controller, fragt jeden einzelnen, ob er die Kontrolle übernehmen möchte, und lässt den Kandidaten mit der höchsten Priorität die Geschwindigkeit setzen. Gehen hat keinen Sonderstatus. Es ist lediglich der Controller mit der niedrigsten Priorität, der immer Ja sagt, und damit der Standard, wenn nichts anderes ausgelöst wird. Ein Controller besteht aus sechs kleinen Funktionen: einem tick, das seinen internen Zustand in jedem Frame aktualisiert, selbst wenn der Controller nicht aktiv ist; einem reinen wantsControl-Prädikat, das den Frame beansprucht; einem applyForces, das nur beim Gewinner ausgeführt wird, die Geschwindigkeit setzt und bei Bedarf seine eigene Schwerkraft anwendet; sowie optional onEnter, onExit und stateName. Schwimmen gibt aus applyForces den Wert ownsCollision: true zurück, um die Terrainbehandlung zu übernehmen, weil die Auftriebsfeder sonst gegen das Einrasten der Füße am Boden arbeiten würde.

Selbst Dinge, die keine Fortbewegung sind, dringen durch diesen Vertrag hindurch, und genau das erzwang die nächste Idee. Das entscheidende Element der Architektur sind Kanäle. In einem früheren Einkanal-Design lief alles durch dieselbe Prioritätsentscheidung. Das bedeutete, dass eine Ausdauerverwaltung oder eine geduckte Haltung so tun musste, als wäre sie Fortbewegung, und dann über einen wantsControl-Hack mit dem Rückgabewert false die Kontrolle ablehnen musste, nur um ihre Buchhaltung auszuführen. Die Lösung teilt Controller in benannte Kanäle auf, die unabhängig voneinander priorisiert und in einer festen Reihenfolge angewendet werden: zuerst Ressourcen, dann Haltung, dann Fortbewegung. Ressourcen werden zuerst verarbeitet, weil ihre Änderungen, etwa der Ausdauerverbrauch, von den anderen gelesen werden. Haltung folgt als Zweites, weil die durch das Ducken verringerte Kapselhöhe feststehen muss, bevor der Geh-Controller sie liest, um die Höchstgeschwindigkeit zu begrenzen. Fortbewegung läuft zuletzt und besitzt den Schreibzugriff auf die Geschwindigkeit des aktuellen Frames. So können ein Ausdauerbeobachter, ein Duck-Modifikator und ein aktiver Schwimm-Controller sauber nebeneinander bestehen, jeweils in ihrem eigenen Kanal, ohne dass ein Controller darüber lügen muss, was er ist. Die Demo visualisiert all das mit einer unverkleideten Kapsel, deren Farbe sich abhängig vom aktiven Controller ändert. So lässt sich die Prioritätsentscheidung direkt beobachten: Grünes Gehen wechselt an einem steilen Hang zu Orange, wenn Rutschen übernimmt, im See zu Cyan, wenn Schwimmen gewinnt, und in der Luft zu Cremefarben, wenn du Gleiten antippst.

Eine Engine, drei Körper

Der eigentliche Test für „enthält keinerlei Fortbewegungslogik“ ist nicht der Spieler. Entscheidend ist, ob dieselbe unveränderte Engine auch etwas steuern kann, das überhaupt kein Spieler ist. createCapsuleEngine ist eine reine Factory ohne Zustand auf Modulebene, ohne Singletons und ohne instanzbezogene Nebeneffekte, daher instanziiert der Spike sie dreimal. Der Spieler ist eine Instanz mit dem vollständigen Controller-Satz. Ein reitbares Pferd ist eine zweite Instanz, die ausschließlich einen Geh-Controller registriert. Damit wird die gesamte Idee greifbar: Das Bewegungsrepertoire eines Körpers besteht einfach aus den registrierten Controllern. Das Pferd ist daher in der Ebene schneller und kann physisch weder klettern noch schwimmen oder gleiten, weil diese Controller nie hinzugefügt wurden. Ein autonomer NPC ist eine dritte Instanz, die in jedem Frame zusammen mit dem Spieler aktualisiert und von einem Umherstreifen-Controller gesteuert wird. Dieser erzeugt seine eigenen Eingaben, sodass der Körper sich selbst lenkt, ohne dass jemand die Tastatur berührt. Die Engine erfährt nie, dass einer ihrer Körper ein Pferd oder ein anderer KI-gesteuert ist. Sie alle verwenden denselben Kapsel-Integrator mit unterschiedlichen Controller-Listen.

Das Aufsteigen ist der einzige Teil, der bewusst außerhalb der Engine lebt. Um die Kontrolle zwischen Spieler und Pferd zu wechseln, müssen zwei Engines koordiniert werden. Ein Controller läuft innerhalb einer Engine und kann diese Grenze nicht überschreiten, daher befindet sich die Reitlogik auf Host-Ebene: eine kleine Zustandsmaschine, die entscheidet, welche Engine in diesem Frame aktualisiert wird, die andere einfriert, den Reiter über drei Viertel einer Sekunde per Smoothstep sanft in den Sattel bewegt und der Kamera mitteilt, welchem Körper sie folgen soll. Die Engine-Factory erfährt von alldem nichts. Diese Grenze zieht die Architektur und hält sie konsequent ein. Verhaltensweisen, die zu einem einzelnen Körper gehören, sind Controller; die Koordination zwischen Körpern ist Aufgabe des Hosts. Weil beides getrennt bleibt, würde ein vierter oder vierzigster Körper nichts grundsätzlich Neues kosten.

Ohne Browser getestet

Da die Engine weder auf window noch auf document oder Three.js zugreift, läuft sie vollständig headless. Der Spike enthält ein Node-Testsystem, das die Terrain-Schnittstelle nachbildet, die Engine Frame für Frame mit vorgegebenen Eingaben steuert und den resultierenden Zustand prüft. So werden Regressionen, die sich durch Spieltests nur mühsam finden lassen, stattdessen von einem Skript erkannt: der Übergang von Sprung zu Landung, die Schwellenwerte für Ein- und Austritt beim Schwimmen, das automatische Beenden des Kletterns, wenn eine Oberfläche flacher wird, und die Verbrauchsraten der Ausdauer. Wenn dieselbe Folge aus Eingaben und Zeitschritten zweimal eingespeist und anschließend auf byte-identische Ausgabezustände geprüft wird, lässt sich die Deterministik der Engine festschreiben – eine Eigenschaft, auf die ein späterer Netzwerk-Build angewiesen sein wird. Eine vom Testsystem gefundene Regression ist besonders erwähnenswert: Beim Wechsel von begehbarem Boden auf einen zu steilen Hang aktivierte sich früher der Rutsch-Controller und schleuderte den Spieler den Hang hinauf zurück. Die Lösung war eine Abstiegsbedingung. Das Rutschen beginnt nur, wenn die Kapsel tatsächlich auf den Hang hinabfällt. Wer nun horizontal gegen eine steile Fläche läuft, wird sauber blockiert, statt zu rutschen, und der Test mit der Aussage „Das Laufen gegen einen nicht begehbaren Hang blockiert den Spieler“ sorgt dafür, dass es so bleibt.

Einen echten Avatar einbinden, ohne die Controller anzufassen

Spike 59 in einem neuen Tab öffnen ↗ · Quellcode ansehen

Spike 59 prüft, ob die Controller-Schicht wirklich vom Körper entkoppelt ist. Er ersetzt die farbige Kapsel durch einen echten geskinnten Charakter: das 3MIKE-FBX-Rig mit darauf retargeteten Clips aus der Quaternius Universal Animation Library. Die Controller ändern sich dabei überhaupt nicht. Die Nahtstelle ist eine einzelne Zeichenkette. Jeder Controller meldet bereits über stateName einen Zustandsnamen – idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle – und die Avatar-Schicht ordnet diesen Namen über eine Alias-Tabelle einem retargeteten Clip zu und blendet beim Wechsel über. Der Geh-Controller wählt seinen Unterzustand dynamisch aus Bodenkontakt, vertikaler Geschwindigkeit und horizontalem Tempo. Dadurch steuert ein einziger Geh-Controller idle, walk, run, jump, fall und land, während der Avatar lediglich dem gemeldeten Namen folgt. Da dieser Spike nur einen Spieler hat, verzichtet der Avatar auf die indirekte Darstellung über Ganzzahlen im Netzwerkprotokoll und die Abstraktion für mehrere Charaktere aus den früheren Netzwerk-Spikes und ordnet den Zustandsnamen direkt einem Clip zu. Der Beweis liegt darin, dass das gesamte visuelle Upgrade von der Kapsel zum geriggten Menschen keine einzige Zeile Fortbewegungscode berührt hat – genau diesen Vorteil soll ein austauschbarer Controller bieten.

Ein Paket ohne Retargeting und eine testbare Clip-Auswahl

Spike 60 in einem neuen Tab öffnen ↗ · Quellcode ansehen

Spike 60 bindet einen dritten Körper ein, Syntys POLYGON Base Locomotion Pack, und der Loader besteht fast aus nichts. Jeder Clip wird als eigenständige FBX-Datei ausgeliefert, die eine eingebettete Kopie desselben Synty-Skeletts sowie eine einzelne gebackene Animation enthält. Da das Charakter-Rig und alle Clips identische Knochennamen verwenden, kannst du den Clip einfach aus fbx.animations[0] entnehmen und direkt im Mixer des Charakters abspielen, ohne dass eine Retargeting-Bibliothek beteiligt ist. Three.js löst die Ziele von Animationstracks anhand der Knochennamen statt über die Objektidentität auf. Ein Synty- oder Mixamo-artiges Paket, das für das passende Rig erstellt wurde, funktioniert daher direkt. Das ist der bewusste Gegensatz zum UAL-Pfad aus dem vorherigen Spike, der aufwendiges Retargeting benötigt, weil die Quellclips und das Ziel-Rig für unterschiedliche Skelette erstellt wurden. Derselbe Controller, dieselbe Nahtstelle über Zustandsnamen, dahinter zwei völlig verschiedene Animations-Pipelines.

In der anderen Hälfte von Spike 60 geht es darum, die Logik zur Clip-Auswahl testbar zu machen. Die Entscheidung, welcher Clip abgespielt werden soll, steckt voller Ermessensschwellen. Diese Logik war zuvor in der Avatar-Schicht neben FBXLoader und dem DOM vergraben, wo sie sich nicht isoliert ausführen ließ. Der Spike extrahiert die Auswahl in reine Funktionen, die weder Three.js noch das Fenster berühren: Sie erhalten einen einfachen Spieler-Datensatz – Geschwindigkeit, horizontales Tempo, Blickrichtung, Bodenkontakt, Bodennormale und Aufprallgeschwindigkeit – sowie einen Stellvertreter für die Clip-Aktion und geben eine Zeichenkette mit dem Clip-Alias zurück. Dadurch können die Schwellenwerte als benannte und durch Assertions abgesicherte Konstanten definiert werden. Sprünge werden anhand von Geschwindigkeitsbereichen, die an der tatsächlichen Sprintschwelle des Geh-Controllers ausgerichtet sind, in Geh-, Lauf- und Sprintvarianten aufgeteilt. Landungen werden anhand der Aufprallgeschwindigkeit als weich, mittel oder hart eingestuft. Clip-Varianten für bergauf und bergab werden über ein Skalarprodukt der Hangprojektion ausgelöst, mit einer Totzone für ebene Clips unterhalb von etwa sieben Grad. Ein Übergang vom Stillstand zur Fortbewegung wird immer vorwärts aufgelöst, damit beim Loslaufen aus dem Stand nie ein rückwärts abgespielter Clip erscheint. Und ein Übergang zum Anhalten wird durch eine Mindestdauer in der Schleife begrenzt, weil Syntys Stop-Clips für verschiedene Fußphasen etwa eine Sekunde inszenierter Verzögerung enthalten, die lächerlich aussieht, wenn sie an einen kaum begonnenen Schritt angehängt wird. Eine kleine Zustandsentprellung verzögert den Fallzustand um einige Frames, damit der Verlust des Bodenkontakts für einen einzigen Frame beim Überqueren einer Naht die Animation nicht flackern lässt. Da all das aus der Rendering-Hülle herausgelöst wurde, lassen sich die Regeln ohne GPU mit Unit-Tests prüfen. Es ist dieselbe Headless-Disziplin, die bereits die Engine in Spike 58 erhalten hat – und genau das ist der Unterschied zwischen einem Fortbewegungsgefühl, das man durch Raten abstimmt, und einem Fortbewegungsgefühl, das man zuverlässig festschreiben kann.

In diesem Kapitel verwendete Technologie

Eine Physik-Engine ohne Fortbewegungslogik. Die Kapsel-Engine integriert einen kinematischen Körper gegen das Terrain und enthält keine Logik für Gehen, Laufen, Springen, Reibung, Geschwindigkeitsbegrenzung oder Schwerkraft. Jedes Verhalten ist ein registrierter Controller, der tick, wantsControl, applyForces und optional onEnter/onExit/stateName bereitstellt. Gehen ist lediglich der Standard mit der niedrigsten Priorität, der immer Ja sagt. Ein Controller kann ownsCollision: true zurückgeben, um die Terrainbehandlung zu übernehmen. Schwimmen tut das, damit seine Auftriebsfeder nicht gegen das Einrasten der Füße am Boden arbeitet.

Unabhängige Prioritätskanäle. Controller werden in benannten Kanälen – Ressourcen, Haltung, Fortbewegung – registriert, die getrennt priorisiert und in einer festen Reihenfolge angewendet werden. So können Beobachter wie die Ausdauerverwaltung und Modifikatoren wie das Ducken neben der aktiven Fortbewegung existieren, statt Kontrolle vorzutäuschen und sie anschließend abzulehnen. Änderungen an Ressourcen wie der Ausdauer werden von Haltung und Fortbewegung gelesen. Änderungen an der Haltung wie die Kapselhöhe beim Ducken werden von der Geschwindigkeitsbegrenzung der Fortbewegung gelesen. Die Fortbewegung läuft zuletzt und besitzt den Schreibzugriff auf die Geschwindigkeit. Eine Factory, viele Körper. createCapsuleEngine ist eine reine Factory ohne Singletons. Dadurch steuert dieselbe Engine den Spieler, ein reitbares Pferd, das nur gehen kann, und einen selbstständig navigierenden NPC, der synthetische Eingaben erhält. Das Bewegungsrepertoire eines Körpers entspricht exakt den registrierten Controllern. Das Pferd kann daher weder klettern noch schwimmen, weil diese Controller nie hinzugefügt wurden. Das Auf- und Absitzen wird auf Host-Ebene statt in einem Controller umgesetzt, da dabei zwei Engines koordiniert werden, die nicht aufeinander zugreifen können. Siehe GPU-gesteuertes LOD.

Headless und deterministisch testen. Die Engine greift weder auf window noch auf document oder Three.js zu. Daher kann ein Node-Testsystem sie Frame für Frame ausführen und Sprung und Landung, Schwellenwerte fürs Schwimmen, das automatische Beenden des Kletterns sowie den Ausdauerverbrauch prüfen. Werden identische Eingaben zweimal wiederholt und dabei identische Ausgaben festgestellt, ist der Determinismus nachgewiesen. Ein Regressionstest fixiert außerdem die Abstiegsbegrenzung, die verhindert, dass eine horizontale Bewegung gegen eine steile Fläche den Spieler rückwärts rutschen lässt.

Körperunabhängige Avatar-Anbindung mit testbarer Clip-Auswahl. Controller melden den Namen eines Zustands als String, den die visuelle Ebene einem Clip zuordnet. Dadurch lässt sich eine Kapsel durch einen retargeteten 3MIKE- und UAL-Avatar ersetzen, ohne den Fortbewegungscode zu verändern. Die Clips von Synty POLYGON verwenden dieselben Knochennamen wie das Rig und lassen sich ohne Retargeting abspielen, da Three.js Tracks anhand der Knochennamen bindet. Anders als beim ressourcenintensiven Retargeting des UAL-Pfads wird die Clip-Auswahl in reine Funktionen mit benannten Konstanten ausgelagert: für Geschwindigkeitsbereiche beim Springen, die Stärke der Landung, Varianten der Hangprojektion, einen stets vorwärts gerichteten Idle-Übergang, eine Mindestzeit-Schutzbedingung für den Stopp-Übergang und die Entprellung des Fallzustands. So lässt sich das Bewegungsgefühl mit Unit-Tests prüfen, statt es nur nach Gefühl abzustimmen.


Teil 29 von 30. Zurück: Teil 28 – Gras bis zum Horizont und ein Boden, der sich selbst verbirgt Weiter: Teil 30 – Eine Kamera, die Wände respektiert Serienleitfaden: /de/blog/2026-02-25-open-world-browser-series-guide