Darkwood Blog Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators
de
  • en
  • fr
Anmeldung
  • Blog
  • Artikel
  • Beobachtung
  • Releases
  • Creators

đŸ‘Ÿ Ich habe Quake III Arena nach PHP portiert – von PK3-Dateien zu einer spielbaren Karte

vom 14. August 2026

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

Quake III Arena ist kein Spielzeug-Code. Es ist ein vollwertiger Arena-Shooter mit einem ZIP-basierten Dateisystem, einem binĂ€ren Kartenformat, einer Shader-Sprache, die kein GLSL-Programm ist, Kollisionserkennung, die nicht mit dem gezeichneten Mesh ĂŒbereinstimmt, und einem Bewegungscode, der sich auch 25 Jahre spĂ€ter noch unverwechselbar anfĂŒhlt. Einen Teil dieses Stacks nach PHP zu portieren, klingt nach einer Herausforderung. Und das war es zum Teil auch. Es war aber auch eine bewusste Methode, eine Engine durch die Nachimplementierung ihrer Kernstrukturen kennenzulernen, anstatt nur ein Wiki zu ĂŒberfliegen.

Dieser Artikel handelt von diesem Experiment: einer progressiven PHP-Portierung, die ein natives Fenster starten, Daten der Vollversion oder Demoversion von Quake III laden, eine IBSP-Karte parsen, Zeichenblöcke erstellen, Spielerbewegungen im Quake-Stil mit BSP-Kollisionserkennung ausfĂŒhren, ein kleines Waffenset abfeuern, TĂŒren öffnen und maps/q3dm0.bsp so flĂŒssig gestalten kann, dass man darin laufen, kĂ€mpfen und sterben kann. Es wird nicht behauptet, dass Quake III komplett in PHP neu geschrieben wurde. Es gibt weder QVM noch Multiplayer, keine Bot-KI, kein MenĂŒsystem und die Weltmaterialien sind noch unvollstĂ€ndig. Was existiert, ist ein spielbarer Teil, dessen Engine-Logik in PHP implementiert ist und dessen Fenster hinter FFI lĂ€uft.

Wenn Sie nur den Befehl „AusfĂŒhren“ benötigen, springen Sie direkt zu So fĂŒhren Sie ihn aus. Alles andere beschreibt den Ablauf, der diesen Befehl sinnvoll macht.

Warum sollte man Quake III nach PHP portieren?

PHP verbringt den Großteil seiner Laufzeit mit der Beantwortung von HTTP-Anfragen. Diese Art von Arbeit ist kurzlebig: Anfrage parsen, Datenbankabfrage, HTML- oder JSON-Ausgabe, Beendigung. Game-Engines hingegen funktionieren völlig anders. Sie speichern ZustĂ€nde ĂŒber Minuten, arbeiten dutzende Male pro Sekunde, dekodieren BinĂ€rformate und berĂŒcksichtigen den Unterschied zwischen einem Render-Mesh und einem Clipping-Pinsel.

Ich wusste bereits, dass PHP mehr kann als Webseiten. Darkwood arbeitet unter anderem mit umfangreichen Offline-Tools und FFI-basierten nativen Fenstern. Quake III war eine grĂ¶ĂŸere Herausforderung: Es ging nicht nur darum, „ein paar Dreiecke zu zeichnen“, sondern darum, „die id Tech 3-Pipeline so weit zu respektieren, dass die gewohnte Steuerung des Originalspiels erhalten bleibt“.

Die Ziele waren konkret:

  • Behandeln Sie den GPL-Quellcode der Engine als Spezifikation, nicht als Inspirationsquelle.
  • Speichere die Spieldaten nicht im Repository, sondern lade sie so, wie es Quake macht: ĂŒber PK3-Archive.
  • Lieber detailgetreue Darstellung der Details, die das SpielgefĂŒhl von Quake ausmachen (Dateisystemreihenfolge, BSP-Lumps, pmove-Nummern, Spawn-Regeln), als eine aufwendig gestaltete, aber erfundene Architektur. Akzeptiere, dass PHP langsamer als C sein wird, und frage dich trotzdem, ob es korrekt genug sein kann, um zu spielen

Ich wollte nicht beweisen, dass PHP C bei der Entwicklung von Shootern ersetzen sollte. Ich wollte lediglich herausfinden, wie weit eine moderne PHP-Laufzeitumgebung gehen kann, wenn man aufhört, jedes Programm als Anfragebearbeiter zu betrachten.

Was id Software tatsÀchlich veröffentlicht hat

2005 veröffentlichte id Software den Quellcode der Quake III Arena-Engine unter der GPL. Diese Veröffentlichung bildet die rechtliche und technische Grundlage dieser Portierung. Sie umfasst den C-Code fĂŒr Client, Server, Renderer, Kollisionserkennung und Spielmodule – die gesamte Spielmechanik.

Es enthĂ€lt keine kommerziellen Spielressourcen. Die Karten, Texturen, Sounds, Modelle und PK3-Pakete, die Quake III sein Aussehen und seinen Klang verleihen, bleiben urheberrechtlich geschĂŒtzt. FĂŒr die Entwicklung wurden lokal gespeicherte oder per CLI-Parameter referenzierte Demodaten verwendet. Diese Dateien gehören niemals in den Git-Baum.

Diese Unterscheidung ist in jedem Satz dieses Artikels wichtig. Wenn ich sage „der Port lĂ€dt pak0.pk3“, meine ich Ihre legale baseq3-Datei, nicht ein weiterverbreitetes Spiel. Wenn ich sage „basierend auf Quake III Arena“, meine ich Algorithmen und Datenstrukturen aus den GPL-Quellen, neu implementiert in PHP.

Spieldaten in PHP einlesen

Quake III liefert kein loses Verzeichnis mit TGA-Dateien als primĂ€res Inhaltsmodell aus. Die Inhalte befinden sich in PK3-Dateien: ZIP-Archiven mit einer Quake-Pfadkonvention. Ein typisches baseq3-Verzeichnis enthĂ€lt die Datei pak0.pk3 bis hin zu spĂ€teren Point-Release-Paketen. SpĂ€tere Pakete ĂŒberschreiben frĂŒhere, wenn sie denselben internen Pfad enthalten.

Die PHP-Portierung spiegelt dieses Konzept in src/Filesystem/FileSystem.php wider. Sie ĂŒbergeben --basepath, das auf ein Installationsverzeichnis (mit einem baseq3-Unterverzeichnis), auf baseq3 selbst oder auf ein Verzeichnis mit PK3-Dateien verweist. Der Loader erkennt *.pk3-Dateien, sortiert die Namen analog zu Quakes paksort und fĂŒgt jedes Archiv voran, sodass alphabetisch spĂ€tere Pakete Vorrang haben. Anschließend durchlĂ€uft eine Suche diese Liste und stoppt beim ersten Treffer (Semantik von FS_FOpenFileRead).

// FileSystem::addGameDirectory + resolve — src/Filesystem/FileSystem.php
usort($names, static fn (string $a, string $b): int => Path::compare($a, $b));
foreach ($names as $name) {
    $loaded[] = $this->addPak($real . DIRECTORY_SEPARATOR . $name); // prepend
}

public function resolve(string $qpath): ?ResolvedFile
{
    // ...
    foreach ($this->packs as $pack) {           // head = highest priority
        $entry = $pack->getEntry($qpath);
        if ($entry !== null) {
            return new ResolvedFile($entry, $pack);
        }
    }
    return null;
}

Das ist der Quake-spezifische Aspekt – nicht „eine ZIP-Datei öffnen“, sondern „mehrere Archive als ein virtuelles Dateisystem mit ĂŒberschriebener Reihenfolge darstellen“. Dass pak1.pk3 gegenĂŒber pak0.pk3 fĂŒr denselben qpath Vorrang hat, ist die Grundlage fĂŒr Punkt-Releases und Patches.

Pk3Archive indiziert jede ZIP-Datei einmal. Path::normalize in src/Filesystem/Path.php entfernt fĂŒhrende Trennzeichen, berĂŒcksichtigt Groß- und Kleinschreibung und verwirft ..-Escapezeichen, sodass Maps/Q3DM0.BSP und maps/q3dm0.bsp gleich aufgelöst werden. In diesem Port gibt es keine Suche nach Verzeichnissen mit losem Suchpfad; diese EinschrĂ€nkung ist beabsichtigt.

CLI-Tools wie fs:which, pk3:list und data:inspect existieren, weil Dateisystemfehler wie Renderer-Bugs aussehen. Wenn das falsche Paket verwendet wird, könnte man meinen, der Shader-Parser sei defekt. Wenn bsp:mesh oder der Start von play ausgibt, woher ein BSP stammt, werden der qpath und das verwendete Archiv angezeigt – diese eine Zeile hat mehr Zeit beim Debuggen gespart als jede Profiler-Ausgabe.

IBSP verstehen

Karten sind BSP-Dateien: Binary Space Partitioning-BÀume plus eine Reihe zugehöriger Lumps. Quake III Arena verwendet die IBSP-IdentitÀt und die Version 46 mit 17 Lumps im Header.

BspLoader (src/Bsp/BspLoader.php) wandelt die Datei nicht zuerst in einen komplexen Objektgraphen um. Stattdessen werden die Bytes als Layout behandelt, das dem Quake-Header entsprechen muss: IdentitĂ€t, Version und siebzehn {fileofs, filelen}-Paare. Das ist etwas strenger als bei manchen Ă€lteren C-Pfaden, die sich allein auf die Version verließen – ungĂŒltige Offsets werden verworfen, bevor auf den Puffer zugegriffen wird.

// BspLoader::loadFromString — src/Bsp/BspLoader.php
$head = unpack('Vident/Vversion', substr($bytes, 0, 8));
$ident = $head['ident'];
$version = $head['version'];
if ($ident !== BspIdent::IDENT) { /* expect IBSP */ }
if ($version !== BspIdent::VERSION) { /* expect 46 */ }

$lumps = [];
$offset = 8;
for ($i = 0; $i < BspIdent::HEADER_LUMPS; $i++) {
    $chunk = unpack('Vfileofs/Vfilelen', substr($bytes, $offset, 8));
    // ... bounds-check fileofs/filelen against strlen($bytes) ...
    $lumps[] = new BspLump(
        index: $i,
        name: BspIdent::LUMP_NAMES[$i],
        fileofs: $chunk['fileofs'],
        filelen: $chunk['filelen'],
    );
    $offset += 8;
}

unpack('V
') ist ein Little-Endian-Datentyp mit vorzeichenlosen 32 Bit – dieselbe Breite und Byte-Reihenfolge wie die On-Disk-Integer von Quake auf den relevanten Plattformen. Die idiomatische PHP-Modellierung erfolgt erst nach dieser Genauigkeit. Ein Lesefehler von vier Bytes bei der LĂ€nge eines Datenblocks fĂŒhrt spĂ€ter nicht zu einer korrekten DomĂ€nenausnahme, sondern zu einer fehlerhaften Map.

Die Klumpen sind nicht austauschbar. Der Hafen benötigt fĂŒr verschiedene Aufgaben unterschiedliche Klumpen:

  • Geometrie zeichnen: Shader, Drawverts, Drawindexes, OberflĂ€chen, Lightmaps, Lightgrid, Sichtbarkeit, Nebel (teilweise)
  • Kollision: Ebenen, Knoten, BlĂ€tter, Blattpinsel, Pinsel, Pinselseiten, Shader (fĂŒr Inhaltsflags)
  • Spielelemente: Der EntitĂ€tenblock – ein Textblock aus in geschweiften Klammern getrennten SchlĂŒssel/Wert-WörterbĂŒchern
  • Modelle / Bewegungselemente: Modelle, die Indizes enthalten, auf die func_door und Ă€hnliche Funktionen als *N-BĂŒrstenmodelle verweisen.

Die Geometrie-Rendering-Funktion beantwortet die Frage „Was soll ich zeichnen?“. Die Kollisionserkennung beantwortet die Frage „Wo kann ein Kasten platziert werden?“. Die Objekterkennung beantwortet die Frage „Wo startet der Spieler und welche GegenstĂ€nde gibt es?“. Wenn man diese drei Aspekte verwechselt, erhĂ€lt man klassischerweise eine wunderschöne Karte, durch die man hindurchfĂ€llt.

Von BSP-OberflÀchen zu etwas Zeichnbarem

Sobald die Karte im Speicher ist, durchlĂ€uft WorldBatchBuilder die OberflĂ€chen und erstellt statische Zeichenblöcke, die durch Shader und Lightmap gesteuert werden. Planare FlĂ€chen und Dreiecksstrukturen werden zu indizierten Netzen. BĂ©zier-Patches werden auf der CPU tesselliert. Himmels- und LichtreflexionsflĂ€chen werden als Volumenkörper klassifiziert und oft ĂŒbersprungen; die Bearbeitung erfolgt dann ĂŒber dedizierte Pfade.

Die Überraschung fĂŒr einen typischen PHP-Entwickler liegt darin, wie unscheinbar der Vertexpfad ist. Quakes drawVert_t ist ein fester 44-Byte-Datensatz. BspDrawVert::fromBytes in src/Bsp/BspDrawVert.php zerlegt ihn Feld fĂŒr Feld:

// BspDrawVert::fromBytes — src/Bsp/BspDrawVert.php (44-byte drawVert_t)
return new self(
    xyz: [
        BspBinary::float32($bytes, $offset),
        BspBinary::float32($bytes, $offset + 4),
        BspBinary::float32($bytes, $offset + 8),
    ],
    st: [
        BspBinary::float32($bytes, $offset + 12),
        BspBinary::float32($bytes, $offset + 16),
    ],
    lightmap: [
        BspBinary::float32($bytes, $offset + 20),
        BspBinary::float32($bytes, $offset + 24),
    ],
    normal: [
        BspBinary::float32($bytes, $offset + 28),
        BspBinary::float32($bytes, $offset + 32),
        BspBinary::float32($bytes, $offset + 36),
    ],
    color: [
        ord($bytes[$offset + 40]),
        ord($bytes[$offset + 41]),
        ord($bytes[$offset + 42]),
        ord($bytes[$offset + 43]),
    ],
);

Das ist der ganze Witz: Aus einer BSP-Byte-Zeichenkette werden $xyz, $st, Lightmap-UVs, eine Normale und vier Farbbytes. Anschließend werden die Vertices von OberflĂ€chen in Dreiecke indiziert, und WorldBatchBuilder gruppiert sie fĂŒr OpenGL. Kein ORM. Kein „VertexRepository“. Wieder binĂ€res Layout.

Beim Start von q3dm0 sieht eine typische stderr-Zusammenfassung so aus: Zehntausende Dreiecke verteilt auf etwa hundert Batches, mehrere Lightmap-Seiten, Patch-Kollisionsdreiecke fĂŒr die Clipping-Seite gekrĂŒmmter OberflĂ€chen und Tausende von PVS-Clustern. Diese Zahlen sind kein Benchmark-Wert; sie verdeutlichen lediglich, dass selbst eine „kleine“ Quake-Map ein realer Datensatz ist.

Der Renderer streamt nicht den ursprĂŒnglichen C-Szenengraphen refEntity_t. PHP bereitet Batches und ZustĂ€nde vor; OpenGL (ĂŒber FFI) sendet Fixed-Function-Zeichenbefehle. Dies ist eine pragmatische Abgrenzung: PHP ist fĂŒr die korrekte Interpretation der Assets verantwortlich; die GPU ĂŒbernimmt weiterhin die Rasterberechnung.

AnfĂ€ngliche Fehler waren lehrreich. Die unnötige vertikale Spiegelung der Lightmap-UVs ließ jede beleuchtete OberflĂ€che leicht fehlerhaft aussehen. Die Mittelung der Portalebenennormalen anstelle der Spiegelung der ersten Dreiecksebene fĂŒhrte zu einer falschen Portalausrichtung. Das Speichern eines Shadernamens als „bereits geladen“, bevor verwandte Stage-Maps gebunden wurden, ĂŒbersprang Umgebungs- und Mischtexturen. OberflĂ€chen, die Vertexfarben verwenden sollten, wurden kurzzeitig wie Lightmap-FlĂ€chen behandelt und leuchteten voll auf. Keiner dieser Fehler wirkt fĂŒr sich genommen dramatisch. Zusammengenommen verdeutlichen sie jedoch, warum bei einer Portierung Zwischenwerte und nicht nur die endgĂŒltigen Pixel debuggt werden mĂŒssen.

Die Sichtbarkeit (PVS) wird geladen und dient dazu, die relevanten Batches fĂŒr einen Kameracluster einzuschrĂ€nken. Im Statusprotokoll wird die KohĂ€renz bewertet, was bedeutet, dass sie fĂŒr das Spielen mit q3dm0 ausreichend ist, aber nicht, dass jeder Grenzfall der Cluster-Packung in Quake erneut ĂŒberprĂŒft wurde. Das Deaktivieren von PVS per Flag ist weiterhin sinnvoll, wenn man eher Culling als Materialeffekte vermutet.

Auswahl der Renderinggrenze

Eine naheliegende Frage: Warum nicht den ursprĂŒnglichen C-Code mit Emscripten kompilieren und ihn als PHP-Projekt bezeichnen? Das wĂ€re eine Paketierungsaufgabe, keine Portierung.

Dieses Projekt bewahrt die Logik von Quake in PHP:

  • PK3-Suchreihenfolge
  • BSP-Dekodierung
  • Shader-Skript-Parsing
  • Kollisionsspuren
  • Spielerbewegung
  • EntitĂ€ts-/Spielregeln fĂŒr den unterstĂŒtzten Bereich
  • Render-Orchestrierung (was gezeichnet werden soll, mit welchen Texturen und ÜbergĂ€ngen)

Die Plattform ist schlank: SdlGl öffnet ein Fenster, erstellt einen GL-Kontext, fragt Eingaben ab und tauscht Puffer aus. Gl ist eine umfangreiche, festverdrahtete OpenGL-Hilfsfunktion – fĂŒr Welt, MD3-Objekte, Waffenansicht, HUD und Effekte –, die weiterhin vom PHP-eigenen Zustand gesteuert wird.

Es gibt außerdem einen serve-Befehl, der einen Browser-WebGL-Orbit-Viewer fĂŒr Mesh-Dumps bereitstellt. Dieser ist fĂŒr Diagnosezwecke nĂŒtzlich. Er ist nicht das Spiel. Der spielbare Pfad ist nativ.

PK3 archives
  ↓  FileSystem (override order)
BSP bytes
  ↓  BspLoader / WorldBatchBuilder / CollisionModel
world + collision + entities
  ↓  Application play loop
input → Pmove → MapGame → view
  ↓  Gl (FFI) + SdlGl::swap
native OpenGL window

Oder, als Anrufkette:

php bin/quake3.php play → Application::cmdPlay
  → FileSystem + BspLoader + CollisionModel + WorldBatchBuilder
  → MapGame + Player + Pmove
  → loop: SdlGl input → pmove/game → Gl draw → swap

PHP verwaltet den Spielzustand so lange, wie das Fenster geöffnet ist. Es wird nicht nur eine HTTP-Anfrage bearbeitet und anschließend beendet. Der WebGL-Befehl serve serialisiert ein statisches Mesh fĂŒr die Orbitansicht; er fĂŒhrt diese Schleife nicht aus.

Diese Aufteilung entspricht einem Ă€lteren Darkwood-Muster: PHP als robustes Gehirn, FFI als Hardware-TĂŒr.

Texturen, Shader und Lightmaps

Die „Shader“ von Quake III sind keine modernen GPU-Shader-Programme. Es handelt sich um Materialskripte im Verzeichnis scripts/*.shader: Stufen mit Texturen, Mischmodi, Alpha-Tests, tcMod-Transformationen, Animationstexturen, Umgebungsmapping und SortierschlĂŒsseln. Die GPU von 1999 nutzte eine deutlich einfachere Pipeline; das Skript beschrieb, wie sie mit Daten versorgt wurde.

Die PHP-Portierung analysiert diese Skripte (ShaderParser, Q3Shader, stages) und fĂŒhrt beim Zeichnen eine Teilmenge davon aus: Lichtkartenmodulation, verschiedene Mischmodi, Scroll-/Rotations-/Turb-Effekte, Umgebungsstufen auf bestimmten OberflĂ€chen und MD3s, Himmelsboxen, Portale/Spiegel und Flares. Das Laden von Texturen unterstĂŒtzt die Formate, die Quake in der Praxis fĂŒr diese Daten verwendet (TGA/JPEG-Pfade ĂŒber ImageLoader).

Was nicht garantiert wird: vollstĂ€ndige Shader-KompatibilitĂ€t. Die Bewertung des Live-Status der Shader-AusfĂŒhrung ist unvollstĂ€ndig. Einige Weltmaterialien auf q3dm0 werden selbst nach Auflösung der Bilddateien weiterhin schwarz oder fehlerhaft dargestellt. Dies ist das aktuelle visuelle Problem: keine fehlenden Dateien, sondern unvollstĂ€ndiges oder fehlerhaftes Verhalten der Stufe. Tools wie --debug-material= und --debug-surface-pick ermöglichen es, die Kamera auf eine fehlerhafte OberflĂ€che zu richten und den beteiligten Shader-Namen zu ermitteln, anstatt anhand eines Screenshots raten zu mĂŒssen.

Lightmaps werden als Seiten geladen und nach dem klassischen Quake-Prinzip angewendet: Eine zweite Texturierungsstufe moduliert das diffuse Erscheinungsbild der Welt. Die korrekte lineare Upload- und UV-Ausrichtung war wichtiger als jede ausgeklĂŒgelte PHP-Optimierung.

Spieler-Spawn finden

Die EntitĂ€ten-Liste ist Klartext, der in die BSP eingebettet ist – Quake benötigt keine separate Datenbank, um zu wissen, wo sich Spieler und GegenstĂ€nde befinden. EntityParser::deathmatchSpawns in src/Game/EntityParser.php durchlĂ€uft die geparsten WörterbĂŒcher und extrahiert Deathmatch-Pads (wobei info_player_start wie in g_client.c als Alias ​​behandelt wird):

// EntityParser::deathmatchSpawns — src/Game/EntityParser.php
if ($classname === 'info_player_start') {
    $classname = 'info_player_deathmatch';
}
if ($classname !== 'info_player_deathmatch') {
    continue;
}
$origin = $this->parseVec3($ent['origin'] ?? '0 0 0');
$angles = [0.0, 0.0, 0.0];
if (isset($ent['angles'])) {
    $angles = $this->parseVec3($ent['angles']);
} elseif (isset($ent['angle'])) {
    $angles[1] = (float) $ent['angle']; // F_ANGLEHACK → yaw only
}
$spawns[] = new SpawnPoint($classname, $origin, $angles, $i, (int) ($ent['spawnflags'] ?? 0));

Das liefert lediglich eine Liste. Der erste Spawn in Quake bevorzugt ein Pad mit spawnflags & 1. Auf q3dm0 ist das das Intro-Hall-Pad, nicht Entity-Order Null. Die Portierung spiegelt SelectInitialSpawnPoint / SelectRandomFurthestSpawnPoint in src/Game/SpawnPoint.php wider – dies ist eine getreue Regel fĂŒr den unterstĂŒtzten Slice, keine Vereinfachung durch „pick [0]“.

// SpawnPoint::selectInitial — src/Game/SpawnPoint.php
foreach ($spawns as $s) {
    if (($s->spawnflags & 1) !== 0) {
        return $s;
    }
}
return self::selectRandomFurthest($spawns, [0.0, 0.0, 0.0]);

Die Respawn-Punkte werden nach Entfernung vom Todesort sortiert und zufĂ€llig aus der am weitesten entfernten HĂ€lfte ausgewĂ€hlt. --spawn=N ĂŒberschreibt diese Einstellung weiterhin zu Debugging-Zwecken.

Der Spawn-Ursprung erhÀlt den klassischen +9-Befehl auf Z (playerOrigin()), bevor der Spieler mit einem Box-Trace auf den Boden fÀllt. Vergisst man den Lift, spawnt man im Boden. LÀsst man den Fall aus, spawnt man in der Luft, und die ersten Frames sehen verzerrt aus.

MapGame verknĂŒpft anschließend die restlichen unterstĂŒtzten Objekte fĂŒr diese Karte: GegenstĂ€nde mit MD3-Modellen, einen Teleporter, TĂŒren (einschließlich gekoppelter TĂŒren, die sich gemeinsam bewegen mĂŒssen), Umgebungslautsprecher, Timer, rotierende Kartenobjekte und PortalflĂ€chen. Die Intro-Sprachausgabe wird bewusst ĂŒbersprungen; das Ziel war eine spielbare Arena, kein Spieler fĂŒr Zwischensequenzen.

Aufbau einer echten Spielschleife

bin/quake3.php setzt Speicher- und Zeitlimits, lĂ€dt den Composer-Autoloader und ĂŒbergibt argv an Application. Der Befehl play ist kein Plugin-Framework. Er besteht aus einer expliziten Setup-Phase, gefolgt von einer while ($running)-Schleife.

Das Setup lĂ€dt das BSP, erstellt Kollisionsabfragen und Batches, konstruiert die Spielwelt, erstellt den Spieler, initialisiert SDL/GL, lĂ€dt GPU-Ressourcen hoch und gibt eine kurze Übersicht der gefundenen Objekte aus. Die Schleife lĂ€uft dann wie folgt ab:

  1. Misst ein Delta eines eingespannten Rahmens
  2. Ruft SDL-Ereignisse ab und erstellt einen strukturierten Eingabe-Snapshot.
  3. Erstellt ein Usercmd (vorwĂ€rts/seitlich/oben, Tasten, Winkel, WaffenwĂŒnsche)
  4. FĂŒhrt Pmove gegen das Kollisionsmodell aus (wobei TĂŒrkörper als EntitĂ€tsboxen eingefĂŒgt werden)
  5. Aktualisiert MapGame (AufsammelgegenstÀnde, Bewegungsobjekte, Projektile, Timer)
  6. Steuert das Abfeuern und den Waffenstatus.
  7. Berechnet die Offsets der Ego-Perspektive (Bob, Land, Damage Kick, Duck Lerp)
  8. Zeichnet die Welt, Objekte, Effekte, die Waffenansicht und das HUD.
  9. Tauscht Puffer

Das ist fĂŒr ein Spiel ĂŒblich. Ungewöhnlich fĂŒr PHP ist es nur deshalb, weil in der PHP-Programmierkultur solche Programmstrukturen selten vorkommen. Die Schleife lĂ€uft in Application::cmdPlay (src/Console/Application.php) als langlebiger Prozess – nicht als Anfragezyklus.

// Application::cmdPlay — frame head (src/Console/Application.php)
while ($running) {
    $now = $sdl->ticks();
    $dt = max(0.001, min(0.05, ($now - $last) / 1000.0));
    $last = $now;
    $frame++;
    $gameTime += $dt;

    $ev = $sdl->pollEvents();
    if ($ev['quit'] || $ev['escape']) {
        $running = false;
    }
    // ... zoom FOV, mouse look / +strafe, Usercmd, Pmove, MapGame, draw, swap ...
}

Nichts in diesem Ausschnitt ist fiktive Middleware. Die Delta-Zeit ist begrenzt, damit ein Ruckler den Spieler nicht durch eine Wand teleportiert. Die Eingabe wird in einem Snapshot gespeichert. Der Rest des Frames – das Erstellen eines Usercmd, das AusfĂŒhren von Pmove, das Aktualisieren von Objekten, das Zeichnen – ist die KomplexitĂ€t, die die Schleife aufruft, nicht die Schleife selbst.

Maussteuerung und Tastatureingabe

Die Eingabe erfolgt ĂŒber SdlGl::pollEvents. Tasten sind kurzen Zeichenketten zugeordnet (w, shift, pgdn, 
). Mausbewegungen werden pro Frame akkumuliert. SchaltflĂ€chen steuern Angriff, SeitwĂ€rtsbewegung und Zoom.

Look verwendet das Standard-SteuerungsgefĂŒhl von Quake: m_yaw * sensitivity mit Standardwerten, die 0,022 × 5 Grad pro Mausschritt entsprechen. Die Pfeiltasten drehen sich mit cl_yawspeed 140 (und die Pitch-Tasten mit cl_pitchspeed 140), wobei die Shift-Taste den klassischen cl_anglespeedkey-Multiplikator von 1,5 fĂŒr die „Geschwindigkeit“ anwendet.

Halten Sie die rechte Maustaste gedrĂŒckt oder drĂŒcken Sie Alt fĂŒr +strafe: Die Maus gleitet, anstatt sich zu drehen, und die Pfeiltasten dienen als SeitwĂ€rtsbewegung. Halten Sie die mittlere Maustaste gedrĂŒckt fĂŒr +zoom, wodurch das horizontale Sichtfeld (FOV) innerhalb von 150 ms um cg_zoomfov 22,5 variiert und die Blickempfindlichkeit im Zoom-Modus mit fov_y / 75 skaliert wird – dieselben Beziehungen, die auch in Quakes cgame verwendet werden.

Die Waffentasten folgen der Quake-Waffennummerierung: 2 fĂŒr Maschinengewehr, 3 fĂŒr Schrotflinte, 8 fĂŒr Plasmagewehr. Mit dem Mausrad und den Klammern kann man zwischen den Waffen wechseln, solange noch Munition vorhanden ist. Feuern wird mit der linken Maustaste, Strg oder F ausgelöst. Ducken ist mit C. Gehen ist mit Shift möglich. Da das Fenster nativ ist und den relativen Mausmodus verwendet, ist keine Browser-Zeigerfixierung erforderlich.

Hafenbewegung

Pmove ist eine Teilmenge von Quakes bg_pmove.c / bg_slidemove.c. Es handelt sich nicht um einen Unity CharacterController mit Quake-Ästhetik. Die Konstanten entsprechen denen von Quake: Sprunggeschwindigkeit 270, Reibung 6, Bodenbeschleunigung 10, Luftbeschleunigung 1, Stoppgeschwindigkeit 100, Ducken-Skalierung 0,25, SchrittgrĂ¶ĂŸe 18, minimale Gehnormale 0,7.

Jeder Befehlsrahmen:

  • Erzeugt Reibung beim Aufsetzen
  • Beschleunigt die gewĂŒnschte Geschwindigkeit der VorwĂ€rts-/SeitwĂ€rtsbewegung, skaliert mit der Lauf- oder Gehgeschwindigkeit
  • Integriert die Schwerkraft
  • FĂŒhrt Gleitbewegungen mit Ebenenbeschneidung durch
  • Stufen hinauf
  • Behandelt Sprung-, Duck-, Bruchlande- und Wackelbewegungen fĂŒr Ansicht und Schritte

Die Luftsteuerung ist Quakes schwache Luftbeschleunigung, weshalb sich RaketensprĂŒnge und PlasmastĂ¶ĂŸe wie in Quake anfĂŒhlen, wenn auch der RĂŒckstoß durch Spritzer originalgetreu umgesetzt ist. Der Kern dieses GefĂŒhls liegt nicht in $pos += $vel * $dt. Es ist die PM_Accelerate-förmige Wunschgeschwindigkeitslogik in Pmove::accelerate (src/Game/Pmove.php) – eine getreue Portierung des Quake-Beschleunigungsschritts fĂŒr die implementierten Boden-/Luftpfade (Wasser/Zuschauer bleiben verzögert):

// Pmove::accelerate — mirrors PM_Accelerate (src/Game/Pmove.php)
private function accelerate(array $wishdir, float $wishspeed, float $accel): void
{
    $currentspeed = $this->dot($this->ps->velocity, $wishdir);
    $addspeed = $wishspeed - $currentspeed;
    if ($addspeed <= 0) {
        return;
    }
    $accelspeed = $accel * $this->frametime * $wishspeed;
    if ($accelspeed > $addspeed) {
        $accelspeed = $addspeed;
    }
    $this->ps->velocity[0] += $accelspeed * $wishdir[0];
    $this->ps->velocity[1] += $accelspeed * $wishdir[1];
    $this->ps->velocity[2] += $accelspeed * $wishdir[2];
}

Wunschrichtung und -geschwindigkeit werden durch den Benutzerbefehl und die Lauf-/Geh-Skala bestimmt; die aktuelle Geschwindigkeit ist die Projektion der Geschwindigkeit auf diesen Wunsch; die Beschleunigung ist begrenzt, damit man den Wunsch nicht in einem Frame ĂŒberschießt. Ändert man die Luftbeschleunigungskonstante von 1 auf einen „angenehmeren“ Wert, fĂŒhlt sich das Spiel nicht mehr wie Quake an, selbst wenn die Reibung weiterhin funktioniert.

Tote Körper behalten ihre Geschwindigkeit und nutzen die ReibungskrĂ€fte der Bewegung, anstatt in ihrer Position zu erstarren. Teleporter stellen die RĂŒckstoßzeit so ein, dass man ausgespuckt wird, ohne dass die Reibung am Boden den Impuls absorbiert.

Es gibt den Regressionsbefehl regress:pmove, der Bewegungen wie Fallenlassen, Gehen, Springen und Streifen an WĂ€nden simuliert. Es handelt sich dabei um eine Art Barriere, nicht um eine vollstĂ€ndige Physiksimulation. Auch bei BewegungsĂ€nderungen muss diese Barriere weiterhin durchlaufen werden. Ein kĂŒrzlich durchgefĂŒhrter Testlauf mit q3dm0 ergab eine stabile Fallhöhe, eine Gehdistanz, eine Sprunggeschwindigkeit von 270 (Quake-Skala, 257,2 nach dem ersten Integrationsschritt im Testframework) und eine endliche Wandlauf-Delta – ausreichend, um Fehler wie „Ich habe die Schwerkraft umgekehrt“ zu erkennen, bevor jemand das Fenster öffnet.

Die Ansichtsdarstellung liegt ĂŒber der PMove-Ansicht, nicht darin. ViewOffset fĂŒgt BewegungsunschĂ€rfe, Landeabfall, TreppenstufenglĂ€ttung, lineare Anpassung der Duckhöhe und richtungsabhĂ€ngigen SchadensrĂŒckstoß hinzu. Die Waffenansicht fĂŒgt eigene BewegungsunschĂ€rfe und Landekorrekturen hinzu, damit die Waffe nicht wie ein HUD-Aufkleber schwebt. Diese Ebenen lassen sich leicht ĂŒberspringen, wenn man nur Wert auf die WASD-Steuerung legt, und genau sie entscheiden darĂŒber, ob eine Portierung fertig oder billig wirkt.

Wasser- und Zuschauerbewegungen werden verschoben. FĂŒr q3dm0 ist das akzeptabel.

Kollision mit Quakes BSP

Man kann nicht mit dem Render-Mesh kollidieren und es dann „Quake“ nennen. Render-OberflĂ€chen sind fĂŒr die Darstellung optimiert. Clipping-Daten befinden sich in Brushes, die an BlĂ€ttern im BSP-Baum angehĂ€ngt sind.

CollisionModel lĂ€dt Ebenen, Knoten, BlĂ€tter, Pinsel und Pinselseiten und implementiert anschließend Box-Traces analog zu CM_BoxTrace. Die Spielerbewegung verwendet eine Maske, die den Spielerkörper als Inhalt verwendet. Kugeln und Plasma nutzen eine Schussmaske, die den Spielerclip ignoriert. Patch-OberflĂ€chen tragen zusĂ€tzliche Kollisionsdreiecke bei, die wĂ€hrend der Batch-Generierung erstellt werden, da gekrĂŒmmte Clip-Geometrie nicht immer einfach mit Pinseln zu berechnen ist.

TĂŒren sind Brush-Modelle. Ihr Volumenkörper ist nicht immer in den Welt-Blatt-Brush-Listen enthalten, wie es bei statischen Welt-Brushes der Fall ist. Daher fĂŒgt die Wiedergabeschleife in jedem Frame bewegliche AABBs in das Kollisionsmodell ein. Dies ist eine pragmatische Übergangslösung: VollstĂ€ndiges Submodell-Brush-Clipping kann spĂ€ter erfolgen; das Verhindern, dass der Spieler durch eine geschlossene TĂŒr geht, ist nicht möglich.

Spuren liefern Endposition, FlĂ€chennormale, Bruchteil und OberflĂ€chenmarkierungen. Diese Markierungen steuern Fußspuren auf Metall, den Himmelseffekt „Kein Einschlag“ bei Kugeln und gegebenenfalls Ausnahmen fĂŒr FallschĂ€den.

Der Brush-Clip-Schritt selbst ist der Punkt, an dem die Portierung aufhört, wie eine Mesh-Demo auszusehen. CollisionModel::traceThroughBrush (src/Collision/CollisionModel.php) durchlĂ€uft die Brush-Seiten, berechnet die Start-/EndebenenabstĂ€nde (wobei der AABB-Offset des Spielers in den Ebenenabstand eingerechnet wird) und verfolgt die Eintritts-/Austrittsanteile – die Idee hinter CM_TraceThroughBrush:

// CollisionModel::traceThroughBrush — src/Collision/CollisionModel.php
for ($i = 0; $i < $brush['numsides']; $i++) {
    $side = $this->brushsides[$brush['firstSide'] + $i];
    $plane = $this->planes[$side['plane']];
    $n = $plane['normal'];
    $off = $tw['offsets'][$plane['signbits']];
    $dist = $plane['dist'] - ($off[0] * $n[0] + $off[1] * $n[1] + $off[2] * $n[2]);

    $d1 = $tw['start'][0] * $n[0] + $tw['start'][1] * $n[1] + $tw['start'][2] * $n[2] - $dist;
    $d2 = $tw['end'][0] * $n[0] + $tw['end'][1] * $n[1] + $tw['end'][2] * $n[2] - $dist;
    // ... miss / startout / getout bookkeeping ...
    if ($d1 > $d2) {
        $f = ($d1 - self::SURFACE_CLIP_EPSILON) / ($d1 - $d2);
        if ($f > $enterFrac) {
            $enterFrac = $f;
            $clipplane = $plane;
            $leadSurfaceFlags = $side['surfaceFlags'];
        }
    } else {
        $f = ($d1 + self::SURFACE_CLIP_EPSILON) / ($d1 - $d2);
        if ($f < $leaveFrac) {
            $leaveFrac = $f;
        }
    }
}
if ($enterFrac < $leaveFrac && $enterFrac < $tw['fraction']) {
    $tw['fraction'] = $enterFrac < 0 ? 0.0 : $enterFrac;
    // ... store planeNormal / surfaceFlags ...
}

Deshalb ist die Kollisionsberechnung mit sichtbaren Dreiecken die falsche Abstraktion: Das BSP speichert Ebenen und Brushes, die fĂŒr diese rĂ€umlichen Abfragen vorgesehen sind. Der obige Ausschnitt ist ein teilweiser Kollisionsstapel (EntitĂ€tsbewegungen verwenden weiterhin eingefĂŒgte AABBs; einige Patch-FĂ€lle verwenden zusĂ€tzliche Dreiecke), aber die Berechnung des Brush-Anteils ist der eigentliche Quake-Algorithmus und kein temporĂ€rer Workaround fĂŒr Kugeln vs. Meshes.

Der Moment, als es spielbar wurde

Das Projekt entstand nicht in einem einzigen Sprung zu einem spielbaren Spiel. Die hilfreiche Geschichte ist eine Beweiskette:

  1. PK3-lesbar – Pfade auflisten und extrahieren; Reihenfolge der PaketĂŒberschreibung nachweisen.
  2. BSP-anerkannt — IBSP 46, siebzehn Lumpen, Bounds Checks
  3. Erste Dreiecke – FlĂ€chen in CPU-Netze dekodieren; spĂ€ter Stapel hochladen
  4. Erste texturierte Welt – Bilder verbinden sich; Lichtkarten modulieren; der Himmel erscheint
  5. Erster Spawn – EntitĂ€t analysieren, +9 Z, zu Boden fallen lassen, Kamera auf Augen
  6. Maussteuerung und WASD – zunĂ€chst freie Kamera, dann echte Benutzerbefehle
  7. Schwerkraft und Kollision – bewegt sich gegen BĂŒrsten; kein Clipping mehr am Boden
  8. GegenstĂ€nde, TĂŒren, Teleporter – die Karte wird zu einem Ort, nicht zu einem Museum.
  9. Waffen und HUD – Maschinengewehr, Schrotflinte, Plasmagewehr; Statusleiste; Schmerz und Tod
  10. KohĂ€renter Ausschnitt – genug Quake-Regeln, damit sich das Spielen von q3dm0 zielgerichtet anfĂŒhlt

Die Statusmeldung ist wichtig. Die Portierung verfolgt einen Meilenstein wie Q3DM0_PLAYABLE_COHERENT und weigert sich, ohne visuelle Freigabe durch einen Benutzer den Status „Referenz fast fertig“ zu verkĂŒnden. Das ist Disziplin, nicht Marketing. Eine Karte kann mechanisch spielbar sein, wĂ€hrend Zinnfiguren und Portale immer noch fehlerhaft aussehen.

So wird es ausgefĂŒhrt

Voraussetzungen: PHP 8.5+ mit ext-zip und ext-ffi, installiertes SDL2 und ein gĂŒltiges Quake III baseq3 (oder Installations-Root) auf der Festplatte.

composer install

php -d ffi.enable=true bin/quake3.php play \
  --basepath=/path/to/quake3-or-baseq3 \
  --map=maps/q3dm0.bsp

--basepath kann Folgendes sein:

  • das Spielinstallationsverzeichnis, das baseq3/ enthĂ€lt, oder
  • das baseq3-Verzeichnis selbst, oder
  • ein Verzeichnis, das bereits *.pk3-Dateien enthĂ€lt

Dadurch wird ein natives SDL-Fenster geöffnet. Es wird kein Browser-Tab geöffnet.

Steuerelemente

Eingabe Aktion
WASD Bewegen
Pfeiltasten Drehen (seitwÀrts gehen, wÀhrend +seitwÀrts); Auf-/AbwÀrtsgehen
Maus Schauen
RMB / Alt +strafe
Mittlere Maustaste +Zoom
Weltraum Sprung
C Ducken
Schicht Gehen
Klicken / Strg / F Feuern
2 / 3 / 8 Maschinengewehr / Schrotflinte / Plasma
Rad, [ ] Vorherige / nÀchste Waffe
Bild ab / Entf Blick nach oben / unten
Ende Mittelfeld
R Wiederbelebung nach Todesverzögerung
Esc Beenden

NĂŒtzliche Optionen sind beispielsweise --freecam, --debug-lighting, --debug-surface-pick und --spawn=N.

Pmove-Regression:

php bin/quake3.php regress:pmove \
  --basepath=/path/to/quake3-or-baseq3 \
  maps/q3dm0.bsp

Diagnose-Browser-Mesh-Viewer (nicht das Spiel):

php bin/quake3.php serve \
  --basepath=/path/to/quake3-or-baseq3 \
  --map=maps/q3dm0.bsp

So sieht die Umsetzung heute aus

Auf maps/q3dm0.bsp unterstĂŒtzt der Port eine Solo-Deathmatch-Ă€hnliche Sitzung:

  • Ego-Perspektive mit Quake-Steuerung: ducken, gehen, springen, treten, rutschen
  • Maschinengewehr, Schrotflinte und Plasmagewehr mit Hebe-/Senkzeitpunkt, Munitionsregeln und Einschlagseffekten
  • SammelgegenstĂ€nde (Gesundheit, RĂŒstung, Munition, Waffen) mit Quake-Greiffeldern und Respawn-Wartezeiten
  • TĂŒren (einschließlich TĂŒren fĂŒr zusammengehörige Teams), ein Teleporter, Umgebungslautsprecher, einige Timer
  • HUD-Statusleiste, Fadenkreuz, Waffenauswahlleiste, Namen von AufsammelgegenstĂ€nden, Schadensanzeige
  • Waffenmodelle mit MĂŒndungsfeuer und Maschinengewehrlaufdrehung anzeigen
  • Portale/Spiegel, Himmel, Leuchtraketen, Spielerschatten
  • DĂŒnne Audioausgabe ĂŒber Plattformplayer fĂŒr eine Teilmenge von Ereignissen

Architektonisch gesehen sind die wichtigen PHP-Typen im besten Sinne des Wortes langweilig:

Bereich Arten
FS FileSystem, Pk3Archive, Path
BSP BspLoader, BspMap, Lump-Hilfsfunktionen, VisData
Rendervorbereitung WorldBatchBuilder, ShaderParser, MaterialResolver, ImageLoader
Plattform SdlGl, Gl, ThinSound
Bewegen/Kollisionieren Pmove, Player, Usercmd, CollisionModel
Spiel MapGame, EntityParser, SpawnPoint, MoverSystem, WeaponState
Shell Anwendung

Die Konsolenanwendung ist so groß, weil sie aktuell die Wiedergabeschleife steuert. Das ist nachvollziehbar. Eine zukĂŒnftige Bereinigung könnte eine GameSession-Klasse extrahieren; der Datenfluss wĂŒrde sich dadurch nicht Ă€ndern.

Was fehlt noch?

Seien Sie prÀzise.

Die Weltmaterialien stellen weiterhin die grĂ¶ĂŸte visuelle LĂŒcke dar. Einige OberflĂ€chen sind schwarz oder fehlerhaft, obwohl die Texturen aufgelöst sind. Die vollstĂ€ndige Quake-Shader-Implementierung ist noch nicht abgeschlossen.

**Der Inhaltsbereich ist ein einzelner q3dm0-Ausschnitt. Andere Karten können zwar gestartet werden, sind aber nicht das angestrebte QualitÀtsziel.

Waffen außer MG/SG/Plasma fehlen. Der Handschuh ist nummeriert, wird aber nicht benutzt. In diesem Abschnitt gibt es keine Railgun-Blitz-BFG-Raketengranaten-Pfade.

Nicht im Leistungsumfang enthaltene Systeme: Bots, Netzwerkfunktionen, QVM-Bytecode, MenĂŒs/Cvar-UI, SpielstĂ€nde, vollstĂ€ndige rĂ€umliche Klangdarstellung, vollstĂ€ndige dynamische Lichtinfrastruktur, vollstĂ€ndige KartenobjektparitĂ€t, Wasservolumen.

Die Kollisionserkennung fĂŒr komplexe Objekte erfolgt mittels AABB-UnterstĂŒtzung anstelle einer vollstĂ€ndigen Modellverfolgung mit Pinseln.

Der Ton ist nur teilweise vorhanden: Wichtige Ereignisse werden wiedergegeben; es handelt sich nicht um einen Mischpult.

Browser serve ist kein zweiter Spielclient.

Es wĂ€re irrefĂŒhrend, dies ohne diese EinschrĂ€nkungen als „Quake III in PHP“ zu bezeichnen. Die Bezeichnung „ein spielbarer Ausschnitt einer Quake-III-Karte in PHP mit einer echten Engine-Pipeline“ ist hingegen korrekt.

Ein damit verbundener Ehrlichkeitscheck: Es gibt Vergleichsprojekte in JavaScript und anderen Sprachen, die Quake-Karten im Browser darstellen. Sie sind wertvolle Referenzen fĂŒr die visuellen Erwartungen. Diese PHP-Portierung wurde nicht zu solchen Projekten. Sie behielt bewusst ein natives Fenster und eine PHP-Spielschleife bei. Beim Vergleich der Darstellung mit einem WebGL-Viewer galt weiterhin folgende Reihenfolge: zuerst Quakes C-Quellcode, dann der Viewer als Referenz fĂŒr die Darstellung der OberflĂ€che und schließlich das zu testende PHP-Verhalten. Diese Reihenfolge verhindert, dass sich die Portierung an den AnnĂ€herungen anderer orientiert.

Performance und wo PHP SchwÀchen aufweist

Ich werde keine Frametime-Zahlen erfinden. Korrektheit stand an erster Stelle. Subjektiv betrachtet ist der q3dm0-Slice auf einem modernen Rechner interaktiv; er ist keine Baseline von 1999 auf Hardware von 1999 und will es auch gar nicht sein.

Wahrscheinliche Kostenstellen, die eher auf technische Beurteilungen als auf Messungen zurĂŒckzufĂŒhren sind:

  • PHP-Arrays und -Objekte fĂŒr Knoten, Batches und Traces benötigen mehr Speicher als kompakte C-Strukturen.
  • Die Arbeit pro Frame in der Wiedergabeschleife (Aktualisierungen von EntitĂ€ten, HUD, Effekte) ist bequem zu schreiben und einfach zu gestalten.
  • Die Shader-Stufenkonfiguration auf dem Pfad der festen Funktionen weist mehr Validierungen und Verzweigungen auf als ein gebackenes Material.
  • Bild- und MD3-Uploads werden zu Beginn ausgefĂŒhrt; das ist gut fĂŒr die Wiedergabe, aber schlecht fĂŒr die Startverzögerung. Kollisionsabfragen sind algorithmisch; PHP-Konstanten spielen eine Rolle, wenn viele Abfragen pro Sprung ausgefĂŒhrt werden.

Nichts davon war ein Grund aufzuhören. Es war vielmehr ein Grund, die Architektur einfach zu halten: einmal dekodieren, wo möglich statisch bĂŒndeln und die Mathematik von Quake nicht in eine intelligentere, aber falsche Form umschreiben.

Wenn dieses Projekt jemals wirklich Geschwindigkeit benötigt, sind die ehrlichen Optionen Profiling, Reduzierung der Zuweisungen pro Frame, Verschieben von Hot Traces hinter eine engere ReprĂ€sentation oder Akzeptanz von FFI fĂŒr ausgewĂ€hlte Kernel – und nicht so zu tun, als ob der PHP-Interpreter mit vm_x86.c ĂŒbereinstimmen wĂŒrde.

Debugging und Beobachtbarkeit

Die TriebwerksanschlĂŒsse versagen unbemerkt. Die Dreiecke sehen fast richtig aus. Der Spawnpunkt ist fast korrekt. Die Waffe feuert in den Spielerclip und explodiert an einer unsichtbaren Wand.

So wurden fĂŒr die CLI Inspektionswerkzeuge entwickelt:

  • data:inspect – Welche Pakete wurden von --basepath tatsĂ€chlich angehĂ€ngt?
  • fs:which — Welches Archiv liefert einen qpath?
  • bsp:info, bsp:entities, bsp:spawns, bsp:surfaces, bsp:lightmaps
  • shader:missing, shader:audit, render:audit
  • trace:test — eine Spielerbox auf den Boden fallen lassen
  • regress:pmove — Bewegungstor
  • Spielflags wie --debug-surface-pick, --debug-lighting, --lightmap-only, --no-pvs

Der Punkt bleibt immer derselbe: die Zwischenwelt untersuchen. Die Entwickler von Quake selbst pflegten diese Vorgehensweise. Eine PHP-Portierung benötigt sie umso mehr, da die Laufzeitumgebung bei einem fehlerhaften Lump-Offset nicht jedes Mal höflich einen Segmentierungsfehler ausgibt; manchmal zeichnet sie einfach Unsinn.

--debug-surface-pick ist das beste Beispiel fĂŒr diese Philosophie. Man zielt auf eine OberflĂ€che, drĂŒckt P, und dumpSurfacePick in src/Console/Application.php sendet eine Kollisionsabfrage vom Auge aus und durchlĂ€uft dann die Render-Batches, um den Shader unter dem Fadenkreuz zu benennen:

// Application::dumpSurfacePick — src/Console/Application.php
[$fwd] = QuakeMath::angleVectors($angles);
$end = [
    $eye[0] + $fwd[0] * 8192.0,
    $eye[1] + $fwd[1] * 8192.0,
    $eye[2] + $fwd[2] * 8192.0,
];
$tr = $cm->trace($eye, $end, [0, 0, 0], [0, 0, 0], CollisionModel::MASK_PLAYERSOLID);
fwrite(STDERR, sprintf(
    "screen hit world=(%.1f %.1f %.1f) fraction=%.4f ...\n",
    $tr->endPos[0], $tr->endPos[1], $tr->endPos[2], $tr->fraction,
));
// ... ray vs batches → shader name / lightmap / PVS cluster ...

Wenn die Zinnoptik immer noch fehlerhaft aussieht, benötigen Sie den Namen des Shaders, nicht einen weiteren Screenshot. Aus diesem Grund werden die Weltmaterialien im Statusprotokoll auch weiterhin als vom Benutzer visuell geprĂŒfte Aufgabe und nicht als vom System abgeschlossene Aufgabe aufgefĂŒhrt.

Was mir dieses Experiment beigebracht hat

Die Spieltreue ist eine Abfolge von Details. Packreihenfolge, Lum-Rollen, Spawnflags, Inhaltsmasken, Waffennummerierung – jedes Detail ist klein. Fehlen drei davon, fĂŒhlt sich das Spiel irgendwie „falsch“ an, was Spielern sofort auffĂ€llt und Debuggern erst nach und nach auffĂ€llt.

Das Render-Mesh lĂŒgt. Schöne Geometrie ist keine Kollisions-API. Sobald pmove Pinselspuren erzeugte, hörte die Karte auf, ein Filmset zu sein.

PHP kann eine Spielschleife hosten. Die Sprache selbst ist nicht das Problem. Das Problem besteht darin, BinĂ€rformate und EchtzeitzustĂ€nde ohne den Komfort eines Frameworks zu berĂŒcksichtigen.

Teilweise Shader-UnterstĂŒtzung ist nach wie vor ein großes Problem. Selbst wenn die Texturen grĂ¶ĂŸtenteils vorhanden sind, kann das Ergebnis fehlerhaft aussehen, da Quake-Materialien geschichtet sind. Fehlende Umgebungsvariablen oder fehlerhafte Überblendungen fĂŒhren nicht zu einem eleganten Grafikfehler; sie verwandeln Metall in Leere.

Die Dokumentation gehört neben die eigentliche Arbeit, nicht dauerhaft in den Laufzeitcode. Statusprotokolle und FormatprĂŒfungen sind beim Portieren unerlĂ€sslich, lenken aber ab, sobald jemand das Spiel einfach nur ausfĂŒhren möchte. Durch die Trennung bleibt das Code-Repository ĂŒbersichtlich.

Meilensteine ​​nicht eigenmĂ€chtig bewerben. PLAYABLE_COHERENT ist eine Aussage ĂŒber einen Ausschnitt. REFERENCE_CLOSE benötigt Augen auf Zinn und Portalen. Maschinen sollten den Geschmack nicht abstempeln.

Abschluss

Der GPL-Quellcode der Quake III Arena-Engine ist ein Geschenk: eine vollstĂ€ndige, meinungsstarke Spezifikation, die beschreibt, wie ein Arena-Shooter der spĂ€ten 90er-Jahre funktioniert. Die Portierung eines Teils davon nach PHP legte alle Annahmen offen – ĂŒber Dateisysteme, Karten, Materialien, Bewegungssteuerung und die Schnittstelle zwischen Sprache und GPU.

Das Ergebnis ist kein Produkt, das Quake III ersetzt. Es ist ein funktionierendes Labor: php bin/quake3.php play gegen ein echtes baseq3, das mit q3dm0 lÀuft, wobei Quakes Sprunggeschwindigkeit immer noch auf 270 eingestellt ist, Plasma immer noch die Schussinhaltsmaske um Erlaubnis fragt und das HUD immer noch niedrige Gesundheit mit einem 256-ms-Takt blinkt, weil das die Bedeutung von (cg.time >> 8) & 1 ist.

Wenn Sie ĂŒber legale Spieldaten, eine PHP 8.5-Toolchain und SDL2 verfĂŒgen, können Sie es ausfĂŒhren. Falls Sie sich fĂŒr Engines interessieren, können Sie PHP so lesen, wie man frĂŒher C las – nicht als Framework-Tutorial, sondern als Leitfaden fĂŒr Entscheidungen.

Genau darum ging es. Nicht darum, PHP fĂŒr Spiele populĂ€r zu machen, sondern darum, eine Engine so ernst zu nehmen, dass ihre Schnittstellen neu implementiert werden mussten, bis aus einer Karte wieder ein Ort wurde.

Quellcode: https://github.com/matyo91/quake-III-arena-php

Anmelden um auf diesen Beitrag zu reagieren

🚀 1

Site

  • Sitemap
  • Kontakt
  • Impressum

Network

  • Hello
  • Blog
  • Apps
  • Photos

Social

Darkwood 2026, alle Rechte vorbehalten