Retour aux projets

Levain

Un moteur de jeu 3D en C++23 construit avec Claude Code : je tranche l'architecture et la roadmap, je fixe les règles de relecture, et l'agent écrit le code, les tests et la documentation.

Mon rôle

  • Levain est construit avec Claude Code. L’agent écrit le code, les tests, la CI et la documentation. Je décide de l’architecture et du périmètre, je réponds à ses questions de conception et je relis. Les commits portent Claude en co-auteur ; les merge commits, faits depuis mon compte GitHub, ne portent que mon nom.
  • Décisions d’architecture : tout choix structurant passe d’abord par un ADR, que je valide avant son implémentation. Plus d’une trentaine, de NVRHI et flecs jusqu’à la forme de code que j’impose à l’agent (la logique en fonctions libres qui reçoivent toutes leurs dépendances en paramètres, la glu ECS en une seule ligne).
  • Roadmap et milestones : neuf phases (0 à 8), planifiées en heures de mon attention plutôt qu’en sessions de l’agent. Chaque milestone a un critère d’acceptation mesurable et se clôt par une release taguée dont les notes consignent le résultat mesuré, le plus souvent avec la commande qui l’a produit.
  • Règles de relecture et de vérification : une pull request doit se relire en 30 minutes ; zéro erreur de validation en Debug (NVRHI, couches Vulkan, couche de debug Direct3D 12) ; on ne désactive jamais un test, un warning ou un sanitizer pour faire passer quelque chose ; chaque chiffre s’accompagne de la commande ou du script versionné qui l’a produit.
  • Le workflow de l’agent : des skills versionnés dans le dépôt (session, build, pull request autonome, clôture de milestone, questions), chacun avec le journal des pièges déjà rencontrés. En mode autonome, un subagent relit chaque pull request avant que l’agent la fusionne ; je tranche par de courts sondages et je relis après coup.
  • Séparation moteur et jeu : le jeu, Rando, un court jeu d’exploration, vit dans son propre dépôt public et récupère le moteur à un commit figé. Son choix a remodelé la roadmap (terrain, eau et herbe, animation squelettique, caméra à la troisième personne, nage et planeur), et j’ai arbitré chaque ajout.

Ce que contient le moteur

  • Un backend WebGPU pour NVRHI, écrit dans le dépôt (NVRHI n’en a pas) à côté de ses backends Vulkan et Direct3D 12 d’origine. Le moteur se compile en WebAssembly : la démo en ligne est un renard qu’on mène à travers une vallée, dans un navigateur où WebGPU est activé (Firefox demande encore un réglage sous Linux).
  • Un build Windows compilé depuis Linux par clang-cl, lancé en Vulkan et en Direct3D 12 sur un portable RTX 4070, et testé en CI sous WARP et lavapipe.
  • Rendu : PBR en forward+ en clusters, ombres en cascades (4 cascades, PCF), HDR et tonemapping, éclairage d’environnement (IBL), frustum culling, et terrain par heightmap, eau et herbe instanciée sur GPU en plugins moteur.
  • Simulation : ECS flecs, boucle à pas fixe avec interpolation du rendu, input par actions, physique Jolt avec character controller.
  • Animation : ozz-animation derrière une passerelle glTF, skinning en compute shader, fondus entre clips.
  • Assets : import glTF, GUID dans des fichiers .meta qui survivent aux renommages, cuisson hors ligne (KTX2 avec un cache BC7), hot reload des shaders et des textures.
  • Éditeur : panneaux Dear ImGui, avec une hiérarchie et un inspecteur pilotés par la réflexion des composants : une déclaration par composant, les champs lus dans la struct elle-même, sans générateur de code dans le build.

Pourquoi c’était intéressant

L’essentiel du travail consiste à décider, pas à taper. L’agent expose les options et leurs compromis, je choisis, et le choix est écrit là où la session suivante le trouvera. Avant que le moteur passe dans le navigateur, un prototype jetable a mesuré WebGPU dans Firefox ; avant le retour de Windows dans la roadmap, un autre a mesuré clang-cl depuis Linux. Une décision coûte peu à prendre et cher à défaire : elle passe d’abord par des mesures.

Une autre règle vient de la phase 0 : trois garde-fous étaient restés verts sans rien vérifier. Depuis, un contrôle dont la condition n’est pas réunie doit échouer bruyamment, au lieu de simplement ne pas s’exécuter. L’agent l’applique désormais à chaque contrôle qu’il écrit.

Le processus se conçoit comme le code. Quand la CI tournait pour chaque pull request d’une pile et que l’attente se comptait en heures, j’ai décidé qu’elle ne tournerait plus qu’une fois par fonctionnalité, sur la pull request vers main, et que les pull requests cesseraient d’évincer les caches de main : un passage est tombé d’une heure ou plus à moins d’une demi-heure. Les phases 0 à 6 se sont closes à environ 54 heures de mon temps, pour 53 estimées.