Darkwood Blog Blog
  • Articles
  • Veille
  • Releases
  • CrĂ©ateurs
fr
  • de
  • en
Connexion
  • Blog
  • Articles
  • Veille
  • Releases
  • CrĂ©ateurs

đŸ‘Ÿ J'ai portĂ© Quake III Arena sur PHP — Des fichiers PK3 Ă  une carte jouable

le 14 août 2026

Connectez-vous pour réagir à cet article

🚀 1

Quake III Arena n'est pas un simple prototype. C'est un jeu de tir en arÚne complet, doté d'un systÚme de fichiers ZIP, d'un format de carte binaire, d'un langage de shaders autre que GLSL, d'un systÚme de collisions différent du maillage dessiné et d'un code de déplacement qui, vingt-cinq ans plus tard, reste trÚs particulier. Porter une partie de ce systÚme en PHP ressemblait à un défi. C'en était un, en partie. C'était aussi une maniÚre délibérée d'apprendre à maßtriser un moteur en réimplémentant ses mécanismes internes, et non en survolant un wiki.

Cet article dĂ©crit une expĂ©rience : un portage PHP progressif capable de lancer une fenĂȘtre native, de charger des donnĂ©es PK3 (version commerciale ou dĂ©mo), d'analyser une carte IBSP, de gĂ©nĂ©rer des lots de rendu, de gĂ©rer les dĂ©placements du joueur Ă  la Quake en tenant compte des collisions BSP, de tirer avec un arsenal rĂ©duit, d'ouvrir des portes et de rendre la carte maps/q3dm0.bsp suffisamment cohĂ©rente pour s'y dĂ©placer, combattre et mourir. Il ne s'agit pas d'affirmer que Quake III a Ă©tĂ© entiĂšrement réécrit en PHP. Il n'y a ni QVM, ni multijoueur, ni IA de bots, ni systĂšme de menus, et les textures du monde sont encore incomplĂštes. Ce qui existe, c'est une version jouable, avec la logique du moteur implĂ©mentĂ©e en PHP et l'interface graphique gĂ©rĂ©e par FFI.

Si vous souhaitez uniquement exécuter la commande, passez directement à la section Comment l'exécuter. Le reste décrit le chemin d'accÚs qui a permis de rendre cette commande utile.

Pourquoi porter Quake III sur PHP ?

PHP passe la majeure partie de son temps Ă  rĂ©pondre aux requĂȘtes HTTP. Ce type de traitement est de courte durĂ©e : analyser une requĂȘte, interroger une base de donnĂ©es, gĂ©nĂ©rer du HTML ou du JSON, puis terminer. Les moteurs de jeu fonctionnent Ă  l’inverse. Ils conservent l’état pendant plusieurs minutes, effectuent des dizaines d’itĂ©rations par seconde, dĂ©codent des formats binaires et font la distinction entre un maillage de rendu et un pinceau de dĂ©coupage.

Je savais dĂ©jĂ  que PHP pouvait faire bien plus que des pages web. La gamme Darkwood inclut des outils hors ligne complexes et des fenĂȘtres natives compatibles avec FFI. Quake III reprĂ©sentait un dĂ©fi plus ardu : il ne s’agissait pas simplement de « dessiner des triangles », mais de « respecter suffisamment le pipeline id Tech 3 pour que les rĂ©flexes acquis dans le jeu original restent intacts ».

Les objectifs étaient concrets :

  • ConsidĂ©rez le code source du moteur GPL comme une spĂ©cification, et non comme une source d'inspiration.
  • Conservez les donnĂ©es du jeu hors du dĂ©pĂŽt et chargez-les comme le fait Quake : via des archives PK3.
  • PrivilĂ©gier la fidĂ©litĂ© aux dĂ©tails qui donnent au jeu l'aspect de Quake (ordre du systĂšme de fichiers, blocs BSP, nombre de mouvements, rĂšgles d'apparition) plutĂŽt qu'une architecture tape-Ă -l'Ɠil mais inventĂ©e. Acceptez que PHP soit plus lent que C, et demandez-vous tout de mĂȘme s'il peut ĂȘtre suffisamment correct pour ĂȘtre jouable.

Je ne cherchais pas Ă  prouver que PHP devrait remplacer C pour le dĂ©ploiement de logiciels de tir. Je cherchais plutĂŽt Ă  voir jusqu'oĂč un environnement d'exĂ©cution PHP moderne peut aller lorsqu'on cesse de considĂ©rer chaque programme comme un gestionnaire de requĂȘtes.

Qu'est-ce que id Software a réellement publié

En 2005, id Software a publiĂ© le code source du moteur Quake III Arena sous licence GPL. Cette publication constitue le fondement juridique et technique de ce portage. Elle inclut le code C du client, du serveur, du moteur de rendu, du systĂšme de collisions et des modules de jeu — la machinerie mĂȘme.

Ce fichier n'inclut pas les ressources commerciales du jeu. Les cartes, textures, sons, modĂšles et packages PK3 qui donnent Ă  Quake III son apparence et son ambiance sonore restent la propriĂ©tĂ© de Quake III. Le dĂ©veloppement a utilisĂ© des donnĂ©es locales ou des donnĂ©es de dĂ©monstration accessibles via une option en ligne de commande. Ces fichiers ne doivent en aucun cas ĂȘtre intĂ©grĂ©s Ă  l'arborescence Git.

Cette distinction est essentielle à chaque étape de cet article. Lorsque je dis « le port charge pak0.pk3 », je parle de votre fichier baseq3 légal, et non d'un jeu redistribué. Lorsque je dis « basé sur Quake III Arena », je fais référence aux algorithmes et à la structure des données issus des sources GPL, réimplémentés en PHP.

Récupérer les données du jeu dans PHP

Quake III n'utilise pas un rĂ©pertoire de fichiers TGA comme modĂšle de contenu principal. Le contenu est stockĂ© dans des fichiers PK3 : des archives ZIP respectant la convention de chemin d'accĂšs de Quake. Un rĂ©pertoire baseq3 typique contient pak0.pk3 jusqu'aux correctifs ultĂ©rieurs. Les correctifs ultĂ©rieurs remplacent les prĂ©cĂ©dents lorsqu'ils partagent le mĂȘme chemin d'accĂšs interne.

Le portage PHP reprend ce principe dans src/Filesystem/FileSystem.php. L'option --basepath permet de spĂ©cifier le chemin d'accĂšs Ă  la racine d'installation (avec un sous-rĂ©pertoire baseq3), au rĂ©pertoire baseq3 lui-mĂȘme, ou Ă  un rĂ©pertoire contenant des fichiers PK3. Le chargeur dĂ©tecte les fichiers *.pk3, trie leurs noms comme le fait paksort dans Quake, et ajoute un prĂ©fixe Ă  chaque archive pour que les paquets suivants soient prioritaires par ordre alphabĂ©tique. Une recherche parcourt ensuite cette liste et s'arrĂȘte au premier rĂ©sultat (sĂ©mantique de 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;
}

C’est lĂ  la spĂ©cificitĂ© de Quake : il ne s’agit pas d’« ouvrir un seul fichier ZIP », mais de « prĂ©senter plusieurs archives comme un seul systĂšme de fichiers virtuel avec un ordre de priorité ». Le fait que pak1.pk3 l’emporte sur pak0.pk3 pour le mĂȘme chemin d’accĂšs (qpath) est le principe de fonctionnement des mises Ă  jour mineures et des correctifs.

Pk3Archive indexe chaque fichier ZIP une seule fois. La fonction Path::normalize dans src/Filesystem/Path.php supprime les sĂ©parateurs initiaux, convertit la casse et rejette les caractĂšres d'Ă©chappement .. afin que Maps/Q3DM0.BSP et maps/q3dm0.bsp soient rĂ©solus de la mĂȘme maniĂšre. Il n'existe pas de chemin de recherche par rĂ©pertoire libre dans ce portage ; cette restriction est intentionnelle.

Les outils en ligne de commande comme fs:which, pk3:list et data:inspect existent car les erreurs du systĂšme de fichiers peuvent ĂȘtre confondues avec des bogues du moteur de rendu. Si le mauvais paquet est sĂ©lectionnĂ©, on pourrait croire que l'analyseur de shaders est dĂ©fectueux. Lorsque bsp:mesh ou le dĂ©marrage du jeu affiche la provenance d'un BSP, il indique le chemin d'accĂšs (qpath) et l'archive sĂ©lectionnĂ©e ; cette simple ligne de commande nous a fait gagner un temps prĂ©cieux de dĂ©bogage, bien plus que n'importe quelle sortie de profileur.

Comprendre IBSP

Les cartes sont des fichiers BSP : arbres de partitionnement spatial binaire plus un ensemble de blocs associĂ©s. Quake III Arena utilise l’identitĂ© IBSP et la version 46, avec 17 blocs dans l’en-tĂȘte.

BspLoader (src/Bsp/BspLoader.php) ne transforme pas d'abord le fichier en un graphe d'objets complexe. Il traite les octets comme une structure qui doit correspondre Ă  l'en-tĂȘte de Quake : identitĂ©, version, puis dix-sept paires {fileofs, filelen}. Cette approche est lĂ©gĂšrement plus stricte que certaines anciennes mĂ©thodes C qui se fiaient uniquement Ă  la version : les dĂ©calages incorrects sont rejetĂ©s avant mĂȘme que quiconque n'accĂšde Ă  l'index du tampon.

// 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
') manipule des entiers non signĂ©s 32 bits en little-endian, soit la mĂȘme largeur et le mĂȘme ordre d'octets que les entiers stockĂ©s sur disque dans Quake sur les plateformes concernĂ©es. La modĂ©lisation PHP idiomatique intervient aprĂšs cette fidĂ©litĂ©. Une erreur de lecture de la longueur d'un bloc de quatre octets ne gĂ©nĂšre pas une exception de domaine Ă©lĂ©gante par la suite ; elle produit une carte illisible.

Ces éléments ne sont pas interchangeables. Le port utilise des éléments différents pour différentes tùches :

  • Dessiner la gĂ©omĂ©trie : shaders, drawverts, drawindexes, surfaces, lightmaps, lightgrid, visibility, fogs (partiellement)
  • Collision : plans, nƓuds, feuilles, brosses Ă  feuilles, brosses, cĂŽtĂ©s de brosses, shaders (pour les indicateurs de contenu)
  • EntitĂ©s de jeu : le bloc d'entitĂ©s — un bloc de texte contenant des dictionnaires de paires clĂ©/valeur dĂ©limitĂ©s par des accolades
  • ModĂšles / dĂ©mĂ©nageurs : les modĂšles regroupent les indices que func_door et les fonctions associĂ©es rĂ©fĂ©rencent comme des modĂšles de brosse *N

Le rendu gĂ©omĂ©trique rĂ©pond Ă  la question « Que dois-je dessiner ? » La gestion des collisions rĂ©pond Ă  la question « OĂč peut aller une boĂźte ? » La gestion des entitĂ©s rĂ©pond Ă  la question « OĂč commence le joueur et quels sont les objets ? » Confondre ces trois Ă©lĂ©ments est le moyen classique d’obtenir une belle carte Ă  travers laquelle on tombe.

Des surfaces BSP Ă  un format dessinable

Une fois la carte chargée en mémoire, WorldBatchBuilder parcourt les surfaces et crée des lots de rendu statiques indexés par le shader et la lightmap. Les faces planes et les structures triangulaires deviennent des maillages indexés. Les patches de Bézier sont tessellés sur le processeur. Les surfaces du ciel et des halos sont classées et souvent ignorées en tant que géométrie solide, puis traitées par des chemins dédiés.

Ce qui surprend un développeur PHP lambda, c'est le manque d'élégance du chemin des sommets. Dans Quake, drawVert_t est un enregistrement fixe de 44 octets. La fonction BspDrawVert::fromBytes du fichier src/Bsp/BspDrawVert.php le décompose champ par champ :

// 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]),
    ],
);

C'est là tout le principe : une chaßne d'octets BSP devient $xyz, $st, les coordonnées UV de la lightmap, une normale et quatre octets de couleur. Ensuite, les surfaces indexent ces sommets en triangles, puis WorldBatchBuilder les regroupe pour OpenGL. Sans ORM. Sans « VertexRepository ». Fidélité de la disposition binaire, encore une fois.

Au lancement de q3dm0, un rĂ©sumĂ© typique des erreurs standard (stderr) affiche des dizaines de milliers de triangles rĂ©partis sur une centaine de lots, plusieurs pages de lightmaps, les triangles de collision des patchs pour la face de dĂ©coupage des surfaces courbes, et un nombre de clusters PVS se chiffrant en milliers. Ces chiffres ne constituent pas une performance de rĂ©fĂ©rence ; ils rappellent simplement que mĂȘme une « petite » carte Quake reprĂ©sente un vĂ©ritable jeu de donnĂ©es.

Le moteur de rendu ne diffuse pas le graphe de scÚne C original refEntity_t. PHP prépare les lots et l'état ; OpenGL (via FFI) soumet des rendus à fonction fixe. Il s'agit d'une limite pragmatique : PHP garantit l'exactitude de l'interprétation des ressources ; le GPU effectue toujours le traitement raster.

Les premiĂšres erreurs ont Ă©tĂ© instructives. Appliquer une inversion verticale inutile aux UV des lightmaps donnait un aspect lĂ©gĂšrement incorrect Ă  toutes les surfaces Ă©clairĂ©es. Faire la moyenne des normales du plan du portail au lieu d'utiliser le plan du premier triangle a inversĂ© l'orientation du portail. Mettre en cache le nom d'un shader comme « dĂ©jĂ  chargĂ© » avant la liaison des textures des Ă©tapes sƓurs a ignorĂ© les textures d'environnement et de fusion. Les surfaces de type « soupe », qui auraient dĂ» utiliser les couleurs des sommets, ont Ă©tĂ© briĂšvement traitĂ©es comme des faces Ă©clairĂ©es et sont devenues trĂšs lumineuses. Pris individuellement, aucun de ces bugs ne semble dramatique. C'est leur accumulation qui explique pourquoi un portage doit dĂ©boguer les valeurs intermĂ©diaires, et pas seulement les pixels finaux.

Le systĂšme de visibilitĂ© (PVS) est chargĂ© et utilisĂ© pour limiter les lots pertinents pour un groupe de camĂ©ras. Sa cohĂ©rence est Ă©valuĂ©e dans le journal d'Ă©tat, ce qui signifie qu'il est suffisant pour jouer Ă  q3dm0, mĂȘme si tous les cas limites de l'organisation des groupes de camĂ©ras de Quake n'ont pas Ă©tĂ© vĂ©rifiĂ©s. DĂ©sactiver le PVS avec un paramĂštre reste utile si vous soupçonnez un problĂšme de culling plutĂŽt qu'un problĂšme de matĂ©riaux.

Choix de la limite de rendu

Une question lĂ©gitime se pose : pourquoi ne pas compiler le code C original avec Emscripten et le prĂ©senter comme un projet PHP ? Il s’agirait alors d’un simple empaquetage, et non d’un portage.

Ce projet conserve la logique de Quake en PHP :

  • Ordre de recherche PK3
  • DĂ©codage BSP
  • Analyse des scripts de shaders
  • Traces de collision
  • Mouvements des joueurs
  • RĂšgles de l'entitĂ©/du jeu pour la tranche prise en charge
  • Orchestration du rendu (quoi dessiner, avec quelles textures et mĂ©langes)

La plateforme est lĂ©gĂšre : SdlGl ouvre une fenĂȘtre, crĂ©e un contexte GL, interroge les entrĂ©es et Ă©change les tampons. Gl est une grande bibliothĂšque d'assistance OpenGL Ă  fonction fixe — monde, objets MD3, vue des armes, HUD, effets — toujours gĂ©rĂ©e par un Ă©tat appartenant Ă  PHP.

Il existe également une commande serve qui permet d'afficher un visualiseur d'orbites WebGL pour les dumps de maillage. Elle est utile pour les diagnostics. Ce n'est pas le jeu. Le chemin d'accÚs au jeu est natif :

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

Ou, sous forme de chaßne d'appels :

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

PHP conserve l'Ă©tat du jeu tant que la fenĂȘtre est ouverte. Il ne se contente pas de traiter une seule requĂȘte HTTP avant de se fermer. Le chemin serve de WebGL sĂ©rialise un maillage statique pour la visualisation orbitale ; il n'exĂ©cute pas cette boucle.

Cette séparation correspond à un ancien modÚle Darkwood : PHP comme cerveau durable, FFI comme porte métallique.

Textures, shaders et lightmaps

Les « shaders » de Quake III ne sont pas des programmes de shaders GPU modernes. Ce sont des scripts de matériaux situés dans scripts/*.shader : des étapes avec des textures, des modes de fusion, des tests alpha, des transformations tcMod, des animMaps, le mappage d'environnement et des clés de tri. Le GPU de 1999 exécutait un pipeline beaucoup plus simple ; le script indiquait comment l'alimenter.

Le port PHP analyse ces scripts (ShaderParser, Q3Shader, stages) et exécute un sous-ensemble partiel lors du rendu : modulation des lightmaps, nombreux modes de fusion, effets de défilement/rotation/turbulence, stages d'environnement sur certaines surfaces et MD3, skyboxes, portails/miroirs, flares. Le chargement des textures prend en charge les formats réellement utilisés par Quake pour ces données (chemins TGA/JPEG via ImageLoader).

Ce qui n'est pas garanti : la compatibilitĂ© totale des shaders. L'Ă©tat d'avancement de l'exĂ©cution des shaders est partiel. Certains matĂ©riaux du monde sur q3dm0 apparaissent toujours noirs ou incorrects, mĂȘme aprĂšs la rĂ©solution des fichiers image. Il s'agit du principal obstacle visuel actuel : non pas des fichiers manquants, mais un comportement incomplet ou incorrect de l'interface. Des outils comme --debug-material= et --debug-surface-pick permettent Ă  un utilisateur de pointer la camĂ©ra vers une surface dĂ©fectueuse et d'identifier le shader concernĂ©, au lieu de devoir deviner Ă  partir d'une capture d'Ă©cran.

Les lightmaps sont chargĂ©es comme des pages et appliquĂ©es de maniĂšre classique dans Quake : une seconde Ă©tape de texturage module l’aspect diffus du monde. La rĂ©ussite du chargement linĂ©aire et de l’orientation UV Ă©tait plus importante que n’importe quelle optimisation PHP.

Trouver le point d'apparition du joueur

Le bloc « entities » est du texte brut intĂ©grĂ© au BSP ; Quake n’a pas besoin d’une base de donnĂ©es sĂ©parĂ©e pour savoir oĂč se trouvent les joueurs et les objets. La fonction EntityParser::deathmatchSpawns dans src/Game/EntityParser.php parcourt les dictionnaires analysĂ©s et extrait les zones de deathmatch (avec info_player_start traitĂ© comme un alias, comme dans g_client.c).

// 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));

Cela ne vous donne qu'une liste. Le premier point d'apparition de Quake privilĂ©gie une zone avec spawnflags & 1. Sur q3dm0, il s'agit de la zone du hall d'entrĂ©e, et non de l'entitĂ© d'ordre zĂ©ro. Le portage reproduit SelectInitialSpawnPoint / SelectRandomFurthestSpawnPoint dans src/Game/SpawnPoint.php — il s'agit d'une rĂšgle fidĂšle pour la zone prise en charge, et non d'une simplification du type « choisir [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]);

Les réapparitions sont triées par distance par rapport au point de mort et choisies aléatoirement parmi les deux plus éloignées. L'option --spawn=N reste prioritaire pour le débogage.

Le point d'apparition reçoit le classique +9 sur Z (playerOrigin()) avant que le joueur ne soit lùché au sol avec un effet de rendu visuel. Si vous oubliez l'ascenseur, vous apparaissez au sol. Si vous sautez la chute, vous apparaissez en l'air et les premiÚres images sont étranges.

MapGame configure ensuite le reste des entités prises en charge pour cette carte : objets avec modÚles MD3, téléporteur, portes (y compris les portes groupées qui doivent se déplacer ensemble), haut-parleurs d'ambiance, minuteurs, objets de carte rotatifs et surfaces de portail. Les dialogues d'introduction sont volontairement omis ; l'objectif était de créer une arÚne jouable, et non un personnage se contentant de visionner des cinématiques.

Créer une véritable boucle de jeu

Le script bin/quake3.php augmente les limites de mémoire et de temps, charge l'autoloader de Composer et transmet argv à Application. La commande play ne constitue pas un ensemble de plugins. Il s'agit d'une phase d'initialisation explicite suivie d'une boucle while ($running).

La fonction d'installation charge le BSP, crée les collisions et les lots, construit le monde du jeu, crée le joueur, initialise SDL/GL, charge les ressources GPU et affiche un bref inventaire des éléments trouvés. La boucle suivante s'exécute :

  1. Mesure le delta d'un cadre fixé
  2. Collecte les événements SDL dans un instantané d'entrée structuré
  3. Crée un Usercmd (avant/cÎté/haut, boutons, angles, souhaits d'armes)
  4. Exécute Pmove sur le modÚle de collision (avec les solides de porte injectés comme boßtes d'entités).
  5. Mises à jour de MapGame (objets à ramasser, éléments mobiles, projectiles, minuteurs)
  6. GÚre le tir et l'état de l'arme.
  7. Calcule les décalages de vue à la premiÚre personne (bob, land, damage kick, duck lerp)
  8. Affiche le monde, les entités, les effets, l'arme visible et l'interface.
  9. Échange les tampons

C'est courant pour un jeu. C'est inhabituel pour PHP uniquement parce que la culture PHP Ă©crit rarement ce type de programme. La boucle rĂ©side dans Application::cmdPlay (src/Console/Application.php) en tant que processus de longue durĂ©e — et non comme un cycle de requĂȘtes.

// 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 ...
}

Rien dans cet extrait ne relĂšve d'un middleware fictif. Le temps delta est limitĂ© afin qu'un accroc ne tĂ©lĂ©porte pas le joueur Ă  travers un mur. Les entrĂ©es sont enregistrĂ©es dans un instantanĂ©. Le reste de la trame — la crĂ©ation d'un objet Usercmd, l'exĂ©cution de Pmove, la mise Ă  jour des entitĂ©s, le rendu — correspond Ă  la complexitĂ© induite par la boucle, et non Ă  la boucle elle-mĂȘme.

Regard Ă  la souris et saisie au clavier

Les entrées sont gérées par SdlGl::pollEvents. Les touches sont associées à de courts noms (w, shift, pgdn, 
). Les déplacements de la souris s'accumulent à chaque image. Les boutons permettent d'attaquer, de se déplacer latéralement et de zoomer.

Look utilise le comportement par dĂ©faut de Quake : m_yaw * sensitivity avec des valeurs par dĂ©faut correspondant Ă  0,022 × 5 degrĂ©s par dĂ©placement de la souris. Les touches flĂ©chĂ©es permettent de tourner Ă  cl_yawspeed 140 (et les touches de tangage Ă  cl_pitchspeed 140), la touche Maj appliquant le multiplicateur classique cl_anglespeedkey 1,5 pour la « vitesse ».

Maintenez le bouton droit de la souris ou Alt enfoncĂ© pour activer le dĂ©placement latĂ©ral : la souris vous fait glisser au lieu de vous faire tourner, et les flĂšches servent Ă  effectuer un pas de cĂŽtĂ©. Maintenez le bouton central de la souris enfoncĂ© pour activer le zoom : le champ de vision horizontal est ajustĂ© par interpolation linĂ©aire vers cg_zoomfov 22,5 sur 150 ms et la sensibilitĂ© de la camĂ©ra est multipliĂ©e par fov_y / 75 en mode zoom — les mĂȘmes relations que celles utilisĂ©es par le jeu Quake.

Les touches d'armes suivent la numĂ©rotation « arme N » de Quake : 2 mitrailleuse, 3 fusil Ă  pompe, 8 plasma. La molette et les crochets permettent de sĂ©lectionner les armes en votre possession disposant encore de munitions. Tirer s'effectue avec le clic gauche, Ctrl ou F. S'accroupir s'appuie sur C. Marcher s'effectue avec Maj. Aucune de ces actions ne nĂ©cessite le verrouillage du curseur du navigateur, car la fenĂȘtre est native et utilise le mode souris relatif.

Mouvement de portage

Pmove est un sous-ensemble de bg_pmove.c / bg_slidemove.c de Quake. Il ne s'agit pas d'un CharacterController Unity avec des Ă©lĂ©ments cosmĂ©tiques de Quake. Les constantes sont celles de Quake : vitesse de saut 270, friction 6, accĂ©lĂ©ration au sol 10, accĂ©lĂ©ration dans les airs 1, vitesse d'arrĂȘt 100, Ă©chelle de dĂ©placement accroupi 0,25, taille du pas 18, vitesse de marche minimale 0,7.

Chaque trame de commande :

  • Applique une friction lorsqu'il est mis Ă  la terre
  • AccĂ©lĂšre la vitesse souhaitĂ©e lors des mouvements avant/latĂ©raux en fonction de la vitesse de course ou de marche.
  • IntĂšgre la gravitĂ©
  • Effectue des dĂ©placements glissĂ©s avec dĂ©coupage de plan
  • Monter les escaliers
  • GĂšre les Ă©vĂ©nements de saut, d'accroupissement, d'atterrissage brutal et de balancement pour la vue et les bruits de pas.

Le contrÎle aérien est similaire à l'accélération aérienne de Quake, ce qui explique pourquoi les sauts propulsés et les impacts de plasma donnent cette impression, d'autant plus que le recul des éclaboussures est également fidÚle. Cette sensation ne provient pas de $pos += $vel * $dt, mais de la logique de vitesse souhaitée, inspirée de PM_Accelerate, implémentée dans Pmove::accelerate (src/Game/Pmove.php) : une adaptation fidÚle de l'étape d'accélération de Quake pour les déplacements au sol et dans les airs (les déplacements dans l'eau et en spectateur restent différés).

// 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];
}

La direction et la vitesse du mouvement sont dĂ©finies par la commande utilisateur et l'Ă©chelle de marche/course ; la vitesse actuelle correspond Ă  la projection de la vitesse sur le mouvement ; l'accĂ©lĂ©ration est plafonnĂ©e pour Ă©viter de dĂ©passer le mouvement en une seule image. Modifier la constante d'accĂ©lĂ©ration aĂ©rienne de 1 Ă  une valeur plus « agrĂ©able » donne au jeu des sensations diffĂ©rentes de celles de Quake, mĂȘme si la friction est toujours prĂ©sente.

Les corps morts conservent leur vitesse et utilisent la friction des mouvements statiques au lieu de se figer. Les téléporteurs ajustent le temps de recul pour que l'impulsion ne soit pas absorbée par la friction du sol.

Il existe une commande de rĂ©gression, regress:pmove, qui permet de se laisser tomber au sol, de marcher, de sauter et de frĂŽler un mur. Il s'agit d'une Ă©tape prĂ©liminaire, et non d'un systĂšme physique complet. Lorsque le mouvement change, cette Ă©tape prĂ©liminaire doit toujours ĂȘtre franchie. Un test rĂ©cent sur q3dm0 a confirmĂ© une hauteur de chute au sol stable, une distance de marche, une vitesse de saut (velz) de 270 Ă  l'Ă©chelle de Quake (257,2 aprĂšs la premiĂšre Ă©tape d'intĂ©gration dans le banc d'essai), et un delta de dĂ©placement mural fini — suffisant pour dĂ©tecter les erreurs de type « inversion de gravité » avant mĂȘme que quiconque n'ouvre la fenĂȘtre.

La présentation de l'interface se superpose à pmove, et non à l'intérieur. ViewOffset ajoute des effets de balancement, d'atterrissage, de lissage des marches, d'interpolation linéaire de la hauteur de la position accroupie et de recul directionnel. L'arme affichée ajoute ses propres décalages de balancement et d'atterrissage pour éviter qu'elle ne flotte comme un élément du HUD. Ces couches sont faciles à ignorer lorsqu'on se contente de vérifier que les touches WASD fonctionnent, et pourtant, ce sont précisément elles qui donnent à un portage un aspect bùclé ou, au contraire, un aspect négligé.

Le déplacement de l'eau et des spectateurs est différé. Pour q3dm0, cela est acceptable.

Collision avec le BSP de Quake

Il est impossible de provoquer une collision avec le maillage de rendu et d'appeler cela Quake. Les surfaces de rendu sont optimisées pour le dessin. Les données de découpage sont stockées dans des brosses rattachées aux feuilles de l'arbre BSP.

CollisionModel charge les plans, les nƓuds, les feuilles, les brosses et leurs cĂŽtĂ©s, puis effectue un rendu de boĂźtes Ă  la maniĂšre de CM_BoxTrace. Les mouvements du joueur sont masquĂ©s par un masque de contenu solide. Les balles et le plasma utilisent un masque de tir qui ignore les collisions avec le joueur. Les surfaces des patchs ajoutent des triangles de collision supplĂ©mentaires, construits lors de la gĂ©nĂ©ration par lots, car la gĂ©omĂ©trie des collisions courbes n'est pas toujours aussi simple Ă  modĂ©liser.

Les portes sont modĂ©lisĂ©es par des brosses. Leur volume n'est pas toujours prĂ©sent dans les listes de brosses du monde comme les brosses statiques, c'est pourquoi la boucle de jeu injecte des AABB mobiles dans le modĂšle de collision Ă  chaque image. C'est une solution pragmatique : le clipping complet des sous-modĂšles pourra ĂȘtre implĂ©mentĂ© ultĂ©rieurement ; empĂȘcher le joueur de franchir une porte fermĂ©e, en revanche, est impossible.

Les traces renvoient la position finale, la normale au plan, la fraction et les indicateurs de surface. Ces indicateurs déterminent la présence d'empreintes de pas sur du métal, l'absence d'impact de balles dans le ciel et les exemptions de dommages dus aux chutes, le cas échéant.

L'Ă©tape de clipping du brush est celle oĂč le port cesse de ressembler Ă  une dĂ©mo de maillage. CollisionModel::traceThroughBrush (src/Collision/CollisionModel.php) parcourt les cĂŽtĂ©s du brush, calcule les distances entre les plans de dĂ©part et d'arrivĂ©e (avec le dĂ©calage AABB du joueur intĂ©grĂ© Ă  la distance du plan) et suit les fractions d'entrĂ©e et de sortie — le principe de 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 ...
}

C'est pourquoi considĂ©rer les collisions avec les triangles visibles comme une abstraction erronĂ©e est une mauvaise approche : le BSP stocke des plans et des brosses destinĂ©s Ă  ces requĂȘtes spatiales. L'extrait ci-dessus reprĂ©sente une pile de collisions partielle (les entitĂ©s dĂ©placĂ©es utilisent toujours des AABB injectĂ©es ; certains cas de patch utilisent des triangles supplĂ©mentaires), mais le calcul de la fraction de brosse constitue le vĂ©ritable algorithme de Quake, et non une solution de contournement temporaire pour simuler une collision sphĂšre-maillage.

Le moment oĂč il est devenu jouable

Le projet n'est pas devenu un jeu jouable du jour au lendemain. L'histoire utile se présente comme une succession de preuves :

  1. Lecture PK3 — lister et extraire les chemins ; prouver l’ordre de remplacement des paquets
  2. BSP reconnu — IBSP 46, dix-sept contrĂŽles de type « lumps, bounds »
  3. Premiers triangles — dĂ©codage des faces en maillages CPU ; chargement ultĂ©rieur par lots
  4. Premier monde texturĂ© — les images se lient ; les lightmaps se modulent ; le ciel apparaĂźt
  5. PremiĂšre apparition — analyse de l'entitĂ©, +9 Z, chute au sol, camĂ©ra au niveau des yeux
  6. Regard Ă  la souris et WASD — initialement camĂ©ra libre, puis commandes utilisateur rĂ©elles
  7. GravitĂ© et collisions — DĂ©placement contre les brosses ; fini les collisions avec le sol
  8. Objets, portes, tĂ©lĂ©porteur — la carte devient un lieu, et non un musĂ©e
  9. Armes et interface — mitrailleuse, fusil Ă  pompe, plasma ; barre d’état ; douleur et mort
  10. Tranche cohĂ©rente — suffisamment de rĂšgles de Quake pour que jouer Ă  q3dm0 semble intentionnel

Le langage utilisĂ© pour l'Ă©tat d'avancement est important. Le portage suit une Ă©tape clĂ© comme Q3DM0_PLAYABLE_COHERENT et refuse de se promouvoir automatiquement comme « rĂ©fĂ©rence presque terminĂ©e » sans validation visuelle humaine. C'est une question de rigueur, pas de marketing. Une carte peut ĂȘtre jouable mĂȘme si les Ă©lĂ©ments en Ă©tain et les portails ont encore un aspect incorrect.

Comment l'exécuter

Configuration requise : PHP 8.5+ avec ext-zip et ext-ffi, SDL2 installé et une copie légale de Quake III baseq3 (ou une racine d'installation) sur le disque.

composer install

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

--basepath peut ĂȘtre :

  • le rĂ©pertoire d'installation du jeu qui contient baseq3/, ou
  • le rĂ©pertoire baseq3 lui-mĂȘme, ou
  • un rĂ©pertoire contenant dĂ©jĂ  des fichiers *.pk3

Cela ouvre une fenĂȘtre SDL native. Cela n'ouvre pas un onglet de navigateur.

Commandes

Entrée Action
WASD Déplacer
FlÚches Tourner (déplacement latéral en maintenant la touche +déplacement latéral) ; marcher en montée/descente
Souris Regarder
Clic droit / Alt + déplacement latéral
Souris centrale +zoom
Espace Saut
C S'accroupir
Changement Marche
Clic / Ctrl / F Tir
2 / 3 / 8 Mitrailleuse / fusil Ă  pompe / plasma
Roue, [ ] Arme précédente / suivante
Page suivante / Supprimer Rechercher vers le haut / vers le bas
Fin Terrain central
R Réapparition aprÚs le délai de mort
Échap Quitter

Les options utiles incluent --freecam, --debug-lighting, --debug-surface-pick et --spawn=N.

Régression Pmove :

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

Visualiseur de maillage pour navigateur de diagnostic (pas le jeu) :

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

À quoi ressemble la mise en Ɠuvre aujourd'hui

Sur maps/q3dm0.bsp, le port prend en charge une session solo de type match à mort :

  • DĂ©placements Ă  la premiĂšre personne avec les chiffres de Quake : se baisser, marcher, sauter, faire un pas, glisser
  • Mitrailleuse, fusil Ă  pompe et plasma avec gestion du temps de montĂ©e/descente, rĂšgles de munitions et effets d'impact
  • Ramassage d'objets (santĂ©, armure, munitions, armes) avec des plateformes de rĂ©cupĂ©ration et des temps de rĂ©apparition Ă  la Quake
  • Des portes (y compris pour les Ă©quipes appariĂ©es), un tĂ©lĂ©porteur, des haut-parleurs d'ambiance, des minuteurs
  • Barre d'Ă©tat, rĂ©ticule, sĂ©lection d'armes, noms des armes ramassĂ©es, affichage des dĂ©gĂąts
  • Visualisez les modĂšles d'armes avec l'effet de flamme Ă  la bouche et la rotation du canon de la mitrailleuse.
  • Portails/miroirs, ciel, lueurs, ombre du joueur
  • Audio minimaliste via les lecteurs de la plateforme pour une sĂ©lection d'Ă©vĂ©nements

D'un point de vue architectural, les types PHP importants sont ennuyeux, et c'est tant mieux :

Zone Types
FS SystĂšme de fichiers, Archive Pk3, Chemin
BSP BspLoader, BspMap, fonctions d'assistance groupées, VisData
Préparation du rendu WorldBatchBuilder, ShaderParser, MaterialResolver, ImageLoader
Plateforme SdlGl, Gl, ThinSound
Déplacement/collision Pmove, Player, Usercmd, CollisionModel
Jeu MapGame, EntityParser, SpawnPoint, MoverSystem, WeaponState
Shell Application

L'application console est volumineuse car elle gÚre actuellement la boucle de jeu. C'est un fait. Un nettoyage ultérieur pourrait extraire une classe GameSession, sans modifier le flux de données.

Qu'est-ce qui manque encore ?

Soyez explicite.

Les matériaux du monde restent le principal point faible visuel. Certaines surfaces sont noires ou incorrectes malgré la résolution des textures. L'exécution complÚte des shaders Quake n'est pas encore terminée.

Le contenu correspond Ă  une seule tranche q3dm0. D'autres cartes peuvent ĂȘtre lancĂ©es, mais elles ne constituent pas la cible de fidĂ©litĂ©.

Aucune arme autre que la mitrailleuse, le fusil à pompe ou le plasma n'est présente. Le gantelet est numéroté mais inutilisé. Aucun chemin vers le canon électromagnétique, la foudre, le BFG ou la grenade à roquettes n'est prévu dans cette version.

SystĂšmes non inclus dans le pĂ©rimĂštre : bots, rĂ©seau, bytecode QVM, interface utilisateur des menus/variables de configuration, sauvegardes, spatialisation sonore complĂšte, infrastructure d’éclairage dynamique complĂšte, paritĂ© complĂšte des objets de carte, volumes d’eau.

**La collision pour les mouvements complexes est assistée par AABB plutÎt que par un traçage complet du modÚle de pinceau.

Le son est partiel : seuls les Ă©vĂ©nements importants sont diffusĂ©s ; il ne s’agit pas d’un mixeur.

Le navigateur serve n'est pas un deuxiĂšme client de jeu.

Qualifier cela de « Quake III en PHP » sans ces prĂ©cisions serait malhonnĂȘte. En revanche, le dĂ©crire comme « une portion jouable de la carte Quake III en PHP avec un vĂ©ritable moteur de jeu » est exact.

Un autre point important : des projets de comparaison existent en JavaScript et dans d’autres langages pour afficher les cartes de Quake dans un navigateur. Ils constituent de prĂ©cieuses rĂ©fĂ©rences pour connaĂźtre le rendu visuel attendu. Ce portage PHP n’a pas rejoint ces projets. Il a dĂ©libĂ©rĂ©ment conservĂ© une fenĂȘtre native et une boucle de jeu PHP. Lors de la comparaison de la fidĂ©litĂ© du rendu avec un visualiseur WebGL, l’ordre de rĂ©fĂ©rence Ă©tait toujours le suivant : d’abord les sources C de Quake, puis le visualiseur comme rĂ©fĂ©rence pour dĂ©terminer « à quoi devrait ressembler cette surface », et enfin le comportement PHP testĂ©. Cet ordre empĂȘche le portage de converger vers des approximations d’autres dĂ©veloppeurs.

Performances et points faibles de PHP

Je ne vais pas inventer de nouvelles valeurs de temps d'affichage. La correction primait sur tout. Subjectivement, sur une machine moderne, la tranche q3dm0 est interactive ; elle ne correspond pas aux performances de 1999 sur du matĂ©riel de 1999, et elle ne prĂ©tend pas l'ĂȘtre.

Centres de coûts probables, qualifiés de jugement d'ingénierie plutÎt que de mesures :

  • Les tableaux et objets PHP pour les sommets, les lots et les traces allouent plus de mĂ©moire que les structures C compactes.
  • Le travail image par image dans la boucle de lecture (mises Ă  jour d'entitĂ©s, HUD, effets) est pratique Ă  Ă©crire et facile Ă  rendre bavard.
  • La configuration de l'Ă©tape de shader sur le chemin Ă  fonction fixe comporte plus de validations et de branchements qu'un matĂ©riau prĂ©calculĂ©.
  • Les chargements d'images et de MD3 sont effectuĂ©s en dĂ©but de session ; c'est bien pour le jeu, mais cela entraĂźne une latence de dĂ©marrage importante. Les traces de collision sont algorithmiques ; les facteurs constants PHP sont importants lorsque de nombreuses traces sont exĂ©cutĂ©es par saut.

Rien de tout cela ne justifiait l'arrĂȘt du dĂ©veloppement. Au contraire, cela incitait Ă  conserver une architecture simple : dĂ©coder une seule fois, traiter par lots statiquement lorsque c'Ă©tait possible et Ă©viter de réécrire les calculs de Quake sous une forme plus astucieuse, mais erronĂ©e.

Si ce projet a un jour besoin de performances exceptionnelles, les options honnĂȘtes consistent Ă  profiler, Ă  rĂ©duire les allocations par image, Ă  dĂ©placer les traces critiques derriĂšre une reprĂ©sentation plus compacte ou Ă  accepter l'interface FFI pour certains noyaux — et non Ă  prĂ©tendre que l'interprĂ©teur PHP correspondra Ă  vm_x86.c.

Débogage et observabilité

Les ports du moteur lĂąchent sans bruit. Les triangles sont presque corrects. Le point d'apparition est presque bon. Le tir traverse la zone du joueur et explose contre un mur invisible.

L'interface de ligne de commande (CLI) a donc développé des outils d'inspection :

  • data:inspect — quels paquets --basepath a-t-il rĂ©ellement attachĂ©s ?
  • fs:which — quelle archive fournit un qpath ?
  • bsp:info, bsp:entities, bsp:spawns, bsp:surfaces, bsp:lightmaps
  • shader:missing, shader:audit, render:audit
  • trace:test — dĂ©poser une boĂźte de joueur sur le sol
  • regress:pmove — porte de mouvement
  • Jouez avec des options telles que --debug-surface-pick, --debug-lighting, --lightmap-only, --no-pvs

Le principe reste le mĂȘme : examiner le monde intermĂ©diaire. Les dĂ©veloppeurs de Quake eux-mĂȘmes avaient cette habitude. Un portage PHP en a d'autant plus besoin, car l'environnement d'exĂ©cution ne signalera pas systĂ©matiquement une erreur de segmentation Ă  la moindre imprĂ©cision ; il peut arriver qu'il affiche simplement des caractĂšres illisibles.

L'option --debug-surface-pick illustre parfaitement cette approche. Visez une surface, appuyez sur P, et la fonction dumpSurfacePick dans src/Console/Application.php effectue une analyse de collision depuis l'Ɠil, puis parcourt les lots de rendu pour identifier le shader sous le rĂ©ticule :

// 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 ...

Si l'étain ne s'affiche toujours pas correctement, il vous faut le nom du shader, et non une autre capture d'écran. C'est aussi pourquoi le journal d'état indique toujours que les matériaux du monde sont soumis à une vérification visuelle humaine plutÎt qu'à une tùche automatisée.

Ce que cette expérience m'a appris

La fidélité est une succession de raccords. Ordre des paquets, rÎles des objets, indicateurs d'apparition, masques de contenu, numérotation des armes : chaque raccord est infime. Trois raccords manquants suffisent à donner une impression de « bizarre », perceptible immédiatement par les joueurs et difficilement compréhensible par les débogeurs.

Le maillage de rendu est trompeur. Une belle géométrie n'est pas une API de collision. DÚs que les traces de pinceau des rayons ont été déplacées, la carte a cessé de ressembler à un décor de cinéma.

PHP peut héberger une boucle de jeu. Le langage n'est pas l'obstacle. L'obstacle réside dans la gestion des formats binaires et de l'état en temps réel sans le confort d'un framework.

La prise en charge partielle des shaders est encore loin d'ĂȘtre optimale. MĂȘme avec des textures « presque complĂštes », le rendu peut paraĂźtre dĂ©fectueux, car les matĂ©riaux de Quake sont composĂ©s de plusieurs couches. Une couche d'environnement manquante ou un mĂ©lange incorrect ne provoque pas une dĂ©gradation progressive ; le mĂ©tal se transforme instantanĂ©ment en vide.

La documentation doit accompagner le code source, et non pas ĂȘtre enfouie indĂ©finiment dans l'arborescence d'exĂ©cution. Les journaux d'Ă©tat et les audits de formatage sont prĂ©cieux lors du portage, mais deviennent une source de distraction une fois que l'on souhaite simplement lancer le jeu. Les sĂ©parer permet de prĂ©server l'intĂ©gritĂ© du dĂ©pĂŽt de code.

Ne faites pas l'autopromotion des étapes clés. PLAYABLE_COHERENT est une affirmation concernant une tranche. REFERENCE_CLOSE nécessite des regards sur l'étain et les portails. Les machines ne devraient pas valider le goût.

Conclusion

Le code source du moteur GPL de Quake III Arena est une vĂ©ritable mine d'or : une spĂ©cification complĂšte et explicite du fonctionnement d'un jeu de tir en arĂšne de la fin des annĂ©es 90. En porter une partie vers PHP, on a dĂ» lever le voile sur toutes les hypothĂšses sous-jacentes : systĂšmes de fichiers, cartes, matĂ©riaux, mouvements et mĂȘme l'interaction entre le langage et le GPU.

Le résultat n'est pas un produit qui remplace Quake III. C'est un laboratoire fonctionnel : php bin/quake3.php play contre un véritable baseq3, en parcourant q3dm0 avec la vitesse de saut de Quake toujours fixée à 270, le plasma demandant toujours l'autorisation de masquer le contenu du tir, et l'interface affichant toujours une faible santé clignotante sur une horloge de 256 ms car c'est ce que signifie (cg.time >> 8) & 1.

Si vous disposez de données de jeu légales, d'une chaßne d'outils PHP 8.5 et de SDL2, vous pouvez l'exécuter. Si les moteurs de jeu vous intéressent, vous pouvez aborder le code PHP comme on a abordé le C autrefois : non pas comme un tutoriel sur un framework, mais comme une cartographie des choix à faire.

C'était là le but. Non pas rendre PHP à la mode pour les jeux, mais prendre le moteur suffisamment au sérieux pour en retravailler les moindres recoins jusqu'à ce qu'une carte redevienne un lieu.

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

Connectez-vous pour réagir à cet article

🚀 1

Site

  • Plan du Site
  • Contact
  • Mentions lĂ©gales

Network

  • Hello
  • Blog
  • Apps
  • Photos

Social

Darkwood 2026, tous droits réservés