151 lines
12 KiB
Markdown
151 lines
12 KiB
Markdown
|
|
# Programmation: Philosophie et pratiques universelles
|
||
|
|
|
||
|
|
Ce fichier est le point d'entrée pour comprendre mon approche de la programmation. Il explicite la philosophie qui guide mes décisions de code, les réflexes qui s'appliquent partout, et renvoie vers les fichiers spécialisés pour les détails.
|
||
|
|
|
||
|
|
Contexte : je suis développeur full-stack et développeur de jeux vidéo. Ces pratiques se sont forgées sur des projets commerciaux (Neoproxima chez Lonestone Studio, City Invaders, Manitou Lift and Drift), des projets indés (A Time Paradox, TheRuneMaker), du modding long-terme (mod Minecraft « Capsule », 8.6M+ téléchargements sur CurseForge) et sur les enseignements que je donne en parallèle (back-end, front-end, jeu vidéo, algorithmique). Des articles plus détaillés sur la plupart de ces sujets vivent sur [samuel-bouchet.fr](https://samuel-bouchet.fr) et les cours associés sur [teachings.samuel-bouchet.fr](https://teachings.samuel-bouchet.fr).
|
||
|
|
|
||
|
|
## Index des pratiques spécifiques
|
||
|
|
|
||
|
|
- [`csharp.md`](csharp.md) — idiomes C#, nullable reference types, async, naming, serialisation
|
||
|
|
- [`unity.md`](unity.md) — MonoBehaviour, asmdef, séparation Engine/Client, cycle de vie, éditeur
|
||
|
|
- [`reactive-state.md`](reactive-state.md) — Signal/Observable, ConnectedBehaviour, event sourcing, GameEvent
|
||
|
|
- [`code-review.md`](code-review.md) — revues, pull requests, commits, collaboration
|
||
|
|
- [`a-trier.md`](a-trier.md) — sujets qui ne sont pas encore rangés ailleurs (tests, Docker, documentation LLM-friendly, networking, etc.)
|
||
|
|
|
||
|
|
## Philosophie en une page
|
||
|
|
|
||
|
|
### 1. L'architecture est un outil de testabilité, pas une fin en soi
|
||
|
|
|
||
|
|
Une bonne architecture, c'est une architecture qui rend le code **testable sans ouvrir l'éditeur**. Concrètement, je sépare la logique pure de la présentation :
|
||
|
|
|
||
|
|
- La logique métier (`Engine/`) est un projet C# pur, sans aucune dépendance à l'éditeur (Unity, Godot, UE). Elle peut être compilée en netstandard, exécutée en ligne de commande, testée avec NUnit ou xUnit.
|
||
|
|
- La présentation (`Client/`) est éditeur (Unity, Godot, UE, CLI), réactive, passive. Elle lit l'état et émet des intentions, elle n'en mute jamais (sinon des états de pure présentation).
|
||
|
|
- Les tests ne se lancent pas dans l'éditeur, ils se lancent avec `dotnet test`. Si un bug n'est pas reproductible sans l'éditeur, c'est que la séparation est cassée.
|
||
|
|
|
||
|
|
Cette séparation permet de faire tourner un bot, un agent LLM, un replay, un serveur autoritaire, le tout avec exactement le même code de logique. Une logique qui doit booter un éditeur éditeur (Unity, Godot, UE) pour être exécutée est prisonnière et lente.
|
||
|
|
|
||
|
|
### 2. Source de vérité unique, explicite, documentée
|
||
|
|
|
||
|
|
Pour chaque donnée, je désigne une source de vérité :
|
||
|
|
|
||
|
|
- Les données statiques (définitions de runes, de blocs, de donjons) vivent dans des bases de données statiques ou des factories dans le code directement. (Unity only) J'évite les ScriptableObjects car ils ne sont pas utilisables dans un environnement hors éditeur (serveur, tests).
|
||
|
|
- L'état dynamique vit dans un `GameState` minimal (pas de redondance ou de donnée calculée), serialisable, monolithique, éventuellement observable (selon besoins du projet, Cf. `reactive-state.md`).
|
||
|
|
- Les données dérivées (UI, graphes rendus, animations) sont calculées à partir de l'état, jamais stockées à côté, et invalidées gràce à un système d'observabilité.
|
||
|
|
|
||
|
|
|
||
|
|
### 3. Mutations canalisées : command pattern + event sourcing
|
||
|
|
|
||
|
|
Mon approche rejoint le pattern **Black Box Sim** formalisé par Brian Cronin : la simulation ne sait rien de l'extérieur, elle reçoit des `WorldCommand`, mute son état, et émet des `WorldEvent`. Input → WorldCommand → Sim → WorldEvent → Présentation. Je l'utilisais déjà (Neoproxima, Manitou Lift and Drift, TheRuneMaker) mais le vocabulaire clarifie les discussions.
|
||
|
|
|
||
|
|
Je ne laisse jamais un composant muter l'état directement. Toute mutation passe par un événement typé `WorldCommand` qui :
|
||
|
|
|
||
|
|
- peut être validé avant application (`AssertApplicationConditions`),
|
||
|
|
- produit une trace (`WorldEvent` ou modification du state observable) qui doit contenir exhaustivement les infos nécessaires à la présentation,
|
||
|
|
- est atomique (un `lock` empêche d'appeler un `WorldCommand` dans un `WorldCommand`),
|
||
|
|
- est serialisable et donc transportable sur le réseau ou dans un save.
|
||
|
|
|
||
|
|
Si je peux écrire `state.Gold -= 10` depuis une UI, c'est que j'ai raté quelque chose. Ça doit être un `BuyFromShopWorldCommand` appliqué via `state.ApplyEvent(...)`. Voir [`reactive-state.md`](reactive-state.md) pour le détail.
|
||
|
|
|
||
|
|
|
||
|
|
### 4. Sécurité par défaut : le compilateur est mon allié
|
||
|
|
|
||
|
|
- **Nullable reference types activés partout** (`<Nullable>enable</Nullable>` ou `csc.rsp` avec `-nullable`). Chaque `?` et chaque `!` est une décision consciente, pas un oubli.
|
||
|
|
- **Readonly / immutable quand possible** : `readonly` sur les champs, `init` sur les propriétés, records pour les value objects.
|
||
|
|
- **Interfaces comme contrats** : `IGameEvent`, `INetworkMessage`, `IUpdatable<T>` ne sont pas là pour faire beau, elles sont là pour que le compilateur vérifie l'usage.
|
||
|
|
- **Attributs de validation runtime** pour ce que le compilateur ne peut pas vérifier : `[Required]` (Odin/Artificetoolkit) sur les références injectées via inspecteur, Odin Validator pour les scènes.
|
||
|
|
|
||
|
|
Je préfère une erreur à la compilation qu'une erreur au lancement, et une erreur au lancement qu'une erreur en production.
|
||
|
|
|
||
|
|
### 5. Explicite > implicite
|
||
|
|
|
||
|
|
C'est une règle générale qui se décline partout :
|
||
|
|
|
||
|
|
- **Nommer pour être lu** : un nom long et clair vaut mieux qu'un nom court et cryptique. `PickNecklaceStringGameEvent` est verbeux mais ambigu pour personne.
|
||
|
|
- **Conventions strictes et visuelles** : public `CamelCased`, privé `_camelCased`. L'underscore est un signal visuel de "interne, ne pas toucher depuis l'extérieur".
|
||
|
|
- **Documenter les intentions, pas les mécaniques** : le code dit *quoi*, le commentaire dit *pourquoi*. XML doc (`///`) sur les APIs publiques, commentaires en prose sur la logique métier non triviale.
|
||
|
|
- **Pas de magic numbers, pas de magic strings** : `const` nommées, enums, factories.
|
||
|
|
- **État explicitement observable** : un champ `Signal<T>` est plus clair qu'un champ + un événement `OnChanged` à câbler à la main.
|
||
|
|
|
||
|
|
### 6. Lisibilité et simplicité priment
|
||
|
|
|
||
|
|
Extrait de mes propres guidelines : *"Code should be as simple as possible, performant, robust, and readable."* Dans cet ordre, avec une subtilité : **simple et lisible d'abord**, performant quand c'est mesuré, robuste par construction.
|
||
|
|
|
||
|
|
Concrètement :
|
||
|
|
|
||
|
|
- Je préfère `public string Name;` à `public string Name { get; set; }` quand les accessors n'apportent rien.
|
||
|
|
- J'évite les abstractions prématurées. Une interface qui n'a qu'une seule implémentation est une interface à supprimer, sauf si elle existe pour la testabilité ou le mock.
|
||
|
|
- J'évite les patterns "à la Java" (abstract factory abstract builder, etc.). Un constructor et un `static readonly` vont plus loin 90% du temps.
|
||
|
|
- Je garde les fichiers courts, les classes responsables d'une chose, les méthodes qui tiennent à l'écran.
|
||
|
|
|
||
|
|
### 7. Déterminisme pour pouvoir déboguer
|
||
|
|
|
||
|
|
Un bug qu'on ne peut pas reproduire est un bug qu'on ne peut pas corriger. Je construis mes systèmes pour qu'à entrée identique, la sortie soit identique :
|
||
|
|
|
||
|
|
- **RNG explicite et seedée** : le RNG est passé dans les fonctions, jamais un `Random.Range` global. Les logs et les replays exposent la seed.
|
||
|
|
- **Ordre de résolution déterministe** : tri topologique d'un DAG (runes), tick ordonné (combat), pipeline à étapes nommées (`RECEIVE → GENERATE → TRANSFORM → AMPLIFY`).
|
||
|
|
- **Event sourcing** : rejouer la liste d'événements depuis le début doit reproduire exactement le même état.
|
||
|
|
- **MessagePack avec Union** pour le versioning : une sauvegarde d'il y a 6 mois doit pouvoir être rechargée sans perdre de données.
|
||
|
|
|
||
|
|
### 8. Le bon outil pour la bonne couche
|
||
|
|
|
||
|
|
Je ne suis pas dogmatique sur la techno, mais j'ai des choix qui s'enchaînent bien :
|
||
|
|
|
||
|
|
- **UniTask** plutôt que `Task` côté Unity — zéro-allocation, intégration propre avec le PlayerLoop, cancellation via `GetCancellationTokenOnDestroy()`.
|
||
|
|
- **MessagePack** pour la sérialisation — binaire, rapide, supporte Union pour le versioning.
|
||
|
|
- **TinkStateSharp** (`Signal`, `SignalList`, `SignalDictionary`) pour l'état réactif — observables typées, `AutoRun` pour auto-subscribe.
|
||
|
|
- **DOTween Pro** pour le tweening d'animations, combiné à TinkStateSharp dans le pattern « animation queue » décrit dans [`reactive-state.md`](reactive-state.md).
|
||
|
|
- **Moq + Castle.Core** pour les mocks en tests unitaires (voir [`csharp.md`](csharp.md) — c'est le setup que j'utilise pour Neoproxima).
|
||
|
|
- **Odin / Artificetoolkit** pour la validation éditeur.
|
||
|
|
- **Docker** pour déploiement serveur, EntityFramework Core pour la DB, .NET 6+ pour le serveur, netstandard 2.1 pour la couche partagée.
|
||
|
|
- **Unity Cloud Build + fastlane** quand iOS est une cible (évite le Mac dédié) — voir [`a-trier.md`](a-trier.md).
|
||
|
|
- **Rider** comme IDE — les `.DotSettings` versionnés encodent les conventions d'équipe.
|
||
|
|
|
||
|
|
Le point commun : chaque outil gère bien son rôle et s'arrête là.
|
||
|
|
|
||
|
|
### 9. Documentation qui suit le code
|
||
|
|
|
||
|
|
Je documente **dans le dépôt**, pas ailleurs. Typiquement :
|
||
|
|
|
||
|
|
- Un `README.md` pour le démarrage et la structure.
|
||
|
|
- Un `CLAUDE.md` qui décrit l'architecture pour un agent (humain ou LLM) qui débarque : où sont les sources de vérité, quels sont les patterns clefs, comment lancer les tests, quelles sont les conventions. Ce fichier est aussi de la doc pour moi dans 6 mois.
|
||
|
|
- Un `GDD.md` / `GAME_DESIGN.md` pour les décisions de gameplay.
|
||
|
|
- Des diagrammes Mermaid inline pour les state machines et les flux — ils vivent dans le markdown, ils se mettent à jour quand le code change.
|
||
|
|
- Des specs de protocole (CLI LLM, format de messages) écrites noir sur blanc.
|
||
|
|
|
||
|
|
**Principe** : si un nouveau développeur (ou LLM) ne peut pas comprendre l'architecture en 15 minutes avec le `CLAUDE.md`, c'est le `CLAUDE.md` qui est à revoir, pas le développeur.
|
||
|
|
|
||
|
|
### 10. Penser accessibilité par les agents
|
||
|
|
|
||
|
|
Depuis quelques projets, je conçois en gardant en tête qu'un LLM peut avoir à lire, modifier, ou piloter mon code :
|
||
|
|
|
||
|
|
- Les `CLAUDE.md` sont écrits pour les agents autant que pour les humains.
|
||
|
|
- Quand un jeu peut tourner en headless (`LlmRunner`), je l'expose via un protocole stdin/stdout bien délimité (`---STATE---`, `---ACTIONS---`, `---PROMPT---`).
|
||
|
|
- Les logs sont structurés et utiles, pas du spam.
|
||
|
|
- Les seeds, les timeouts, les limites d'actions sont explicites pour que l'exécution soit reproductible et bornée.
|
||
|
|
|
||
|
|
C'est un bon forçage : ce qui est lisible par un LLM est lisible par un humain.
|
||
|
|
|
||
|
|
## Les réflexes au quotidien
|
||
|
|
|
||
|
|
Quelques questions que je me pose avant de commit :
|
||
|
|
|
||
|
|
- **Est-ce que ça passe sans Unity ?** Si la logique dépend d'un `GameObject`, est-ce qu'elle devrait ?
|
||
|
|
- **Est-ce que ça mute l'état directement ?** Si oui, ça devrait être un GameEvent.
|
||
|
|
- **Est-ce qu'il y a un nullable warning supprimé sans raison ?** Si oui, c'est une dette.
|
||
|
|
- **Est-ce que le nom reflète l'intention ?** Si j'hésite à le lire à voix haute, c'est mauvais signe.
|
||
|
|
- **Est-ce que je duplique une source de vérité ?** Un `state.Foo` plus un champ local qui le cache = bug potentiel.
|
||
|
|
- **Est-ce qu'il y a un test qui prouve que ça marche ?** Si non, est-ce que je peux en écrire un sans lancer Unity ?
|
||
|
|
- **Est-ce qu'un LLM pourrait comprendre ce fichier tout seul ?** Si non, manque-t-il un commentaire, un lien, un doc ?
|
||
|
|
|
||
|
|
## Les anti-patterns que je refuse
|
||
|
|
|
||
|
|
- Les mutations d'état depuis la couche présentation.
|
||
|
|
- Les singletons statiques mutables (hors DI/factories contrôlées).
|
||
|
|
- Les `Find*` appelés en boucle dans `Update()`.
|
||
|
|
- Les tests qui dépendent de l'éditeur Unity quand ils pourraient être purement C#.
|
||
|
|
- Les `#if UNITY_EDITOR` dans la logique métier (tolérés dans du code de debug/assertion).
|
||
|
|
- Les commentaires qui paraphrasent le code au lieu d'expliquer le pourquoi.
|
||
|
|
- Les classes "Manager" fourre-tout.
|
||
|
|
- Les tâches async sans cancellation token quand elles survivent à un lifecycle.
|
||
|
|
- Les breaking changes silencieux sur un format sérialisé (saves, réseau) — il faut versionner.
|