L'architecture est en place. Maintenant, la question clé : comment 100 000 agents prennent-ils des décisions individuelles chaque frame ? Cette étude explore les algorithmes de simulation : flocking, partitionnement spatial, et les 15 forces qui gouvernent chaque poisson.
Algorithmes de Simulation Multi-Agent & Modélisation d'Écosystème
Comment se comportent 100 000 poissons ? Cette étude explique les algorithmes qui donnent vie au banc : trouver ses voisins, nager ensemble, fuir les prédateurs et interagir avec l'environnement.
Architecture multi-agent complète avec grille spatiale compressée, forces Reynolds étendues, champ de potentiel avec diffusion, et pipeline en phases découplées. Profilé et mesuré empiriquement.
🗺️ 2.1 Partitionnement Spatial : Grille Uniforme CSR + Quadtree Adaptatif
⚙️ Le problème : 14 milliards de comparaisons
Le problème majeur : chaque poisson doit connaître ses voisins proches. L'approche naïve compare chacun avec tous les autres — soit 14 milliards de comparaisons pour 100K poissons. Intenable.
La solution : découper le monde en une grille spatiale. Chaque poisson est rangé dans la case correspondant à sa position. Pour trouver ses voisins, il suffit de regarder les 9 cases adjacentes. Le temps de calcul passe de 10 minutes à 3 millisecondes — pour le même résultat. En entreprise, c'est le même principe que les index de base de données : au lieu de chercher partout, on va directement au bon endroit.
🧪 Le format CSR et pourquoi c'est rapide
Le format CSR (Compressed Sparse Row) est standard dans les bases de données columnar (ClickHouse, Apache Arrow). Le principe : au lieu de listes de listes, on utilise 2 tableaux contigus — un pour les données, un pour les offsets. Résultat : parcours séquentiel = cache-friendly.
Le tri par radix (en O(N) !) remplace le sort natif de JS (O(N log N)) et fonctionne sans allocations. C'est le genre d'optimisation qui fait la différence entre 60 FPS et 30 FPS à grande échelle.
Avec grille spatiale : O(N × K) où K = boids/cell ≈ 8-15
Réduction : ~10⁹× plus rapide
Grille Uniforme CSR
Monde divisé en cellules de taille fixe. 6 indices indépendants (boids, food, predators, obstacles, refuges, seaweed) avec dirty-cell tracking. Prefix sums O(dirty) au lieu de O(totalCells). Le seuil 30% décide entre scan linéaire et sweep trié.
Quadtree Sparse Adaptatif
Subdivision dynamique : seules les régions peuplées existent. Depth max 5 (cellules de 625px à monde 20Kpx). Near viewport: depth forcée 4. Far: depth 1 (macro cells 10Kpx). SoA NodePool avec free-list, zéro allocation.
// Lifecycle per frame (SpatialGrid):
1. clear() — reset only dirty cells O(dirty)
2. insertIdx() — accumulate stats + cache boidCell[i]
3. buildCellIndex() — prefix sums + fill flat CSR buffer
4. finalizeCellStats() — coherence, cluster/individual decision
5. query*() — spatial lookups during flocking
// Per-race stats enable race-aware flocking (same O):
cellRaceVxSum[ci * NUM_RACES + raceIdx] += vx;
🐟 3.1 Comportement de Banc en Détail3.1 Modèle de Flocking Étendu (Reynolds++)
⚙️ 15 règles simples pour un comportement réaliste
Chaque poisson suit 15 règles comportementales : ne pas se rentrer dedans, suivre la direction du groupe, rester groupé, fuir les prédateurs, chercher de la nourriture... Individuellement, ces règles sont simples. Mais combinées sur 100 000 poissons, elles produisent des bancs réalistes qui se forment, éclatent et se reforment — exactement comme dans la nature.
Chaque force est pondérée et les résultats sont plafonnés pour éviter les comportements aberrants. Le tout tourne en moins de 3ms pour 10 000 boids.
🧪 Le modèle Reynolds en production
Craig Reynolds (1987) a défini 3 forces : séparation, alignement, cohésion. En production, il en faut beaucoup plus. Ici on en utilise 15 : les 3 de base + fuite prédateur, attraction nourriture, courant marin, frontière monde, etc.
La difficulté : les poids relatifs. La fuite prédateur doit toujours gagner sur la cohésion, sinon les poissons nagent vers le requin. J'ai passé des heures à tuner ces poids — c'est plus de l'artisanat que de la science.
| Force | Formule | Complexité | Skip Rate |
|---|---|---|---|
| Séparation | Σ (pos - voisin) / dist² | O(K) | — |
| Alignement | avg(v⃗voisins) - v⃗self | O(K) ou O(1) cluster | — |
| Cohésion | centroid(voisins) - pos | O(K) ou O(1) cluster | — |
| Fuite prédateur | -Σ dir(pred) × (1/dist) | O(P) | reactionN |
| Recherche nourriture | dir(foodnearest) × hunger | O(F/cell) | reactionN |
| Draft (aspiration) | dir(leader) si cos(angle) > θ | O(K) | — |
| Courant marin | bilinear_sample(flowField) | O(1) | perfFlowSkip |
| Champ potentiel | ∇Φ(x,y) × tierWeight | O(1) | — |
| Formation | Modèle configurable par race | O(1) | — |
💡 Optimisation : Frame Staggering Biologique
Les poissons ne vérifient prédateurs et nourriture que 1 frame sur N (reactionN = 2-12 selon population). Ce n'est pas un hack — c'est biologiquement réaliste ! Les vrais poissons ont un temps de réaction de ~50-200ms. Gain : -66% de scans par frame.
Inspiré par la nature
Ce modèle a été inventé en 1987 par Craig Reynolds. Il est utilisé dans les films (Le Monde de Nemo, Le Seigneur des Anneaux) pour animer des foules réalistes. Ici, on l'a étendu avec 15 comportements différents.
🧲 3.2 Guidage Intelligent des Poissons3.2 Champ de Potentiel pour Navigation O(1)
⚙️ Une carte invisible qui guide les poissons
Comment guider 100 000 poissons sans que chacun doive scanner tout le monde ? Une carte invisible indique les zones de nourriture et de danger. Chaque poisson lit simplement la pente locale pour savoir où aller — comme suivre la pente d'une colline.
Chaque poisson lit le gradient (la pente) du champ à sa position pour savoir où aller. C'est comme suivre une pente : on descend vers la nourriture, on monte pour fuir. Cette technique réduit la navigation à un calcul O(1) par poisson, au lieu de parcourir tous les points d'intérêt du monde.
🧪 Le gradient en pratique
Le "gradient" ici est simple : on regarde la valeur à gauche et à droite d'un poisson sur la grille. Si la valeur est plus grande à droite, le poisson va à droite. C'est une différence finie, pas besoin de maths avancées.
L'important en production : le gradient peut avoir des discontinuités aux bords de la grille. Il faut les gérer proprement (conditions de bord), sinon les poissons se coincent dans les coins. Bug classique que j'ai vu dans 3 projets différents.
∇Φ = (Φ(x+1,y)−Φ(x−1,y), Φ(x,y+1)−Φ(x,y−1)) / 2
F⃗nav = α·∇Φattract − β·∇Φrepulse
Sparse Clear
Seules les cellules modifiées (dirty tracking) sont réinitialisées chaque frame, pas les N×M cellules.
Diffusion Throttled
Box blur 3×3 sur les couches d'attraction/répulsion, exécuté 1 frame sur 4 pour lisser les gradients.
Intégrale Temporelle
Accumulation exponentielle : les boids Tier 2 lisent un gradient moyenné dans le temps, plus stable.
Une carte GPS invisible pour les poissons 🗺️
Chaque poisson a un "GPS" invisible qui lui dit : "la nourriture est par là" et "le danger est par ici". Pas besoin de voir les autres poissons — la carte suffit. C'est comme suivre les odeurs dans l'eau.
💼 Même principe que le guidage d'une app
Ce système de navigation est identique à ce qu'utilise un robot aspirateur pour éviter les obstacles, ou une app GPS pour trouver le chemin le plus court. Le principe de "champ de potentiel" est un standard de la robotique.
🧬 3.3 Génétique Paramétrique & Spéciation Dynamique
Chaque espèce possède un génome de 28 gènes continus [0,1] encodant la morphologie (corps, nageoires, tentacules) et les patterns visuels. La reproduction inter-espèces déclenche un algorithme de spéciation avec crossover, mutation et détection de similarité.
distance(GA, GB) = √(Σ(GA[i] − GB[i])²)
Si similarity(candidate, existing) > 0.85 → absorption (pas de nouvelle espèce)
| Catégorie | Gènes (indices) | Exemples |
|---|---|---|
| Morphologie Corps | 0-14 | bodyElongation, headPointiness, tailForkDepth, dorsalHeight, jawProminence |
| Appendices | 15-22 | finRayLength, tentacleCount, shellCoverage, clawSize, antennaeLength |
| Patterns Visuels | 23-27 | patternType (stripes/spots/rings/…), patternScale, patternContrast, accentHue |
🧬 Résultat : 10 000+ Noms Uniques
Le système de nommage combine préfixes taxonomiques (Neo-, Proto-, Archi-), suffixes latins (-oid, -ella, -opsis), et combinaisons uniques curées (Requin+Poulpe = "Krakenodon"). Slot recycling des espèces éteintes.
L'ADN des poissons virtuels 🧬
Chaque espèce a son propre "code génétique" : 28 caractéristiques numériques qui définissent sa taille, sa couleur, sa vitesse, la forme de sa queue… Quand deux poissons se reproduisent, le bébé hérite d'un mélange des deux parents — avec parfois des mutations surprises !
�� 3.4 Simulation de Fluides : Flow Field & Température
⚙️ Courants marins et température
Le monde possède deux champs superposés : un champ de vélocité (courants marins qui poussent les poissons) et un champ de température (zones chaudes près des rifts, froides en surface). Chaque espèce a une température optimale et cherche naturellement à y rester.
Ces champs se propagent par diffusion. Les poissons lisent simplement la valeur à leur position et ajustent leur trajectoire. La grille fait 64×36 cases, assez précise pour guider sans coûter cher.
🧪 La diffusion en pratique
La diffusion (box blur) est une technique standard en traitement d'image. Le même filtre 3×3 est utilisé dans Photoshop pour le flou gaussien. Ici, on l'applique à la grille de potentiel pour que les signaux se propagent naturellement.
Piège classique : appliquer le blur in-place modifie les données pendant qu'on les lit → résultat incorrect. Il faut un double buffer (lire de A, écrire dans B, puis swap). C'est le même pattern qu'en rendu GPU (double buffering).
Gyres Atmosphériques
Tourbillons permanents avec position, rayon et force configurables. Appliquer un champ circulaire tangentiel sur les cellules voisines.
Interpolation Bilinéaire
Échantillonnage aux 4 coins de la cellule avec pondération (1-tx)(1-ty), évitant les artefacts de marche entre cellules.
Diffusion Thermique
Transfert de chaleur par convolution 3×3, avec zones thermiques saisonnières et modificateurs dynamiques (sorts, heaters).
Des courants marins invisibles 🌊
L'eau n'est pas immobile ! Des courants poussent les poissons dans différentes directions, comme des tapis roulants invisibles. La température varie aussi : les zones chaudes attirent certaines espèces, les zones froides en repoussent d'autres.
⚙️ 3.5 Les 8 Étapes de la Simulation3.5 Pipeline de Simulation en 8 Phases
⚙️ 8 étapes, 60 fois par seconde, sans accroc
Le moteur recalcule tout 60 fois par seconde. Chaque calcul suit 8 étapes dans un ordre précis — comme une chaîne d'assemblage automobile où chaque poste fait une tâche spécifique.
L'ordre est critique : les prédateurs bougent avant le flocking pour que les proies réagissent au danger actuel. Chaque phase est un module JS isolé (~200 à 1000 lignes), testable et profilable indépendamment. Le pipeline complet tourne en moins de 8ms à 10K boids.
🧪 Le pipeline en pratique
Le pipeline est séquentiel par nécessité : la phase de flocking a besoin des résultats de la grille spatiale. On ne peut pas les paralléliser. En revanche, les calculs internes à chaque phase sont parallélisables (un jour avec SharedArrayBuffer + Workers).
Le pattern "pipeline de stages" est identique à ce qu'on trouve dans les systèmes ECS (Entity Component System) des moteurs de jeu modernes. Même Unity est passé à ce modèle avec DOTS. C'est prouvé en production.
Phase 1 — Timers & Cooldowns
Décrément des timers de reproduction, sorts, états. Scheduling d'événements.
Phase 2 — Objets Dynamiques
Update nourriture, pièges, filets, vortex, obstacles. Spawning/despawning.
Phase 3 — Environnement
Flow field, température, cycle jour/nuit, saisons. Shockwaves, tsunamis.
Phase 4 — Auto-Balance Écosystémique
Ratio prédateur/proie → ajustement dynamique spawn, énergie, reproduction.
Phase 5 — Biologie & Grid Rebuild
Aging, énergie, satiété, mort naturelle, gestation. Rebuild SpatialGrid CSR.
Phase 6 — Prédateurs
IA de chasse, territoralité, frenzy mode. Met à jour AVANT flocking.
Phase 7 — Flocking (cœur)
Cluster → Individual → Deferred → PostPhysics. 4 sous-modules, ~70K LOC total.
Phase 8 — Post-Processing
Stats, nettoyage, reproduction spawn, événements, spatial grid rebuild food.
Ce qu'il faut retenir
14 milliards de comparaisons réduites à quelques milliers grâce au spatial hashing. 15 forces combinées pour un comportement réaliste. Un pipeline en 8 phases qui organise tout comme une chaîne de production industrielle — le tout en moins de 8ms.
Une chaîne de montage en 8 étapes ⚙️
À chaque image affichée, le moteur exécute 8 étapes dans l'ordre : trouver les voisins → calculer les forces → bouger les poissons → gérer l'écosystème → dessiner. Comme dans une usine, chaque poste fait son travail et passe le relais au suivant.
Les poissons savent nager en groupe. Mais comment les guider vers la nourriture, les éloigner des dangers, et créer des courants marins — sans scanner chaque entité ? Le champ de potentiel est une carte invisible qui orchestre tout cela en O(1).
Champs de Potentiel & Navigation Intelligente
Comment des milliers de poissons évitent-ils les requins et trouvent-ils la nourriture sans se téléphoner ? 📱 Grâce à une carte invisible qui leur dit "ici c'est dangereux" et "là-bas il y a à manger" !
Champ de potentiel inspiré de la robotique (Khatib 1986). La grille 64×36 = compromis précision/performance. Le blend progressif flocking→guidage est la clé de la scalabilité. 8 couches de signaux empilables indépendantes.
🧲 4.1 Architecture du Champ de Potentiel
⚙️ Un GPS qui coûte presque rien en calcul
Le problème classique : si chaque poisson devait scanner l'océan pour trouver la nourriture et repérer les prédateurs, le coût serait astronomique à 100K entités.
La solution : une carte invisible de 64×36 cases. Les sources de danger (requins, obstacles) et d'attraction (nourriture, refuges) "émettent" un signal qui se diffuse dans les cases voisines. Chaque poisson lit simplement la pente locale de cette carte pour savoir où aller.
📊 Résultat business : navigation de 100K entités avec un coût quasi-nul. C'est le même principe que les CDN (Content Delivery Network) — au lieu de demander au serveur central, on lit la copie la plus proche.
🧪 Pourquoi la grille 64×36
La résolution de la grille (64×36) est un compromis. Plus fin = plus précis mais plus lent. Plus grossier = plus rapide mais les poissons "sentent" les cases. À 64×36, chaque case fait ~30px — assez fin pour un guidage naturel, assez gros pour une mise à jour rapide.
C'est le même dilemme que la résolution de texture en jeu vidéo : 4K est beau mais coûteux. Ici, "basse résolution" suffit car les poissons lissent le résultat par leur mouvement continu.
| Source | Type | Poids | Rayon | Analogie |
|---|---|---|---|---|
| 🍖 Nourriture | Attraction | 1.0 | 3 cells | ⟹ Produit populaire sur un marketplace |
| 🏠 Refuge | Attraction | 2.0 | 5 cells | ⟹ Zone de sécurité réseau |
| 🦈 Prédateur | Répulsion | 3.0 | 4 cells | ⟹ Menace de sécurité détectée |
| 💥 Shockwave | Répulsion | 5.0 | 6 cells | ⟹ Alert critique système |
| 🪨 Obstacle | Répulsion | ∞ | 1 cell | ⟹ Hard limit / firewall |
| 🌡️ Zone chaude | Attraction sélective | 0.8 | 4 cells | ⟹ Feature flag par segment |
Une grille magique sous la mer ✨
Imaginez une grille invisible de 64×36 cases posée au fond de l'océan. Chaque case contient un chiffre : positif = "viens ici", négatif = "fuis". Les poissons "lisent" leur case et nagent en conséquence. Simple mais très efficace !
2 304 cases pour guider 100 000 poissons
La grille ne fait que 64 × 36 = 2 304 cases. Chaque poisson regarde juste 1 case. Pas besoin de calculer les distances avec tous les autres !
🐠 4.2 Interaction Flocking × Potentiel par Tier
⚙️ L'astuce qui économise 80% du calcul
Les 100K poissons ne font pas tous le même travail. Le moteur les classe en 3 niveaux :
🔴 Proches (20%) — Calcul complet : 15 forces de navigation individuelle
🟡 Moyens (30%) — Calcul simplifié : moyennes par cluster + carte
🟢 Lointains (50%) — Carte uniquement : un seul lookup O(1)
En entreprise, c'est exactement le principe du tiered caching : données chaudes en RAM, tièdes en SSD, froides en archive. Le résultat perçu est identique, le coût est divisé par 5.
🧪 Le blend en pratique
Le blend tier0/tier1/tier2 (15% / 50% / 90% champ de potentiel) est tuné à l'œil. Les valeurs théoriques "optimales" n'existent pas — ça dépend du comportement visuel recherché. J'ai itéré pendant des jours pour trouver le bon feeling.
Le pattern de blend linéaire fonctionne bien ici. Dans d'autres projets, j'ai dû utiliser des courbes ease-in/ease-out pour que la transition soit invisible. L'important : que le joueur ne remarque JAMAIS le changement de LOD.
Poissons proches
Suivent les 3 règles naturelles (séparation, alignement, cohésion) — le plus réaliste
Poissons lointains
Suivent la carte invisible — moins cher à calculer, tout aussi convaincant
💼 Le secret de l'extensibilité
C'est cette transition intelligente qui permet de passer de 1 000 à 100 000 poissons sans changer l'expérience utilisateur. Les poissons proches ont le comportement réaliste, les lointains sont plus simples. L'utilisateur ne voit aucune différence.
🌊 4.3 Propagation des Signaux
⚙️ Propagation intelligente à coût minimal
Le signal de danger d'un requin ne se propage pas instantanément — il se diffuse progressivement dans les cases voisines, comme un message qui se transmet de bouche à oreille.
Pour économiser du calcul, cette diffusion ne se fait que 1 frame sur 4. Résultat : un signal qui atteint les poissons à 5+ cases de distance, avec un coût de seulement ~0.2ms. Les poissons réagissent de manière coordonnée, créant des mouvements de foule spectaculaires.
🧪 Vitesse de propagation
Sans throttling, le blur converge en ~20 itérations (la diagonale de la grille). Avec un blur toutes les 3 frames, ça prend ~60 frames (~1 seconde). C'est impceptible pour le joueur car les sources sont déjà perceptibles localement.
Si les sources apparaissent trop lentement, augmenter le rayon initial d'influence (scatter radius) est plus efficace que d'augmenter la fréquence du blur. C'est un truc qu'on apprend en optimisant des simulations de particules.
au requin réagissent
sur 5+ cellules de rayon
Comme une goutte d'encre dans l'eau 🎨
Quand on place de la nourriture, le signal ne se propage pas instantanément. Il se diffuse lentement autour de sa source, comme une goutte d'encre dans un verre d'eau. Plus on est loin, plus le signal est faible.
⚖️ 4.4 Les 15 Forces de Navigation
⚙️ 15 règles simples = comportement complexe
Chaque poisson combine 15 "forces" qui le poussent dans différentes directions. Individuellement, ces forces sont triviales ("s'éloigner du danger", "suivre le groupe"). Mais combinées sur 100 000 poissons, elles produisent des comportements de groupe spectaculaires — formation en V, éclatement face au prédateur, migration saisonnière.
C'est le même phénomène qu'on observe dans la vraie nature, et c'est aussi le principe des systèmes multi-agents utilisés en logistique et en IA.
🧪 L'architecture globale du champ
Le champ de potentiel avec 8 couches (nourriture, danger, refuge, courant, température, profondeur, banc allié, zone de reproduction) est inspiré des architectures robotiques (Khatib 1986). En robotique, c'est le standard pour la navigation autonome.
L'avantage clé : chaque couche est indépendante. On peut ajouter une 9ème couche (ex: zones de peur) sans modifier le reste du code. C'est de la vraie extensibilité architecturale.
| # | Force | Source | Comportement Émergent |
|---|---|---|---|
| 1 | Séparation | Voisins | Anti-collision, espacement naturel |
| 2 | Alignement | Voisins/Cluster | Bancs synchronisés |
| 3 | Cohésion | Voisins/Cluster | Regroupement en bancs |
| 4 | Fuite prédateur | SpatialGrid | Dispersal, évasion |
| 5 | Recherche food | SpatialGrid | Convergence vers nourriture |
| 6 | Draft (aspiration) | Voisins | V-formation, file indienne |
| 7 | Courant marin | FlowField | Dérive passive naturelle |
| 8 | Obstacle avoid | SpatialGrid | Contournement fluide |
| 9 | Edge repulsion | World bounds | Confinement au monde |
| 10-11 | Potentiel ± | PotentialField | Navigation macro |
| 12 | Température | FlowField | Migration thermique |
| 13 | Mate attraction | Flags | Rapprochement reproduction |
| 14 | Vortex | Effects | Aspiration tourbillon |
| 15 | Formation | Race config | Patterns spécifiques espèce |
15 envies en même temps !
Chaque poisson ressent 15 forces simultanées : rester avec le groupe, fuir le prédateur, chercher la nourriture, suivre le courant… Le moteur fait un "vote" entre toutes ces envies pour décider où nager. La fuite est toujours prioritaire ! 🏃♂️
Ce qu'il faut retenir
Une carte invisible de 2304 cases guide 100 000 poissons en O(1) par entité. 15 forces de navigation se combinent pour produire des comportements collectifs spectaculaires — formation, dispersion, migration — sans aucun "chef d'orchestre". Le coût total : moins de 1ms pour l'ensemble du système.
Les poissons sont visibles, intelligents, et guidés. Mais un aquarium sans écosystème, c'est juste des points qui bougent. Cette étude ajoute la couche biologique : énergie, faim, reproduction, prédation. Le résultat : un monde vivant qui s'auto-régule.
Simulation d'Écosystème Vivant : Physique, Comportement & Équilibre Dynamique
C'est comme un vrai aquarium vivant ! 🐠 Les poissons naissent, mangent pour avoir de l'énergie, grandissent, font des bébés... et finissent par mourir. Les requins chassent les petits poissons, qui fuient en banc. Et le plus fou : personne ne contrôle rien — l'équilibre se crée tout seul, comme dans la nature ! 🌊
Oscillations de population conformes au modèle Lotka-Volterra. Système d'énergie avec boucles de rétroaction non-linéaires. IA prédateur par FSM à 4 états. Stabilité du point fixe vérifiée empiriquement.
🌊 6.1 Architecture de l'Écosystème
⚙️ Un écosystème qui se gère tout seul
8 types d'entités (nourriture, prédateurs, refuges, obstacles, vortex, zones de peur...) et 30+ types d'événements créent un écosystème 100% émergent. Le moteur auto-régule les populations : si les prédateurs se multiplient trop, les proies diminuent → les prédateurs meurent de faim → les proies reviennent. C'est le cycle de Lotka-Volterra en action.
🧪 Lotka-Volterra au quotidien
Le modèle prédateur-proie de Lotka-Volterra (1925) prédit des oscillations cycliques. On les observe en temps réel dans la simulation : les proies montent → les prédateurs se multiplient → les proies chutent → les prédateurs meurent de faim → les proies reviennent.
En prod, le risque c'est l'extinction. Si les oscillations sont trop amples, une espèce peut tomber à 0 et ne jamais revenir. Le système ajoute un "filet de sécurité" : si une espèce tombe sous 50 individus, le taux de reproduction est boosté. C'est un pattern de circuit-breaker bio.
| Système | Fichier Source | LOC | Rôle |
|---|---|---|---|
| Ecosystem | ecosystem.js |
324 | Spawning/despawning d'objets, événements, fear zones |
| Phase Biology | phase-biology.js |
~450 | Aging, énergie, satiété, mort naturelle, gestation |
| Phase Balance | phase-balance.js |
~280 | Auto-équilibrage populations, ratios proie/prédateur |
| Phase Predators | phase-predators.js |
~600 | IA de chasse, frenzy mode, territorialité |
| Flow Field | flow-field.js |
~400 | Courants marins, température, diffusion |
| Mutation Manager | mutation-manager.js |
811 | Spéciation, crossover génomique, naming, slot recycling |
Un vrai écosystème vivant 🌊
Ce n'est pas juste des poissons qui nagent ! C'est un écosystème complet : la nourriture pousse, les poissons mangent et grandissent, ils se reproduisent quand ils ont assez d'énergie, les prédateurs chassent… Et personne ne contrôle rien — tout émerge naturellement des règles !
⚡ 6.2 Modèle Énergétique & Cycle de Vie
⚙️ Deux jauges de vie pour des comportements réalistes
Chaque poisson possède deux jauges : l'énergie (vivre) et la satiété (se reproduire). Les deux déclinent à chaque instant — nager coûte de l'énergie. Se nourrir les recharge. Quand la satiété dépasse un seuil, le poisson peut se reproduire. Quand l'énergie tombe à zéro, il meurt.
Ce système simple produit des dynamiques écologiques émergentes : en abondance, la population explose ; en pénurie, la mortalité régule naturellement. Aucune règle globale ne pilote l'équilibre — il émerge des interactions individuelles.
🧪 Le système d'énergie vu par un senior
L'énergie est la monnaie de l'écosystème. Chaque action a un coût énergétique calibré par itérations successives. Le ratio manger/dépenser détermine la population d'équilibre. Trop généreux → explosion démographique. Trop strict → extinction.
Le tuning est un art : j'ai passé des dizaines d'heures à ajuster les constantes pour obtenir des oscillations stables mais pas monotones. La clé c'est la reproduction conditionnelle (énergie > 80) qui crée un seuil non-linéaire dans la dynamique.
satiety(t+1) = satiety(t) − satietyDrain × dt + food_eaten × satNutrition
Si energy ≤ 0 → mort par famine | Si satiety ≥ threshold → reproduction possible
Arbitrage Faim vs Sécurité
Un poisson affamé (energy < 30%) ignore les prédateurs proches pour chercher de la nourriture. Ce n'est pas un bug, c'est biologiquement réaliste : la survie immédiate prime sur l'évitement du danger quand la mort par famine est imminente.
Reproduction Conditionnelle
La reproduction nécessite : (1) satiety > 80%, (2) cooldown expiré, (3) mate de même espèce à portée, (4) population < maxSchoolSize. Chaque condition est évaluée indépendamment pour créer des patterns spatio-temporels naturels.
Drain Paramétrique
Chaque espèce a ses propres energyDrainMult et
satietyDrainMult.
Résultat : certaines espèces sont des « sprinters » qui consomment vite mais
chassent
efficacement, d'autres sont des « endurantes » qui survivent longtemps avec peu.
Chaque poisson a sa barre d'énergie
Manger = +25 énergie. Nager = -0.05/seconde. Fuir un prédateur = coûte encore plus. Se reproduire = -30 énergie. Quand l'énergie tombe à zéro... le poisson meurt. 😢
💼 Régulation naturelle
Ce système d'énergie crée une régulation automatique de la population. Trop de poissons → pas assez de nourriture → moins d'énergie → moins de reproduction → la population diminue. C'est le même principe que les marchés financiers (offre/demande).
🦈 6.3 Intelligence Artificielle des Prédateurs
⚙️ Des prédateurs intelligents qui s'adaptent
Les prédateurs ont une IA à 3 états : patrouille (errance), chasse (poursuite ciblée), et frénésie (attaque massive quand très affamé). En chasse, le prédateur émet une onde de choc dans le champ de potentiel, faisant fuir les proies — créant les mouvements de panique spectaculaires des vrais bancs.
Le passage entre états dépend de la faim et de la distance aux proies. Ce simple automate à 3 états suffit à produire des dynamiques prédateur-proie réalistes et spectaculaires.
🧪 L'IA prédateur en pratique
Les machines à états (FSM) sont le B.A.-BA de l'IA de jeu vidéo — utilisées depuis Pac-Man (1980). Le danger : l'explosion combinatoire. Avec 4 états, c'est gérable. Avec 10 états et des transitions conditionnelles, ça devient un cauchemar à debugger.
Ici, 4 états suffisent car le comportement émerge de l'interaction entre prédateurs et proies, pas de la complexité individuelle. C'est le principe fondamental de l'IA émergente : des règles simples → des comportements complexes.
🔵 État: Patrouille
Le prédateur dérive dans son territoire (rayon configurable). Il scanne les proies dans un rayon de détection large. Si une proie est détectée → transition vers Chasse.
// Patrouille: waypoint + scan
if (distToWaypoint < 30) {
waypoint = randomInTerritory();
}
target = nearestPrey(scanRadius);
🔴 État: Chasse
Poursuite directe de la cible avec accélération boost ×2.5. Si la proie s'échappe (distance > loseRadius) → retour Patrouille. Si contact → kill + énergie + potentiel transition Frenzy.
// Chasse: pursuit + intercept
accel = dir(target) * boostForce;
if (dist < killRange) {
kill(target);
killStreak++;
}
💡 Frenzy Mode (mode frénésie)
Après 3+ kills en séquence rapide, le prédateur entre en Frenzy : vitesse ×3, rayon de détection ×2, durée 5 secondes simulées. Les proies à proximité émettent des shockwaves de peur qui se propagent via le champ de potentiel. Résultat visuel : explosion dispersive du banc.
Les prédateurs ont un cerveau ! 🧠
Chaque prédateur a 4 comportements :
🚶 Patrouille — il nage au hasard en cherchant des proies
🏃 Poursuite — il a repéré une proie et fonce dessus
😤 Frénésie — il est très proche et accélère !
😴 Repos — il digère après avoir mangé
🎮 6.4 Démo Interactive : Mini-Écosystème
⚙️ Cycle prédateur-proie en temps réel
Écosystème simplifié : proies (bleu) fuient les prédateurs (rouge), mangent la nourriture (vert), se reproduisent avec assez d'énergie. L'équilibre émerge naturellement. Observez le graphe de population en bas du canvas.
🧪 La simulation comme preuve
La simulation confirme le modèle théorique : les oscillations de population suivent les cycles prédateur-proie. Le bruit stochastique (positions aléatoires, rencontres probabilistes) empêche les cycles parfaitement réguliers — exactement comme dans la nature.
L'intérêt n'est pas la précision mathématique mais la vraisemblance visuelle — le joueur doit "sentir" que l'écosystème est vivant et auto-régulé.
🌊 Mini-Écosystème Interactif
Ce qu'il faut retenir
Un écosystème complet émerge de quelques règles simples : énergie, faim, reproduction, prédation. Aucune règle globale ne contrôle les populations — l'équilibre naît des interactions individuelles. C'est de l'IA émergente appliquée, pas de la programmation classique.
L'écosystème vit, se nourrit et se reproduit. Mais chaque poisson se ressemble. Cette étude ajoute l'ADN numérique : 28 gènes qui contrôlent taille, couleur, vitesse, comportement. Les espèces naissent, évoluent, mutent et s'éteignent — comme dans la vraie nature.
Génétique Paramétrique, Spéciation & Arbre Phylogénétique
Chaque poisson est unique ! Son apparence — forme, couleur, motifs — est déterminée par un ADN numérique de 28 gènes. Les bébés héritent des traits de leurs parents avec de petites mutations, créant de nouvelles espèces au fil du temps.
28 paramètres par espèce, mutations directionnelles non-convergentes, seuil de similarité à 85%. La rareté est calibrée comme dans les jeux de collection (50% Commun → 2% Légendaire). Le drift empêche la convergence vers la moyenne — clé de la diversité.
🧬 7.1 Structure du Génome : 28 Gènes Continus
⚙️ Un ADN numérique à 28 gènes
Chaque espèce est définie par un génome de 28 gènes (valeurs entre 0 et 1) encodant la forme du corps, les nageoires, les couleurs, les motifs, la vitesse et l'agressivité. Le shader lit directement ces valeurs pour générer la morphologie unique de chaque poisson.
Les 28 gènes forment un espace à 28 dimensions où chaque point = une espèce possible. Deux espèces proches dans cet espace se ressemblent ; deux espèces éloignées sont radicalement différentes.
🧪 28 gènes : pourquoi ce nombre
28 gènes = un compromis entre diversité (plus de gènes = plus de combinaisons) et contrôlabilité (moins de gènes = plus facile à tuner). Avec 28 valeurs continues [0,1], l'espace des possibles est astronomique — assez pour que chaque espèce soit unique.
En pratique, seuls ~8 gènes ont un impact visuel fort (couleurs, taille, forme de queue). Les 20 autres contrôlent des stats invisibles (vitesse, endurance, agilité). C'est intentionnel : ça crée de la diversité fonctionnelle même entre des espèces qui se ressemblent.
| Index | Gène | Plage | Effet Visuel |
|---|---|---|---|
| 0 | bodyElongation |
[0, 1] | Ratio longueur/hauteur du corps |
| 1 | bodyWidth |
[0, 1] | Épaisseur latérale |
| 2 | headPointiness |
[0, 1] | Profil frontal (arrondi → pointu) |
| 3 | jawProminence |
[0, 1] | Taille de la mâchoire |
| 4 | dorsalHeight |
[0, 1] | Hauteur nageoire dorsale |
| 5 | dorsalLength |
[0, 1] | Longueur nageoire dorsale |
| 6 | tailForkDepth |
[0, 1] | Profondeur fourche caudale |
| 7 | tailSpan |
[0, 1] | Envergure de la queue |
| 8-14 | Appendices | [0, 1] | finRayLength, pectoralSize, analFinSize, ventral... |
| 15-22 | Extras | [0, 1] | tentacleCount, shellCoverage, clawSize, antennae... |
| 23-27 | Patterns | [0, 1] | patternType, patternScale, patternContrast, accentHue, luminance |
🧬 Variation combinatoire
Avec 28 gènes continus, l'espace génétique est de dimension 28 à valeurs continues. L'espace des phénotypes visuellement distincts est pratiquement infini. Après 1 heure de simulation, le joueur observe typiquement 50-100 espèces uniques.
L'ADN, c'est 28 curseurs 🎛️
Imagine 28 curseurs qu'on peut déplacer de 0 à 100%. Chaque curseur contrôle un aspect du poisson : taille, couleur, vitesse, forme de la queue… Comme les combinaisons sont infinies, chaque espèce est unique !
Des milliards de combinaisons
Avec 28 paramètres continus, le nombre de combinaisons possibles dépasse 10 milliards de milliards. Chaque espèce qui naît est vraiment unique dans l'histoire de la simulation !
🔀 7.2 Crossover, Mutation & Drift Directionnel
⚙️ Comment les espèces évoluent
À chaque reproduction, les gènes de l'enfant sont un mélange des parents, avec une mutation aléatoire. Mais POISSON utilise un drift directionnel : la mutation pousse dans une direction cohérente plutôt que de disperser au hasard.
Au fil des générations, les descendants dérivent dans l'espace génétique, créant de nouvelles variétés. Sans ce mécanisme, les mutations aléatoires convergeraient vers une moyenne identique. Le drift garantit que la diversité augmente — exactement comme dans la nature.
🧪 Le drift : l'ingrédient secret
Sans drift directionnel, les mutations aléatoires convergent vers la moyenne (loi des grands nombres). Toutes les espèces finissent par se ressembler. Le drift résout ça : chaque mutation a 70% de chance d'amplifier la divergence au lieu de la réduire.
C'est inspiré de la sélection naturelle réelle : dans la nature, les mutations vers des niches écologiques nouvelles sont favorisées. Le paramètre 70/30 est tuné pour maximiser la diversité visuelle tout en gardant des espèces "crédibles".
// mutation-manager.js — mutateValue() réel
// Drift directionnel : 70% pousse LOIN de la moyenne, 30% aléatoire
function mutateValue(a, b, deviation) {
const blend = 0.2 + fastRandom() * 0.6; // Ratio parent (0.2–0.8)
const base = a * blend + b * (1 - blend); // Pas centré sur 0.5 !
const avg = (a + b) * 0.5;
// 70% : amplifie la direction de divergence
const drift = fastRandom() < 0.7
? (base > avg ? 1 : -1) // Pousse plus loin du centre
: (fastRandom() * 2 - 1); // 30% : bruit pur
const delta = base * deviation * Math.abs(fastRandom() * 2 - 1) * drift;
return Math.max(0.01, base + delta);
}
// crossoverGenome() — Uniform crossover avec mutation morphologique
function crossoverGenome(genomeA, genomeB, mutRate, mutDev, driftRate) {
const child = new Float32Array(GENE_COUNT);
for (let i = 0; i < GENE_COUNT; i++) {
// 50/50 parent selection
child[i] = fastRandom() < 0.5 ? genomeA[i] : genomeB[i];
// Mutation chance per gene
if (fastRandom() < mutRate) {
child[i] += (fastRandom() * 2 - 1) * mutDev;
}
// Drift: push towards extreme values over generations
if (fastRandom() < driftRate) {
child[i] += child[i] > 0.5 ? 0.05 : -0.05;
}
child[i] = Math.max(0, Math.min(1, child[i]));
}
return child;
}
drift = sgn(Gchild[i] − avg(GA[i], GB[i])) avec prob 0.70
Divergence cumulée après N générations ≈ O(√N × σ)
Bébé poisson = papa + maman + surprise 🎲
Quand deux poissons se reproduisent, chaque gène du bébé est pris au hasard chez le père ou la mère. Puis une petite mutation aléatoire peut modifier certains gènes. C'est comme ça que naissent de nouvelles espèces au fil des générations !
💼 Algorithmes génétiques appliqués
Ce système de crossover + mutation est exactement celui utilisé en intelligence artificielle pour optimiser des solutions. Les algorithmes génétiques résolvent des problèmes de logistique, de design et de machine learning dans l'industrie.
💎 7.3 Système de Rareté & Taxonomie
⚙️ Des espèces rares comme dans un collectible game
Quand deux espèces de tiers différents se croisent, leur descendant hérite d'un niveau de rareté — Commun, Rare, Épique ou Légendaire. Plus les parents sont génétiquement éloignés, plus la rareté est élevée.
Un système anti-exploitation empêche de « farmer » les raretés : si les parents sont trop similaires, le croisement ne produit que du commun. Le résultat : des événements de spéciation rares et valorisés, qui émergent naturellement de l'écosystème.
🧪 Le game design de la rareté
Le système de rareté est inspiré des jeux de collection (Pokémon, gacha games). La rareté doit être corrélée à l'unicité réelle — sinon le joueur sent que c'est artificiel. C'est pourquoi le calcul combine divergence génétique + stats extrêmes.
Distribution cible : ~50% Commun, ~25% Peu Commun, ~15% Rare, ~8% Épique, ~2% Légendaire. Ces ratios sont standard en game design et créent le bon feeling de "collectionnabilité".
| Rareté | Condition | Bonus Stats | Couleur | Traits Spéciaux Possibles |
|---|---|---|---|---|
| ⬜ Commun | Même espèce ou tier adjacent | ×1.0 | #b0bec5 | — |
| 🟢 Peu commun | Cross-tier ≥ 1 | ×1.35 | #66bb6a | Résistance+ |
| 🔵 Rare | Cross-tier ≥ 2 ou tierSum ≥ 5 | ×1.70 | #42a5f5 | Résistance+, Fertile |
| 🟣 Épique | Cross-tier ≥ 3 ou tierSum ≥ 5 | ×2.05 | #ab47bc | + Véloce, Titanesque |
| 🟠 Légendaire | Cross-tier ≥ 3 + tierDiff ≥ 3 | ×2.40 | #ffa726 | + Omnivore, Prédateur Alpha |
💡 Système de Nommage : 10 000+ Noms Uniques
Le MutationManager génère des noms taxonomiques en combinant :
8 familles de préfixes (Classical, Power, Greek, Oceanic, Mutation,
Environ,
Mythic, Trait) × 10 préfixes chacune = 80 préfixes
20 suffixes latins (-us, -is, -ax, -oid, -ella, -opsis...)
30 combinaisons curées uniques (Baleine+Sardine = "Leviathan",
Requin+Poulpe =
"Krakenodon")
Cross-espèce : portmanteau avec point de coupe variable (35-65% de
chaque nom
parent).
Formaté en chiffres romains pour les homonymes (Neosardine II, III...).
Comme dans un jeu de collection ! ⭐
Chaque espèce reçoit un niveau de rareté :
⬜ Commun (~50%) — les espèces classiques
🟢 Peu Commun (~25%) — légèrement différentes
🔵 Rare (~15%) — des caractéristiques uniques
🟣 Épique (~8%) — vraiment spéciales
🟡 Légendaire (~2%) — extrêmement uniques !
🎮 7.4 Démo Interactive : Labo d'Évolution
⚙️ Observez l'évolution en action
Cette démo simule le crossover génétique. Chaque point = une espèce. Les couleurs et positions reflètent la divergence génétique au fil des générations. Ajustez le taux de mutation et le drift pour voir comment la diversité évolue.
🧪 Mesurer la diversité
La divergence est mesurée comme la distance maximale entre deux espèces dans l'espace des gènes. Si toutes les espèces se ressemblent, la divergence est faible → le drift est trop bas. Si les espèces sont toutes extrêmes, le drift est trop haut.
La valeur idéale de divergence est autour de 0.3-0.5. En dessous, on augmente le drift automatiquement. Au-dessus, on le réduit. C'est un auto-tuning qui maintient la diversité à un niveau visuellement intéressant.
🧬 Labo de Génétique
L'évolution en accéléré
En quelques secondes, vous pouvez observer des dizaines de générations d'évolution. Les points colorés représentent les espèces, les anneaux les générations. Plus un point est loin du centre, plus l'espèce est récente.
♻️ 7.5 Détection de Similarité & Recyclage d'Espèces
⚙️ Anti-doublon intelligent
Avant de créer une nouvelle espèce, le système compare le candidat avec toutes les espèces existantes sur 21 critères pondérés. Si la similarité dépasse 85%, le bébé est absorbé dans l'espèce la plus proche. Résultat : chaque espèce reste visuellement et statistiquement distincte.
🧪 Le seuil de 85% en pratique
Le seuil de 85% a été trouvé empiriquement. À 80%, on crée trop d'espèces quasi-identiques (pollution visuelle). À 90%, les mutations mineures créent de nouvelles espèces trop facilement → on atteint le cap de 64 espèces trop vite.
85% est le sweet spot. Le poids ×3 sur le genome (vs ×1 pour les stats numériques) assure que deux espèces avec les mêmes stats mais des couleurs différentes ne sont pas fusionnées. La couleur est le premier critère visuel du joueur.
// _computeSimilarity() — Extrait réel de mutation-manager.js
// 21 champs numériques pondérés + defense + diet + genome + food prefs
_computeSimilarity(candidate, existing) {
const numericFields = [
{ key: 'speedMult', weight: 2.0 },
{ key: 'forceMult', weight: 1.5 },
{ key: 'sizeMult', weight: 1.5 },
{ key: 'energyEfficiency', weight: 1.0 },
{ key: 'cohesionMult', weight: 1.0 },
{ key: 'separationMult', weight: 1.0 },
{ key: 'tempPref', weight: 1.5 },
{ key: 'fovMult', weight: 0.5 },
// ... + 13 autres champs
];
// Normalized diff per field × weight → weighted average
for (const { key, weight } of numericFields) {
const diff = Math.abs(a - b) / Math.max(|a|, |b|, 0.001);
totalMatch += weight * Math.max(0, 1 - diff);
}
// + Genome similarity (poids 3.0) + Diet match (poids 2.0)
// + Defense match (poids 2.0) + Food prefs (poids 1.5)
return totalMatch / totalWeight; // → [0, 1]
}
Slot Recycling
Quand RACES.length >= maxSpecies, les espèces éteintes les plus
anciennes sont
recyclées. Leur slot dans le tableau RACES[] est réécrit avec la
nouvelle espèce.
La matrice d'affinité RACE_AFFINITY[N×N] est mise à jour in-place.
Résultat :
allocation zéro.
Extinction Detection
checkExtinctions(racePop) est appelé chaque frame dans phase-balance. Si
racePop[r] === 0 pour une espèce mutante (index ≥ 16), elle entre dans
extinctionOrder[]. Les espèces de base (0-15) ne sont jamais recyclées.
Affinity Matrix
Matrice N×N d'affinité inter-espèces. Un nouvel hybride hérite de la moyenne des
affinités de ses
parents : affinity[new][j] = (affinity[A][j] + affinity[B][j]) / 2.
Utilisée pour
le flocking inter-espèces.
Pas de doublons ! 🚫
Avant de créer une nouvelle espèce, le système vérifie qu'elle n'existe pas déjà. Si le bébé ressemble trop à une espèce existante (plus de 85% de similarité), il rejoint cette espèce au lieu d'en créer une nouvelle. Ça évite la pollution !
Ce qu'il faut retenir
28 gènes continus [0,1] encodent la morphologie complète d'un poisson. Le crossover et la mutation produisent une diversité infinie d'espèces visuellement distinctes. Le système de spéciation empêche la convergence génétique — chaque nouvelle espèce est réellement unique.