đŸ Ich habe Quake III Arena nach PHP portiert â von PK3-Dateien zu einer spielbaren Karte
vom 14. August 2026
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_doorund Ă€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:
- Misst ein Delta eines eingespannten Rahmens
- Ruft SDL-Ereignisse ab und erstellt einen strukturierten Eingabe-Snapshot.
- Erstellt ein
Usercmd(vorwĂ€rts/seitlich/oben, Tasten, Winkel, WaffenwĂŒnsche) - FĂŒhrt
Pmovegegen das Kollisionsmodell aus (wobei TĂŒrkörper als EntitĂ€tsboxen eingefĂŒgt werden) - Aktualisiert
MapGame(AufsammelgegenstÀnde, Bewegungsobjekte, Projektile, Timer) - Steuert das Abfeuern und den Waffenstatus.
- Berechnet die Offsets der Ego-Perspektive (Bob, Land, Damage Kick, Duck Lerp)
- Zeichnet die Welt, Objekte, Effekte, die Waffenansicht und das HUD.
- 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:
- PK3-lesbar â Pfade auflisten und extrahieren; Reihenfolge der PaketĂŒberschreibung nachweisen.
- BSP-anerkannt â IBSP 46, siebzehn Lumpen, Bounds Checks
- Erste Dreiecke â FlĂ€chen in CPU-Netze dekodieren; spĂ€ter Stapel hochladen
- Erste texturierte Welt â Bilder verbinden sich; Lichtkarten modulieren; der Himmel erscheint
- Erster Spawn â EntitĂ€t analysieren, +9 Z, zu Boden fallen lassen, Kamera auf Augen
- Maussteuerung und WASD â zunĂ€chst freie Kamera, dann echte Benutzerbefehle
- Schwerkraft und Kollision â bewegt sich gegen BĂŒrsten; kein Clipping mehr am Boden
- GegenstĂ€nde, TĂŒren, Teleporter â die Karte wird zu einem Ort, nicht zu einem Museum.
- Waffen und HUD â Maschinengewehr, Schrotflinte, Plasmagewehr; Statusleiste; Schmerz und Tod
- KohĂ€renter Ausschnitt â genug Quake-Regeln, damit sich das Spielen von
q3dm0zielgerichtet 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--basepathtatsĂ€chlich angehĂ€ngt?fs:whichâ Welches Archiv liefert einen qpath?bsp:info,bsp:entities,bsp:spawns,bsp:surfaces,bsp:lightmapsshader:missing,shader:audit,render:audittrace:testâ eine Spielerbox auf den Boden fallen lassenregress: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