Aller au contenu

Nooki

juillet 202614 min de lectureEn cours

Le jeu

Tout a commencé par un jeu entre amis. On voulait un petit univers à nous, un endroit où se retrouver pour jouer, discuter et faire des conneries ensemble, même quand on était loin les uns des autres.

On a d’abord joué à un jeu de ce genre, avec un vocal qui dépend de la distance : on s’approche de quelqu’un pour lui parler, et sa voix baisse quand on s’éloigne. Mais la carte était plate, il n’y avait pas d’intérieurs, pas de magasins, pas de vie autour. On n’y a joué que quelques jours, et ça m’a suffi pour avoir envie d’aller plus loin et d’y mettre ma touche. Pouvoir se faire son propre chez-soi, avoir une vraie vie à l’intérieur, mon monde à moi quoi.

Nooki, c’est ça. On crée une room, on envoie le code à ses amis, et tout le monde se retrouve sur la même île. Le jeu est en ligne sur nooki.kevin-dev.com. Le plus simple pour le présenter, c’est de raconter une soirée dessus.

Une soirée sur l’île

En arrivant, on reprend là où on s’était arrêté la dernière fois. D’abord, on cherche les autres. La touche M ouvre le plan de l’île, avec ses quartiers, les maisons à vendre et, à droite, qui est où. On clique sur un lieu, puis sur « Y aller », et le personnage s’y rend tout seul, à pied ou en voiture.

Le plan de l’île ouvert avec la touche M : les quartiers, les maisons à vendre et, à droite, qui est où.

Une fois ensemble, on s’entend comme dans le jeu de départ, plus ou moins fort selon la distance. Ensuite, chacun fait un peu ce qu’il veut : on pêche au bord de l’eau, on chasse les insectes, on revend ses prises, on meuble sa maison. Et quand on veut discuter à part, il y a les Bureaux, au Palais. Chaque pièce a son propre vocal, donc on peut se séparer en petits groupes, et on peut même se voir en vidéo dans une vue plein écran façon réunion Teams.

Les Bureaux vus d’en haut, un joueur dans chaque pièce, chacune avec son propre vocal.

L’île vit à l’heure de Paris, la même pour tout le monde. Quand on joue le soir, il fait donc nuit dans le jeu aussi. Le soleil tourne, les ombres suivent, puis les lampadaires, les phares et les fenêtres s’allument, et les habitants qui se baladaient en ville rentrent chez eux. En accéléré, au carrefour devant l’hôtel, ça donne ça :

Le carrefour devant l’hôtel en accéléré, de 15 h 40 à 21 h 30 : les ombres tournent, le soleil se couche, les lumières s’allument et les voitures passent.

C’est le moment d’aller au bar de la plage, qui est en fait une boîte de nuit. Léo, le DJ, s’occupe du jukebox. On lui donne un lien YouTube, la vidéo passe sur l’écran du mur et tout le bar l’écoute en même temps.

Le bar de la plage la nuit, avec Léo à ses platines et des joueurs sur la piste.

Ce qui se passe derrière

Vu de l’écran, tout ça a l’air simple. Mais une île peut accueillir dix joueurs, et ils doivent tous voir la même chose au même moment. C’est donc le serveur qui décide. Trente fois par seconde, il fait bouger les joueurs selon les touches qu’ils envoient, fait rouler les voitures et valide les achats. Quand j’appuie sur E devant une porte, mon navigateur envoie juste la touche, et le serveur regarde lui-même ce qu’il y a devant moi.

Le problème, c’est le délai. Si mon personnage attendait la réponse du serveur à chaque pas, le jeu paraîtrait mou. Le navigateur devine donc mes pas à l’avance, avec les mêmes règles que le serveur, et se recale quand la réponse arrive. Pour que les deux soient toujours d’accord, ces règles n’existent qu’à un seul endroit, dans un dossier partagé entre le serveur et le navigateur : la marche, les collisions, la portée de la touche E, l’aménagement d’une maison.

Ça se voit bien dans l’éditeur de la maison. Quand j’essaie de poser un canapé sur un autre, le refus s’affiche tout de suite, sans attendre le serveur, puisque c’est la même règle des deux côtés.

L’éditeur de la maison refuse un second canapé posé sur le premier : « La place est prise. »

La voix et la vidéo, elles, ne passent pas par mon serveur. Dès son arrivée, chaque joueur est relié directement à tous les autres en WebRTC, sans bibliothèque, et le serveur sert juste à les présenter. Ensuite, chacun calcule la distance lui-même. Il n’envoie rien à ceux qui sont trop loin, et il règle le volume de ceux qu’il entend. J’ai écarté deux autres pistes. Un serveur média, c’était un service de plus à héberger, alors que dix joueurs se relient très bien sans. Et ouvrir les connexions seulement quand on s’approche de quelqu’un laissait jusqu’à deux secondes de silence à chaque rencontre.

Le jukebox et les feux rouges se calent sur l’horloge de la room. Le serveur retient l’instant où le morceau a commencé, et chaque joueur lance la vidéo à la bonne seconde. Les feux changent au même moment chez tout le monde, sans qu’aucun message ne soit envoyé.

Côté technique, le serveur est un seul processus Bun, avec Colyseus pour le temps réel, Better Auth pour les comptes, SQLite et Drizzle pour les données. Le monde est dessiné avec Phaser 4, et tout ce qu’il y a autour (menus, fenêtres, boutiques) avec React 19 et Tailwind. Le tout tourne dans un conteneur sur mon VPS, sous Dokploy.

Trois versions

Cette architecture, je ne l’ai pas trouvée du premier coup. Nooki en est à sa troisième version.

La première, je l’ai faite en une journée, le 3 juillet. Une carte, le déplacement au clic, le multijoueur, les comptes et déjà la voix selon la distance, avec PixiJS et Elysia.

La deuxième a pris tout l’été. Je suis passé aux packs de LimeZu, et l’essentiel de mon temps est parti dans la carte, avec des générateurs de ville, des scripts pour poser les quartiers, et même un éditeur de carte fait maison (j’y reviens plus bas). À côté, chaque fonctionnalité avançait dans son coin, sur sa propre branche : le foot, le jukebox, les succès, le marché, les échanges, les photos, les montures.

Fin septembre, j’ouvrais le projet et je n’arrivais plus à rien relire. C’était du code spaghetti, sans règle ni direction, et je commençais moi-même à me lasser du jeu.

J’ai d’abord supprimé tous les générateurs de la carte. Leur outil de contrôle signalait des défauts partout sur une ville que j’avais retouchée à la main entre-temps, il ne servait plus à rien. Puis le 28 septembre, j’ai fusionné toutes les branches une dernière fois et j’ai tout supprimé, plus de 75 000 lignes. Je n’ai gardé que les sources de la carte et les images.

Cette fois, j’ai commencé par un cahier des charges. J’y ai noté toutes mes idées, et pour chacune ce que je voulais exactement. Repartir de zéro m’a permis de mieux rentrer dans le jeu, et de choisir vraiment ce que je voulais.

Voilà le carrefour du chantier dans la v2 :

Le carrefour du chantier dans la v2.

Et le même aujourd’hui, avec la nouvelle interface et une ville retouchée à la main :

Le même carrefour dans la v3, à midi.

Un code qu’on comprend du premier coup

Après la v2, je me suis donné une règle : quelqu’un qui ouvre le code doit le comprendre à la première lecture, sans connaître son histoire. Le dépôt reste privé pour l’instant, parce que certains éléments ne m’appartiennent pas et que je n’ai pas le droit de les partager.

Chaque choix important est noté dans un ADR, un petit document qui dit ce qu’on a décidé, comment ajouter quelque chose sur le sujet, et ce qu’on a écarté, comme les deux pistes pour la voix. Il y en a une vingtaine, de l’organisation du code jusqu’aux voitures des joueurs. Avant de toucher à la circulation, une session lit celui de la circulation, et elle ne retente pas une idée déjà abandonnée.

Trois jours après avoir tout recommencé, le jeu marchait, mais le code se lisait mal. J’ai lancé un grand ménage : des fichiers de moins de 200 lignes, une seule responsabilité par classe. Sauf qu’appliquée à la lettre, la règle a donné des classes qui ne gardaient rien et des fonctions avec trop de paramètres. À force de tout séparer, on avait ajouté de la complexité au lieu d’en enlever. Une grosse pull request a remis tout ça d’aplomb : ce qui interroge l’île est devenu une méthode de l’île, et un simple calcul, une fonction. Depuis, la règle c’est « pas de classe sans état à elle ».

Les tests ont eu le même traitement. Ils attendaient les vrais pas de l’île et hachaient un mot de passe à chaque inscription, si bien que la suite prenait trois minutes. Avec une fausse horloge, qui saute directement d’un événement au suivant, et sans hachage en test, elle est descendue à une trentaine de secondes, sans toucher au code du jeu.

Travailler avec Claude

Tout ce code, je ne l’ai pas tapé moi-même. Sur Paradise, j’avais laissé Claude Code tout écrire, et à la fin je ne savais plus comment le projet fonctionnait. Nooki est lui aussi entièrement écrit avec Claude, mais cette fois je le cadre. Tout ce que j’attends d’une session est écrit dans le dépôt, et chaque fois que je corrige quelque chose, ça devient une règle que la session d’après va lire.

Ce qui cadre une session Où c’est écrit
Ce qu’on construit, dans l’ordre Le cahier des charges, une couche à la fois. Une case se coche quand la fonctionnalité est faite et vérifiée
Comment on écrit le code Les consignes du dépôt, et les règles de lint pour ce qui se vérifie
Comment le code s’organise Les ADR, à lire avant de toucher à leur sujet
Comment on compose une carte Le skill de la carte
Quand c’est fini Lint, typage et tests au vert, puis vérification dans le navigateur ou sur une capture

Pour chaque tâche du cahier des charges, je fais écrire le prompt par Claude, avec les guides de prompting d’Opus 5.5. Chaque prompt part dans sa propre session, parfois plusieurs en même temps, puis la session me demande de relire. C’est là que je fais le plus de retours, sur la qualité du code, les commentaires ou un bout qui ne sert à rien. Pendant tout le développement, j’ai aussi répété la même consigne : « le jeu n’est pas en production, on peut tout casser ». Sans ça, une session prévoit des migrations pour des données qui n’existent pas.

Évidemment, ça ne marche pas à tous les coups. Des sessions ont recréé des classes inutiles malgré la règle écrite, juste parce que les fichiers d’à côté en avaient. Chaque brief rappelle maintenant les règles en premier, et demande à la session de relire son propre diff avant de committer. Deux fois aussi, Claude m’a annoncé un bug d’affichage corrigé sans l’avoir reproduit. Il n’apparaissait que dans un navigateur sans accélération graphique, comme le mien.

Des outils qu’on jette

Ce qui a le plus changé pour moi, c’est de pouvoir me faire un outil pour un besoin précis, en une soirée. Avant, ça m’aurait pris des semaines, et je ne me serais jamais lancé pour quelque chose qui ne sert que quelques jours. Maintenant je le fais faire, je m’en sers, et je le supprime quand je n’en ai plus besoin.

Une nuit de juillet, entre minuit et deux heures du matin, j’ai fait faire un éditeur de carte complet, avec le sol, les routes, les objets, les zones et le copier-coller. Le matin même, je dessinais une nouvelle île avec. Cinq jours plus tard, je l’ai supprimé pour revenir à Tiled. Entre-temps, j’avais aussi essayé de faire générer les quartiers par l’IA, avec une boucle où un modèle relit une capture du résultat. Une dizaine de quartiers sont sortis dans l’après-midi, et le soir même j’ai tout supprimé pour faire la ville à la main. Une journée m’a suffi pour savoir que ce n’était pas la bonne piste.

Les collisions ont été le plus gros chantier de ce genre. La plupart des graphismes viennent des packs de LimeZu, que j’ai achetés, et j’en profite pour dire un énorme merci à LimeZu pour tout ce travail, pour la qualité de ces packs, pour tout. Le reste des crédits du jeu est sur sa page des crédits. Pour chaque objet de ces packs, il fallait dire ce qui bloque le passage. Le jeu est vu de trois quarts, donc une maison n’occupe au sol que la place de son toit, posée au pied de la façade. Quand on passe derrière, la façade nous cache les jambes, et on ne rentre jamais dedans.

Deux maisons et leurs jardins, telles qu’on les voit dans le jeu.

En rouge, ce qui bloque vraiment sur ces mêmes maisons. Les fleurs, le feuillage et le haut des toits laissent passer, une clôture ou un lampadaire ne bloquent qu’à leur pied. Je ne voulais pas qu’un joueur reste coincé à cause d’un détail du décor.

Les mêmes maisons avec, en rouge, ce qui bloque le passage.

Pour ces masques, j’ai monté un workflow à deux étages : un premier modèle propose le masque d’un objet, un second cherche ce qui ne va pas. Des centaines d’agents ont tourné, et la vérification a rattrapé pas mal d’erreurs. J’ai ensuite tout relu, thème par thème, dans un petit outil fait pour l’occasion. L’outil a été supprimé avec la v2, mais les masques font partie des rares choses que la v3 a gardées.

D’autres outils restent. Quand Claude retouche une carte, il me montre ses rendus sur une page faite pour ça, avant et après. Je dessine dessus des flèches et des cadres numérotés, chacun avec sa remarque, et un bouton lui renvoie le tout. Les remarques qui revenaient souvent, comme un meuble qui se pose par son pied ou une clôture posée du mauvais côté, sont maintenant écrites dans un skill que Claude relit avant de toucher à une carte.

Le dessin et la musique

Ce qui m’a le plus surpris, c’est le dessin. Les fonds des pages hors du jeu ont été dessinés par Claude, pixel par pixel, chacun par un script qui pose ses calques un à un. Presque toutes les images que je lui ai demandées étaient vraiment réussies, alors j’en ai fait un diaporama : le fond montre la scène qui correspond à l’heure qu’il est sur l’île.

Quatre des fonds : les falaises au coucher, le village au crépuscule, la cascade à l’aube et la nuit.

La musique vient aussi de Claude, sans aucun fichier audio. Chaque morceau est une partition écrite en code, que le navigateur joue en direct avec l’API Web Audio. Claude a écrit le moteur et le premier morceau, puis des sous-agents ont composé les autres en parallèle, sans jamais pouvoir les écouter. L’harmonie est pensée sur le papier, et le volume de chaque instrument vérifié par des mesures. Sur les premiers morceaux, je n’ai presque rien eu à redire. Il y en a maintenant un pour chaque moment de la journée, plus un thème pour l’accueil, une valse lente pour l’hôtel et un petit air pour les magasins.

Ce qui reste à moi

Aujourd’hui, Claude peut à peu près tout faire : le code, les cartes, les dessins, la musique. Mais l’idée de départ et le choix final restent les miens. Pour l’hôtel, il m’a proposé cinq intérieurs, de l’Art déco au ryokan, et j’ai gardé le palace Belle Époque. Pour le plan de l’île, il m’a fait plusieurs designs, tous réussis, mais aucun ne collait au style du jeu. Au final, le plan montre simplement la vraie image de l’île, éclairée par le ciel du moment.

L’intérieur de l’hôtel, en palace Belle Époque.

Pour faire ces choix, il faut quand même connaître son métier : savoir quelles bibliothèques existent et quels sont leurs défauts, ce qui rend un code maintenable, comment héberger une appli, où se cachent les failles et comment s’en protéger. Sans ça, j’aurais peut-être eu le même jeu à l’écran, mais chaque modification en aurait cassé une autre, et le projet aurait été celui de Claude plus que le mien. Je n’ai jamais demandé « fais-moi un jeu où l’on se retrouve entre amis ». Chaque partie, je l’ai choisie de A à Z, et c’est pour ça que Nooki, je le considère vraiment comme le mien.

Où en est le projet

Nooki est en ligne depuis le 19 octobre, un peu plus de trois mois après la première ligne de code. Pour la suite, je reprends l’ordre du cahier des charges : les autres mini-jeux (foot, course, cache-cache, quiz), les magasins encore fermés, les montures, puis le partage d’écran et les échanges entre joueurs.

Technologies utilisées