Eine Engine, dreizehn Spiele: die gemeinsame Schicht
Eine Plattform verdient den Namen, wenn das dreizehnte Stück billiger zu bauen ist als das dritte. Das hier ist die Browser-Spiel-Engine unter unserem Lab — was jedes Spiel teilt, was keines teilt, und die Handvoll kleiner Verträge, die dreizehn sehr verschiedene Spielzeuge davon abhalten, sich gegenseitig ins Gehege zu kommen.
Auf dieser Seite
Im Lab laufen dreizehn Spiele — acht davon in Echtzeit-3D mit WebGL, fünf als Canvas-2D-Originale. Die Zahl ist nicht das Interessante. Interessant ist, was das nächste kostet, und diese Frage entscheidet sich fast vollständig daran, welche Teile wir zu teilen vereinbart haben. Wir teilen das Langweilige: wie laut die Dinge sind, wie viele Pixel von der GPU verlangt werden, was passiert, wenn sich ein Telefon dreht, was passiert, wenn der Browser den Grafikkontext wegwirft. Vom Spaßigen teilen wir nichts. Es gibt keine gemeinsame Spielschleife, keine gemeinsame Physik, kein Entity-System, keine Szenengraph-Abstraktion zwischen einem Spiel und three.js.
- 13
- Spiele im Lab
- 8
- Echtzeit-3D in WebGL
- 120 Hz
- Sims mit festem Zeitschritt
- 1
- Mute-Taste für alle zusammen
Das Lab am 27. Juli 2026. Jedes Spiel ist kostenlos, im Browser, ohne Installation und ohne Konto.
Acht in WebGL, fünf auf Canvas
Eine einzige Registry beschreibt jedes Spiel: Slug, Titel, Tagline, Akzentfarbton, unterstützte Geräte, Status, Highlights, ob 3D oder 2D, und ein kurzes Engine-Label in Monospace, das die Maschinerie benennt („chunked voxel terrain“, in der Art). Das Raster auf /games liest diese Registry und teilt sich in zwei Hälften — eine Leiste Echtzeit-3D für die React-Three-Fiber-Spiele und darunter ein Raster für die Originale, die Canvas-2D-Spiele, die überhaupt keine GPU brauchen.
Die Regel, die diese Teilung erst wertvoll macht, ist ein Einzeiler: Jede Spielkomponente und jede Attract-Mode-Komponente ist auf Modulebene in React.lazy gewickelt. three.js wird dadurch pro Spiel aufgeteilt. Wer ein Canvas-2D-Original öffnet und eine Stunde spielt, lädt nie einen Renderer herunter, und wer den Spieleindex öffnet, lädt keinen von beiden — er bekommt Poster. Fünf Spiele auf schlichtem Canvas zu halten ist keine Nostalgie. Manche Ideen sind als Pixel besser, und ein Spiel, dessen Laden nichts kostet, ist ein Spiel, das Leute auch zu Ende spielen.
WebGL-Qualitätsstufen, einmal geraten
Jedes 3D-Spiel stellt beim Mounten dieselbe Frage und danach nie wieder: Auf wie viel Maschine laufe ich hier? Die Antwort ist eine Stufe — low, medium, high —, berechnet aus der Erkennung grober Zeiger, navigator.hardwareConcurrency, navigator.deviceMemory und der kleineren Bildschirmkante. Ein Gerät gilt als Telefon, wenn der Zeiger grob ist und die kurze Seite unter 700 CSS-Pixeln liegt. Die folgenreichste Ausgabe der Stufe ist ein harter Deckel auf das Device Pixel Ratio: 1,25, 1,5 oder 2,0. Auf einem aktuellen Telefon ist der Unterschied zwischen einem Deckel von 1,25 und ungedeckelten 3,0 etwa das Sechsfache an schattierten Fragmenten — also der Unterschied zwischen einem Spiel und einem Heizlüfter.
Dieses Safari-Detail gehört klar ausgesprochen und nicht als Bug abgeheftet. Safari legt deviceMemory nicht offen, ein iPhone kann sich also nie die Leiter hinaufreden; es ist standardmäßig low. Mobile Safari hält außerdem von allen Browsern, gegen die wir ausliefern, die wenigsten lebenden WebGL-Kontexte und holt sich Hintergrund-Tabs aggressiv zurück — die konservative Antwort ist hier also die richtige. Wenn eine Heuristik in eine Richtung falsch liegt, die einem hilft, lässt man sie in Ruhe.
Zwei kleinere Entscheidungen im selben Hook brauchten länger als die Schwellwerte. Serverseitiges Rendern liefert medium zurück — der Server kann nichts messen, also rät er die Mitte und lässt den Client beim Mounten korrigieren. Und die Reduced-Motion-Einstellung eines Nutzers senkt die Stufe nicht. Es ist verlockend, beides in einen Topf zu werfen, aber es sind verschiedene Bitten: Hör auf, Dinge zu bewegen ist nicht Gib mir weniger Pixel, und eine Bitte um Lesbarkeit mit schlechterer Bildqualität zu beantworten wäre eine merkwürdige Art des Zuhörens. Reduzierte Bewegung wird dort geehrt, wo sie hingehört: in der Animation selbst.
Eine Mute-Taste für das ganze Lab
Im Lab gibt es nirgends Audiodateien, weil die Spezifikation Binär-Assets verbietet. Jeder Ton in jedem Spiel kommt aus einem einzigen Synth-Modul mit einer Oberfläche aus exakt zwei Funktionen: beep() und noiseBurst(). Zusammen decken sie ein Docking-Triebwerk, einen landenden Bauern, einen feuernden Turm und ein abgeschlossenes Level ab, was weniger über unser Sounddesign aussagt als darüber, wie viel von Spiel-Audio schlicht Hüllkurvenformung ist.
Der AudioContext dahinter ist ein fauler Singleton, erzeugt beim ersten Gebrauch statt beim Import, denn ein Kontext, der vor einer Nutzergeste entsteht, kommt suspendiert zur Welt und bleibt es. Töne, die angefordert werden, während der Kontext hochfährt, werden eingereiht statt verworfen, damit der Klick, der das Audio entsperrt, selbst hörbar ist. Verschwindet der Tab, wird der Kontext suspendiert.
Ein localStorage-Schlüssel treibt einen Master-Gain-Node. Schalten Sie eine Docking-Sim stumm, wird auch das Schachbrett still.
Dieser Schlüssel — vaynerov:engine3d:muted:v1 — ist mit Absicht global und nicht pro Spiel. Heute importieren fünfzehn Module den Synth, darunter eine Seite, die gar kein Spiel ist, und sie alle hängen an einem einzigen Master-Gain. Die Alternative, ein Mute-Schalter pro Spiel, klingt konfigurierbarer und ist schlechter: Niemand will dreizehn Häkchen jagen, weil gerade ein Meeting angefangen hat. Stummschalten ist eine Notfallaktion, und Notfallaktionen sollten total sein.
Der Rest der gemeinsamen Schicht ist ebenso bewusst klein:
- Touch-Sticks mit 18 % Totzone — genug, damit Daumenzittern nicht lenkt, klein genug, dass Feinsteuerung überlebt.
- Wischerkennung ab 24 px Schwelle, wobei ein Mausziehen als Wisch zählt, damit derselbe Handler auf einem Laptop funktioniert.
- Speicher pro Spiel unter
vaynerov:<slug>:v1— Einstellungen und Fortschritt, so benannt, dass kein Spiel den Zustand eines anderen lesen kann. - Reduzierte Bewegung als Hook, der einen Anfangswert einrastet und sich dann abonniert, damit eine mitten in der Sitzung geänderte Einstellung ohne Neuladen greift.
- Behandlung von Kontextverlust, innerhalb des Canvas gemountet, die aus einem verlorenen WebGL-Kontext ein Poster macht statt eines schwarzen Rechtecks.
Der letzte Punkt trägt einen ehrlichen Kommentar im eigenen Quelltext: Der Schutz ist an die Showcase-Bühne verdrahtet, und die sieben verbleibenden Bühnen pro Spiel brauchen ihn noch. Es ist der älteste offene Posten der Schicht. Ein verlorener Kontext auf einer Spielseite ergibt heute eine Error Boundary statt eines eleganten Posters — behebbar, nicht elegant.
Keine gemeinsame Spielschleife
Die mit Abstand nützlichste Entscheidung der ganzen Schicht war, genau das nicht zu schreiben, was jede Engine zuerst schreibt: eine gemeinsame Update-Schleife.
Stattdessen besitzt jedes Spiel seine eigene Schleife, und die mit echter Physik fahren einen festen Zeitschritt — 120 Hz in der Docking-Sim und im Kran-und-Pendel-Stapler, wo ein variabler Schritt den Schwierigkeitsgrad sichtbar verändert. Wichtiger noch: Die Simulation lebt bei mehreren Spielen in einem schlichten Modul mit einem Kommentar obendrauf, der kein three.js, kein React sagt: Der Crossy-Road-artige Kurier, das Taschen-RTS, der Lane-Runner und die Voxel-Insel halten ihre Regeln alle in Dateien, die nichts importieren. Physik, die man aus einer Testdatei aufrufen kann, ist Physik, der man trauen kann; Physik, die sich in einen Render-Callback verheddert hat, ist ein Gerücht.
Am deutlichsten zahlt sich das in Slipstream aus, dem Lane-Runner. Seine Hindernisreihen entstehen aus Mustern, und ein Muster kann schön und trotzdem unmöglich sein — eine Lücke, in die man gemütlich hineinspaziert, wird zur Wand, sobald die Strecke mit 32 Einheiten pro Sekunde läuft. Die Musterdatei bringt deshalb einen Validator mit, der jede Reihe abläuft und beweist, dass eine überlebbare Spur bei Höchstgeschwindigkeit erreichbar bleibt, indem er die Erreichbarkeit über die drei Spuren hinweg vorwärts propagiert. Er läuft in der Entwicklung bei der Modulinitialisierung, ein unfaires Muster überlebt also nicht einmal einen Seitenaufruf, geschweige denn ein Release.
Was uns daran gefällt, ist die Wiederverwendung: Dasselbe Reihenmodell treibt den Autopiloten im Attract-Modus des Games-Showcase. Die Demo, die sich auf der Startseite selbst spielt, steuert genau durch die Erreichbarkeit, die der Validator geprüft hat. Fair für den Validator ist dasselbe wie fair für den Spieler, und die Demo ist der Beweis, der öffentlich läuft.
Ein Canvas hinter allem
Das Games-Showcase blättert durch Attract-Slides — ein paar Sekunden jedes Spiels, das sich selbst spielt. Die naive Umsetzung mountet ein Canvas pro Slide, und auf einem Telefon stirbt sie sofort: Browser deckeln die Zahl lebender WebGL-Kontexte, und auf einem Telefon liegt diese Decke niedrig genug, um sie vor einer vollen Runde der Leiste zu erreichen. Das Showcase betreibt deshalb ein gemeinsames Canvas hinter jedem 3D-Slide, mit genau einer gemounteten Szene zur Zeit.
Szenen in einem laufenden Canvas zu tauschen ist der hässliche Teil, und wir verstecken ihn hinter einem opaken DOM-Schleier mit einer Choreografie aus fünf Konstanten.
| Konstante | ms | Rolle |
|---|---|---|
| VEIL_UP | 240 | Ein opakes DOM-Panel steigt über das Canvas |
| SETTLE | 160 | Ein Takt hinter dem Schleier, während die Szene wechselt |
| CANVAS_FADE | 700 | Das Canvas selbst blendet zwischen den Szenen über |
| VEIL_DOWN | 480 | Das Panel hebt sich über dem neuen Spiel |
| COOL_DOWN | 400 | Weitere Slide-Wechsel werden abgelehnt, bis das abgelaufen ist |
Der Wechsel von 3D zu 3D. Die Abkühlzeit existiert, weil ein Besucher, der schnell auf die Leiste tippt, sonst eine Szene mitten im Aufbau abreißen kann.
Hinter dem Schleier leistet eine Zeile unverhältnismäßig viel Arbeit: Die Standardkamera wird vor jedem Mounten auf Werkseinstellungen zurückgesetzt — 75° Blickfeld, Far Plane 1000. Spiele lassen die Kamera dort stehen, wo ihre Welt sie brauchte. Der Pointer-Lock-Shooter hinterlässt eine Far Plane von 160, was für eine Korridor-Arena richtig und für die Voxel-Insel katastrophal ist, deren Horizont mindestens 260 braucht. Ohne den Reset entscheidet die Slide-Reihenfolge darüber, ob eine Welt rendert oder abrupt im Nebel endet. Geteilter veränderlicher Zustand bestraft einen irgendwann; ein Werksreset ist die billigste Entschuldigung.
Das Canvas läuft nur dann mit frameloop="always", wenn es wirklich im Bild ist und ein 3D-Slide aktiv ist; sonst rendert es nichts. Und wenn der Kontext trotzdem verloren geht, fällt die Bühne auf ein DOM-Poster zurück.
Diese Poster verdienen einen Absatz. Alle dreizehn Spiele haben eines, und jedes Poster ist reines DOM und SVG — kein WebGL, kein 2D-Canvas, kein einziges Pixel Rastergrafik. Diese Beschränkung macht sie sicher genug, um aus Server-Komponenten gerendert zu werden, und sie gibt dem ganzen Lab eine Rückfallkette für drei verschiedene Fehler: serverseitiges Rendern, einen Browser ohne WebGL und einen faul geladenen Chunk, der noch nicht angekommen ist. Dieselbe Zeichnung deckt alle drei ab. Die Canvas-2D-Originale teilen sich untereinander ein einziges Postermodul, weil ihre Kartengrafik eine Familie ist.
Kleine Verträge, dreizehnfach
Der Rest der Plattform ist eine kurze Liste von Abmachungen, von denen jede existiert, weil einmal etwas schiefging.
Die Geräteklasse rastet ein. Ein Spiel fragt einmal, beim Mounten, ob das Gerät unterstützt wird, und die Antwort gilt für die ganze Sitzung. Der Kommentar im Hook erklärt es besser, als wir es könnten: Ein Fenster, das mitten im Spiel die Größe ändert, darf nicht die Klasse kippen und ein laufendes Spiel gegen den Sperrbildschirm tauschen. Die Erkennung liest zuerst den User-Agent und danach die Viewport-Größe und gibt beim allerersten Render null zurück, damit Server und Client sich einig sind, bevor irgendetwas festgelegt wird. Genau ein Spiel von dreizehn ist Desktop-only — der Pointer-Lock-Shooter, der eine Maus zum Einfangen braucht —, und ein nicht unterstütztes Gerät bekommt einen ordentlichen Sperrbildschirm statt einer kaputten Erfahrung.
Eine Regel haben wir bewusst nicht aufgestellt: Telefone werden nicht plattformweit auf Touch-Steuerung gezwungen. Jedes Spiel rastet den Zeigertyp beim Mounten ein und entscheidet selbst, welche Sticks und welche Steuerungshinweise es rendert. Ein Tablet mit angeschlossener Tastatur gibt es wirklich, und eine plattformweite Regel hätte darüber in beide Richtungen falsch geurteilt.
Vollbild ist ein Vertrag, kein API-Aufruf. Spiele fassen die Fullscreen-API nie selbst an; sie lesen einen Kontext. Wo die native API existiert, benutzen wir sie. Auf iPhone-Safari, das sie für beliebige Elemente nicht anbietet, fallen wir auf ein CSS-Pseudo-Vollbild zurück: eine Hülle, an den Viewport geheftet, auf hoher Stapelebene, mit gesperrtem Seiten-Scroll, damit das Gummiband das Brett nicht mitten im Zug wegzieht. Beide Wege melden denselben Boolean über denselben Hook, und ein Spiel, das Escape belegt, muss im Vollbild früh zurückkehren, weil dort der Host diese Taste besitzt.
Tastaturbelegungen trennen nach Absicht. Bewegungstasten greifen über e.code, die physische Taste, damit WASD auf AZERTY eine Raute unter der linken Hand bleibt. Merkbuchstaben — die, bei denen der Buchstabe der Punkt ist — greifen über e.key. Die Seite erscheint auch auf Russisch, und wer auf kyrillischem Layout spielt, sollte weiterhin die Taste mit dem richtigen Buchstaben drücken können. Fünfzehn Dateien im Lab folgen inzwischen dieser Konvention.
Jedes Spiel bekommt seine eigene Error Boundary, per Slug neu gekeyt, damit ein Absturz in einem Spiel nicht in das nächste durchsickert, das ein Besucher öffnet.
Was die Spezifikation verbietet
Die Master-Spezifikation für das 3D-Lab ist überwiegend eine Liste von Verboten, und das ist die nützliche Hälfte jeder Spezifikation. Keine neuen npm-Abhängigkeiten: Das gesamte Lab ist three.js 0.184 mit React Three Fiber 9.6 und drei 10.7, sonst nichts. Keine Binär-Assets: Jede Textur ist prozedural, jeder Ton kommt aus dem Synth. Jede Schleife, jeder Listener, jeder AudioContext und jeder Pointer-Lock muss beim Unmount aufräumen. Alles pausiert bei visibilitychange und nach zwei Sekunden ohne Fokus. Reduzierte Bewegung wird überall geehrt.
Im Kopf der Spezifikation stehen noch immer neun Spiele. Wir haben sie im Juni geschrieben, drei weitere an einem einzigen Tag veröffentlicht und die Zahl nie angepasst — das gewöhnliche Schicksal jedes Dokuments, das nicht ausführbar ist. Die Beschränkungen hielten trotzdem, weil sie durch Review und Build durchgesetzt werden und nicht durch den Satz ganz oben.
Hier die Ökonomie, denn das war ja die Behauptung. Das dreizehnte Spiel kam als eigenes Verzeichnis mit rund fünfzig Dateien, plus Änderungen an vier gemeinsamen Dingen: der Registry, der Posterkarte, den sechs Locale-Katalogen und der maschinenlesbaren Seitenzusammenfassung. Keine neue Abhängigkeit. Keine Änderung an den Qualitätsstufen, am Synth, am Fullscreen-Vertrag oder am Showcase. Es hat eine Mute-Taste, eine Geräte-Einrastung, eine Poster-Rückfallkette und eine Tastaturkonvention geerbt, über die es nie nachdenken musste, und jede Zeile eigener Komplexität in den einen Teil gesteckt, den vorher niemand gebaut hatte.
Das ist die gesamte Rendite einer gemeinsamen Schicht: nicht, dass sie Spiele macht, sondern dass sie die Teile eines Spiels macht, die niemand zweimal schreiben möchte. Die sieben Kontextverlust-Schutzmechanismen pro Spiel stehen weiterhin auf der Liste, und sie werden dort stehen, bis ein Browser jemandem den Kontext wegnimmt, der gerade gewonnen hätte.