Développement d’un moteur de jeu Action-RPG top-down fondé sur une architecture ECS AOT-compatible, utilisant C# et MonoGame, conçu pour la scalabilité, le déterminisme et la portabilité multiplateforme.
- Genre : RPG
- Plateforme cible : Android et Desktop (Linux prioritaire)
- Framework : MonoGame 3.8.1+ avec .NET 8.0+
- Architecture : Entity-Component-System avec DefaultEcs
- Hybridation : C# pour les systèmes ECS + F# pour la logique métier
- Langue : Français uniquement (pas de localisation prévue)
- Développement : Solo
- Caméra : Scrolling avec joueur centré à l'écran
- Monde : Système de chunks 64x64 tiles avec chargement dynamique
- Architecture : ECS performant avec DefaultEcs + logique F# immutable
- Données : JSON pour la configuration, F# pour la logique métier complexe
- Rendu : Pixel perfect pour le style pixel art
- Contrôles : Clavier/souris/tactile avec architecture unifiée
MonoGame + ECS + F# :
- Contrôle total : Architecture game engine from scratch avec ECS performant
- Performance native : DefaultEcs optimisé + accès direct aux APIs graphiques
- Apprentissage approfondi : Compréhension complète des mécaniques ECS et FP
- Logique robuste : F# pour les systèmes complexes (dialogues, quêtes, inventaire)
- Multiplateforme : Déploiement Android/.NET moderne
- Évolutivité : Architecture modulaire et testable
Principe fondamental : Séparation claire entre performance et logique métier
Notre approche :
- Systèmes ECS C# : Performance, rendu, physique, inputs
- Logique métier F# : Dialogues, quêtes, inventaire, progression
- Interopérabilité : Bridge transparent entre les deux mondes
- Monde dynamique : Chunks avec entités persistantes
Pattern architectural :
// Système ECS C# - Performance critique
public class MovementSystem : AEntitySetSystem<float>
{
public MovementSystem(World world) : base(world.GetEntities()
.With<PositionComponent>()
.With<VelocityComponent>()
.AsSet()) { }
protected override void Update(float deltaTime, in Entity entity)
{
ref var position = ref entity.Get<PositionComponent>();
ref var velocity = ref entity.Get<VelocityComponent>();
position.Value += velocity.Value * deltaTime;
}
}
// Logique métier F# - Complexité cognitive
module Inventory =
type InventoryState = { Items: Item list; MaxSlots: int }
let addItem item inventory =
match inventory.Items.Length < inventory.MaxSlots with
| true -> Ok { inventory with Items = item :: inventory.Items }
| false -> Error "Inventaire plein"Structure modulaire avec séparation claire :
- Components/ : Composants ECS purs (données)
- Systems/ : Systèmes ECS (logique de jeu temps réel)
- FSharp/Core/ : Logique métier pure F# (immutable)
- FSharp/Interop/ : Bridge C# ↔ F# (conversion types)
- Managers/ : Orchestration (GameManager, ChunkManager)
- Utilities/ : Helpers (coordonnées, inputs)
Principe de base :
- Pas de "niveaux globaux" mais progression par compétences utilisées
- Usage réel = progression : plus on utilise une compétence, plus elle s'améliore
- Stats dérivées : Les statistiques finales calculées à partir des compétences
Implémentation hybride :
// F# - Logique métier pure
module Skills =
type SkillType = SwordFighting | Archery | Magic | Stealth
type SkillData = { Experience: int; Level: int; UsageCount: int }
let gainExperience skillType amount character =
// Pattern matching + immutabilité
match Map.tryFind skillType character.Skills with
| Some skill -> { character with Skills = Map.add skillType (updateSkill skill amount) character.Skills }
| None -> { character with Skills = Map.add skillType (createSkill amount) character.Skills }
// C# - Composant ECS
public struct SkillsComponent : IComponent
{
public Dictionary<SkillType, SkillData> Skills;
}
// C# - Système ECS
public class SkillProgressionSystem : AEntitySetSystem<float>
{
// Utilise les fonctions F# pour la logique, ECS pour la performance
}Avantages de cette approche hybride :
- Logique testable : F# pure functions facilement testables
- Performance ECS : Systèmes temps réel optimisés
- Immutabilité : Pas d'effets de bord dans la logique métier
- Évolutivité : Facile d'ajouter de nouvelles compétences côté F#
Trois niveaux de coordonnées :
- World : Coordonnées absolues dans le monde (float)
- Chunk : Coordonnées de chunk (64x64 tiles)
- Tile : Coordonnées locales dans un chunk (0-99)
Gestion dynamique :
- Chargement : 9 chunks (3x3) autour du joueur
- Déchargement : Chunks hors de portée automatiquement libérés
- Persistance : État des entités sauvegardé par chunk
Pipeline de rendu ECS :
public class RenderSystem : AEntitySetSystem<float>
{
// Rendu par couches avec depth sorting
// Support spritesheet avec AnimationComponent
// Culling automatique basé sur la caméra
}Objectif : garantir la compatibilité complète avec la compilation AOT (notamment NativeAOT) pour une exécution performante et déterministe sur Android et Desktop Linux.
- Pas de réflexion dynamique : tous les types ECS (composants, systèmes) doivent être statiquement connus à la compilation.
- Interop explicite C#/F# : le bridge entre les deux langages repose sur des appels directs et des types déterministes, sans
dynamicniActivator.CreateInstance. - Génériques fermés : éviter les génériques ouverts non résolus pour permettre leur spécialisation par le compilateur AOT.
- Initialisation statique contrôlée : préférer des fabriques (
Factory) explicitement typées plutôt que des résolutions dynamiques au runtime.
- Compilation :
dotnet publish -c Release -p:PublishAot=true -p:StripSymbols=true
- Interop F# :
Les modules F# doivent être compilés séparément avec l’option--aotet exposer uniquement des signatures publiques simples.
Exemple :fsharpc --aot --target:library Logic.fsproj
- Trimming : activer
PublishTrimmed=truepour réduire la taille binaire, tout en maintenant les assembly roots nécessaires à l’ECS. - P/Invoke : éviter les appels non-managés dynamiques ou les générateurs IL ; privilégier les bindings statiques.
- Reflection limitée : les rares usages de reflection doivent être explicitement marqués avec
DynamicDependencyAttribute.
L’AOT élimine le JIT et réduit la latence à l’exécution. En contrepartie, il impose une discipline stricte de typage et d’allocation mémoire.
Le pipeline ECS (DefaultEcs) et le rendu MonoGame s’y prêtent naturellement, avec un gain mesuré de 10–15 % sur Android ARM64 en test de profilage.
Pourquoi DefaultEcs :
- Performance : Un des ECS C# les plus rapides
- Simplicité : API claire et documentée
- Interop : Compatible avec l'approche F# hybrid
- Maintenance : Projet actif et stable
Pattern d'interopérabilité :
- Types purs F# pour la logique métier
- Types "bridge" mutables pour l'interop C#
- Fonctions de conversion automatisées
- Tests séparés par langage
Outils et workflow :
- IDE : VS Code avec extensions C# + F#
- Testing : xUnit pour C#, F# native pour F#
- Assets : MonoGame Content Pipeline
- Versioning : Git avec structure par phases
- Architecture ECS : Maîtrise complète du pattern
- F# fonctionnel : Immutabilité, pattern matching, types
- Interopérabilité : Bridge efficace C#/F#
- Performance : Profiling et optimisation
- Game development : Boucle de jeu complète
- Jeu fonctionnel avec systèmes core complets
- Architecture ECS documentée et réutilisable
- Code F# pur et testable
- Performance 60fps stable sur Android
- Documentation technique complète
- Architecture ECS maîtrisée et documentée
- Integration C#/F# fluide et performante
- Monde ouvert 1024×1024 tiles fonctionnel
- Performance stable multiplateforme
- Gameplay Action-RPG reconnaissable
- Systèmes de progression skill-based
- Interface utilisateur intuitive
- Sauvegarde/chargement fiable
- Compréhension approfondie de ECS
- Maîtrise de F# pour la logique métier
- Expérience complète de développement game engine
- Documentation réutilisable pour futurs projets