Die Entstehung von CrazyTaxi: Segas Dreamcast-Klassiker, neu gebaut in Three.js
CrazyTaxi ist ein Nachbau von Segas Dreamcast-Spiel Crazy Taxi in Three.js. Das Auto, der Verkehr, die Kamera, die Fahrgäste und die Menüs führen den eigenen SH-4-Code des Spiels in einem kleinen Interpreter aus, Bit für Bit identisch mit der Decompilation. Agent! hat es in drei Tagen und 157 Commits gebaut. Spielbar unter ct.gokart.games.
Jetzt spielen: ct.gokart.games. Nötig ist ein Browser mit WebGL, gespielt wird mit Tastatur oder einem Xbox-Controller (oder einem anderen Standard-Controller).
CrazyTaxi baut Segas Dreamcast-Spiel Crazy Taxi (USA, MK-51035) in Three.js nach. Der erste Commit ging am 22. Oktober ein. Am Abend des 24. Oktober waren es 157 Commits: 93 am ersten Tag, 41 am zweiten und 23 am dritten. Agent! hat alles geschrieben, einen getesteten Commit nach dem anderen, und die Commit-Nachrichten lesen sich wie ein Laborbuch, mit Adressen, Zählungen und Prüfungen in fast jeder einzelnen.
Es ist kein Remake, das nach Screenshots gebaut wurde. Fast alles, was auf dem Bildschirm zu sehen ist, und das meiste davon, wie es sich verhält, stammt von der Disc selbst.
Tag eins: das Spiel von der Disc holen
Der größte Teil des ersten Tages floss in die Extraktion. Im Repository liegen keine Spieldaten. Man rippt seine eigene Disc, und ein einziger Befehl holt alles heraus:
python3 tools/extract_all.py '<Crazy Taxi (USA).7z or .chd>' --output local-assets
Hinter diesem Befehl stehen 17 Konverter in tools/, jeder in denselben drei Schritten eingecheckt: das Werkzeug, seine Tests und dann ein Dokument über das, was gefunden wurde. Der Reihe nach kümmern sie sich um:
- Die Disc: extrahiert und per Hash verifiziert aus dem rohen High-Density-Track.
extract_chd.pyliest CHD-v5-Images mit eigenen Decodern, einschließlich eines FLAC-Decoders, und rekonstruiert die Fehlerkorrektur der CD-Sektoren. Die Ausgabe stimmt Byte für Byte mit dem.7z-Rip überein. - Texturen und Sprites: die TEXDC-Texturbänke sowie die Sprite-Bänke, deren Paletten aus dem SH-4-Ladecode der ausführbaren Datei rekonstruiert wurden.
- Modelle und Städte: POLDC-Modelle und die BINC-Stadtarchive, umgewandelt in texturiertes glTF.
- Figuren und Bewegungen: zusammengesetzt, indem die eigene Zeichenroutine des Spiels nachvollzogen wurde, einschließlich der Keyframe-Kurven-Bewegungen und der Zwei-Knochen-IK, die sie brauchen.
- Verkehr und Taxis: gebaut aus der USA-Autotabelle und den Taxitabellen, wobei jede Verkehrsfarbe eine glTF-Materialvariante ist.
- Kollision, Streckenlinien, Set-Objekte, Fahrgäste, Haltestellen, Zielbilder und Replays, jeweils gegen die eigenen Tabellen der ausführbaren Datei geprüft.
- Audio: ADX-Musik und -Sprache sowie die DTPK-Soundbänke, deren Wiedergaberaten und Instrumentprogramme so gelesen werden, wie der Soundtreiber der Dreamcast sie liest.
Ein kompletter Durchlauf schreibt 4,744 Dateien (255 MB) in etwa zwei Minuten.
Der eigene Code des Spiels läuft
Das three.js-Spiel startete noch am selben Abend mit der Stadt, einem Platzhalter-Taxi und einer von Hand abgestimmten Verfolgerkamera.
Keine Stunde später warf Commit 02bf19e die Platzhalter-Physik über Bord. src/sh4.js ist ein kleiner Interpreter für den SH-4, die CPU der Dreamcast, und src/dc-car.js nutzt ihn, um init_CarMain und exec_CarMain direkt aus der 1ST_READ.BIN des Spiels auszuführen. Sie laufen über die Streckenpunkte und Kollisionsdaten der Disc, geladen an ihren ursprünglichen Dreamcast-Adressen. Zeichnen, Sound und Vibration sind durch Stubs ersetzt oder werden abgefangen, und three.js zeichnet, was immer das Spiel angefordert hat.
Der Rest des Spiels wanderte System für System in den Interpreter:
- Verkehr: Jedes Auto, jeder Zug, jede Cable Car und jede Ampel ist ein Spielereignis, gesteuert von den eigenen
trafficControlundnlExecuteEventdes Spiels. Das Taxi rammt den Verkehr, und der Verkehr rammt zurück. - Die Kamera: die Verfolgerkamera von
camUpdateund der Schwenk um das Taxi beim Countdown. Three.js übernimmt einfach die View-Matrix des Spiels. - Fahrgäste: wer wo wartet, das Einsteigen, die Fahrt sowie die Bewertung und der Zeitbonus beim Absetzen.
- Menüs: der Titelbildschirm, das Hauptmenü, die Optionen, die Rekorde und die Fahrerauswahl. Sie alle sind Spielcode, der Sprites zeichnet, die
src/menu-view.jsauf ein Canvas bringt.
Jedes davon wird gegen den eigenen SH-4-Simulator der Decompilation geprüft. tools/car_trace.py zeichnet eine ganze Fahrt auf: den Countdown, das Einsteigen, das Fahren, das Absetzen. Jeder Frame des Taxis, der Kamera, jedes Verkehrsautos, der Spieluhr und jedes Fahrgasts stimmt in beiden Städten Bit für Bit überein.
Diese Prüfung hat einen echten Fehler aufgedeckt. Zwei verschiedene Teile der Spielmathematik stimmten nicht überein, bis die 64-Bit-Float-Moves des Interpreters so korrigiert waren, dass sie Registerpaare in der Reihenfolge speichern, die der SH-4 tatsächlich verwendet. Der Simulator der Decompilation erhält denselben Patch in car_trace.py.
Crazy Box
Am zweiten Tag kam Crazy Box hinzu: die Minispiele des Spiels, alle 16 gesteuert von Init_DCmini und execDCmini. Jedes neue Stück des Spiels wurde auf dieselbe Weise gefunden. Die Routine in der ausführbaren Datei finden, sie im Interpreter ausführen und abfangen, was sie zeichnet:
- Die Pins von Crazy Bowling stammen aus
ExecSetObject. Sie fliegen, wenn man sie trifft, und das Spiel zählt sie. - Der grüne Richtungspfeil stammt aus
ExecArrow, gezeichnet im View-Space und zurück in die Welt gesetzt. - Das Fenster TRY AGAIN ist das eigene
windowTryAgaindes Spiels, samt Menücursor. - Geschaffte Aufgaben werden vom eigenen
CheckRankingdes Spiels gespeichert und schalten die nächsten Boxen frei. Der Spielstand sind dieselben 0x25B0 Bytes, die die Dreamcast sichert, abgelegt inlocalStorage.
Eine Entdeckung aus dieser Arbeit: Die Routine namens carRumble ist nicht nur für die Vibration zuständig. Sie führt den Crazy Dash aus. Mit einem Stub an ihrer Stelle kam Crazy Jump auf höchstens 132 m. Läuft sie mit, erreicht ein Dash beim GO 173 m.
60 fps und hochauflösende Texturen
Als es korrekt war, musste es schnell werden. Die Stadt kam als 13,779 einzelne Meshes an, und der Browser setzte 3,000 bis 4,500 Draw Calls pro Frame ab. Auf einem Auto-Pilot-Branch fasst src/static-batch.js die statische Welt zusammen. Alle 374 Stadttexturen landen in einem einzigen Atlas, und die Meshes werden pro Zelle von 4,000 Einheiten zusammengeführt. Das senkt die Zahl auf etwa 300 Draw Calls pro Frame. In der Mac-App bei 2560 × 1920 sind das konstante 60 fps (Minimum 58), vorher waren es 37 bis 47.
Mit H (oder Y am Controller) schaltet man das hochauflösende Texturpaket ein. tools/match_texture_pack.py berechnet den Custom-Texture-Hash von Flycast für jede Textur des Spiels neu, wodurch ein Flycast-Texturpaket auf 1,793 der 2,090 Texturen des Spiels abgebildet wird. Mit aktiviertem Paket zeichnet die zusammengefasste Stadt aus viermal so großen Atlanten (8192 × 8192). Das Release liefert diese Texturen als KTX2 aus: 51 MB gegenüber den 551 MB JPEG und PNG des Pakets.
Der restliche Feinschliff am dritten Tag:
- TAA: gejitterte Frames, die in eine reprojizierte History eingeblendet werden (
src/taa.js), zusätzlich zu 4× MSAA, damit die ferne Stadt nicht mehr flimmert. - Kein Flackern mehr: Überlappende koplanare Details, etwa die Gebäudeschatten auf dem Gehweg, werden zum Auge hin gezogen, damit sie nicht mehr um die Tiefe kämpfen.
- Kleinere Downloads: Musik und Stimmen als Opus und AAC (von etwa 150 MB auf 37 MB) und Brotli für alles andere (von 103 MB auf 28 MB bei der Übertragung).
- Neue Extras: Unterstützung für Xbox-Controller, eine Minikarte der Stadt mit der kürzesten Straßenroute zum Ziel in Grün und ein Zeitbonus beim Absetzen in den Arcade-Regeln.
- Desktop-Apps: eine signierte und notarisierte Mac-App sowie Launcher für Windows und Linux, die alle den Web-Build unter ct.gokart.games spielen.
Wie es geprüft wurde
Nichts davon wurde einfach geglaubt. Die Trace-Fixtures aus der Decompilation prüfen die Spiellogik Frame für Frame. Headless Chrome spielt nach jeder Änderung eine Fahrt in beiden Städten und zählt die Fehler. tools/draw-order-check.mjs fährt das Taxi per Autopilot und vergleicht jeden Frame mit einer tiefenkorrekten Referenz: Vor der Korrektur der Zeichenreihenfolge waren 66 von 95 Stichproben falsch, danach 0 von 565. tools/mac-probe.sh steuert die Mac-App unbeaufsichtigt und gibt jede Sekunde fps und Draw Calls aus.
Ausprobieren
- Spielen: ct.gokart.games
- Quellcode: AgentiLoop/CrazyTaxi. Um es lokal auszuführen, braucht man eine eigene Disc von Crazy Taxi (USA), Node.js und Python 3.
Halte neben einem Fahrgast, innerhalb seines Kreises, um ihn einsteigen zu lassen. Folge dann dem Pfeil zu dem Ort auf dem Bild und halte dort an.
CrazyTaxi ist ein unabhängiges Fanprojekt und steht in keiner Verbindung zu Sega und wird nicht von Sega unterstützt. Crazy Taxi ist eine Marke von Sega. Im Repository liegen keine Spieldaten.