# Rendering System - Sprite Composition ## Vue d'ensemble Le système de rendu graphique de Warfactory utilise une approche de **composition de sprites** inspirée de Rimworld, optimisée pour minimiser le nombre de sprites à dessiner tout en permettant une grande flexibilité visuelle. **Principe fondamental** : Chaque entité est composée de sprites superposés avec des orientations indépendantes, permettant un rendu dynamique efficace. ## Architecture de composition ### Personnages (Humains) Un personnage est composé de **3 sprites maximum** : ``` Personnage ├── Corps (sprite de base + vêtements composés) ├── Tête (orientation indépendante - turret behavior) └── Arme (orientation indépendante - turret behavior) ``` **Optimisation clé** : Les vêtements sont pré-composés avec le corps pour éviter de dessiner chaque couche de vêtement séparément. #### Exemple de composition ``` Rendu final = Corps_Base + Veste + Pantalon + Tête + Arme └─────────────────────┘ 1 sprite composé ``` Au lieu de dessiner 5 sprites par personnage, on en dessine **3** : - 1 sprite corps composé (avec tous les vêtements) - 1 sprite tête - 1 sprite arme ### Véhicules Un véhicule suit le même principe avec un nombre variable de tourelles : ``` Véhicule ├── Corps (sprite fixe avec orientation principale) └── Tourelles[] (orientations indépendantes) ├── Tourelle principale ├── Tourelle secondaire (optionnelle) └── Tourelle tertiaire (optionnelle) ``` **Exemple** : - Tank léger : Corps + 1 tourelle = **2 sprites** - APC : Corps + 1 tourelle principale + 1 mitrailleuse = **3 sprites** - Véhicule lourd : Corps + 3 tourelles = **4 sprites** ## Pipeline unifiée : Concept Fixe/Mobile Le système distingue deux types de composants : ### 1. Composants Fixes **Définition** : Composants dont l'orientation est liée à l'orientation principale de l'entité. **Caractéristiques** : - Rotation synchronisée avec l'entité parente - Pas de calcul d'orientation indépendant - Exemples : corps de personnage, châssis de véhicule ```cpp struct FixedComponent { SpriteID sprite; Vector2 offset; // Position relative au centre de l'entité float rotationOffset; // Rotation additionnelle par rapport à l'orientation principale }; ``` ### 2. Composants Mobiles (Turret Behavior) **Définition** : Composants avec orientation indépendante suivant une cible. **Caractéristiques** : - Orientation calculée indépendamment - Cible une position XY (target) - Contraintes de rotation optionnelles (angle min/max, vitesse de rotation) - Exemples : tête de personnage, arme, tourelle de véhicule ```cpp struct MobileComponent { SpriteID sprite; Vector2 offset; // Position relative au centre de l'entité // Turret behavior Vector2 currentTarget; // Position XY actuelle de la cible float currentAngle; // Angle actuel du composant float rotationSpeed; // Vitesse de rotation max (rad/s) // Contraintes optionnelles float minAngle; // Angle minimum (par rapport au fixe) float maxAngle; // Angle maximum (par rapport au fixe) bool hasConstraints; // Si true, applique les contraintes min/max }; ``` ## Pipeline de rendu unifiée La **même pipeline** gère les têtes, armes et tourelles : ```cpp class EntityRenderer { // Composants fixes (corps) std::vector fixedComponents; // Composants mobiles (têtes, armes, tourelles) std::vector mobileComponents; void render(Vector2 position, float mainOrientation) { // 1. Dessiner les composants fixes for (const FixedComponent& comp : fixedComponents) { float finalAngle = mainOrientation + comp.rotationOffset; drawSprite(comp.sprite, position + rotate(comp.offset, mainOrientation), finalAngle); } // 2. Dessiner les composants mobiles for (MobileComponent& comp : mobileComponents) { // Calculer l'orientation vers la cible float targetAngle = calculateAngleToTarget(position, comp.offset, comp.currentTarget); // Appliquer la vitesse de rotation (smooth rotation) comp.currentAngle = lerpAngle(comp.currentAngle, targetAngle, comp.rotationSpeed * deltaTime); // Appliquer les contraintes si nécessaire if (comp.hasConstraints) { float relativeAngle = comp.currentAngle - mainOrientation; comp.currentAngle = mainOrientation + clamp(relativeAngle, comp.minAngle, comp.maxAngle); } drawSprite(comp.sprite, position + rotate(comp.offset, mainOrientation), comp.currentAngle); } } }; ``` ## Exemples concrets ### Personnage avec arme ```cpp EntityRenderer soldier; // Corps (fixe) soldier.fixedComponents.push_back({ .sprite = SPRITE_SOLDIER_BODY_COMPOSED, .offset = {0, 0}, .rotationOffset = 0 }); // Tête (mobile - turret behavior) soldier.mobileComponents.push_back({ .sprite = SPRITE_HEAD, .offset = {0, 8}, // 8 pixels au-dessus du centre .currentTarget = enemyPosition, .currentAngle = 0, .rotationSpeed = 5.0f, // rad/s .minAngle = -PI/2, // Peut regarder 90° à gauche .maxAngle = PI/2, // Peut regarder 90° à droite .hasConstraints = true }); // Arme (mobile - turret behavior) soldier.mobileComponents.push_back({ .sprite = SPRITE_RIFLE, .offset = {4, 2}, // Décalage pour la main .currentTarget = enemyPosition, .currentAngle = 0, .rotationSpeed = 3.0f, // Plus lent que la tête .minAngle = -PI/4, // Contraintes plus strictes .maxAngle = PI/4, .hasConstraints = true }); // Rendu soldier.render(soldierPosition, soldierOrientation); ``` ### Véhicule avec tourelle ```cpp EntityRenderer tank; // Châssis (fixe) tank.fixedComponents.push_back({ .sprite = SPRITE_TANK_HULL, .offset = {0, 0}, .rotationOffset = 0 }); // Tourelle principale (mobile - rotation 360°) tank.mobileComponents.push_back({ .sprite = SPRITE_TANK_TURRET, .offset = {0, 0}, .currentTarget = targetPosition, .currentAngle = 0, .rotationSpeed = 2.0f, // Rotation lente (réaliste) .hasConstraints = false // Rotation complète }); // Rendu tank.render(tankPosition, tankOrientation); ``` ## Optimisations ### 1. Composition de sprites (Layering) **Problème** : Dessiner tous les vêtements séparément = beaucoup de draw calls. **Solution** : Pré-composer les sprites en cache. ```cpp class SpriteComposer { std::unordered_map compositionCache; SpriteID getComposedSprite(const std::vector& layers) { CompositionKey key = hashLayers(layers); if (compositionCache.contains(key)) { return compositionCache[key]; } // Composer les sprites SpriteID composed = renderToTexture(layers); compositionCache[key] = composed; return composed; } }; ``` **Exemple** : ```cpp // Composition d'un soldat std::vector soldierLayers = { SPRITE_BODY_BASE, SPRITE_PANTS_CAMO, SPRITE_JACKET_TACTICAL, SPRITE_VEST_ARMOR }; SpriteID composedBody = composer.getComposedSprite(soldierLayers); ``` **Avantages** : - Réduction massive des draw calls - Cache persistant entre frames - Mise à jour uniquement quand les vêtements changent ### 2. Sprite Batching Grouper les sprites par texture pour minimiser les changements d'état GPU : ```cpp // Trier par texture avant de dessiner std::sort(entities.begin(), entities.end(), [](const Entity& a, const Entity& b) { return a.getTextureID() < b.getTextureID(); }); // Dessiner tous les sprites de la même texture ensemble for (const Entity& entity : entities) { entity.render(); } ``` ### 3. Culling Ne dessiner que les entités visibles à l'écran : ```cpp void renderEntities(const Camera& camera) { for (Entity& entity : entities) { if (camera.isVisible(entity.getBounds())) { entity.render(); } } } ``` ## Système de couches (Layering) ### Ordre de rendu (Z-order) Les sprites doivent être dessinés dans un ordre précis pour éviter les problèmes de superposition : ``` Z-Order (du plus bas au plus haut) ├── 0: Corps/Châssis (fixe) ├── 1: Tête/Tourelle (mobile) └── 2: Arme (mobile) ``` **Implémentation** : ```cpp struct RenderableComponent { SpriteID sprite; Vector2 position; float rotation; int zOrder; // Ordre de rendu }; void renderEntity(const Entity& entity) { std::vector renderables; // Collecter tous les composants collectFixedComponents(entity, renderables); collectMobileComponents(entity, renderables); // Trier par Z-order std::sort(renderables.begin(), renderables.end(), [](const RenderableComponent& a, const RenderableComponent& b) { return a.zOrder < b.zOrder; }); // Dessiner dans l'ordre for (const RenderableComponent& comp : renderables) { drawSprite(comp.sprite, comp.position, comp.rotation); } } ``` ## Gestion des orientations ### Angles et conventions **Convention** : Angle 0 = direction droite (East), rotation anti-horaire. ``` N (π/2) | W (π) --+-- E (0) | S (3π/2) ``` ### Calcul de l'angle vers une cible ```cpp float calculateAngleToTarget(Vector2 entityPos, Vector2 componentOffset, Vector2 target) { // Position mondiale du composant Vector2 worldPos = entityPos + componentOffset; // Direction vers la cible Vector2 direction = target - worldPos; // Angle en radians return std::atan2(direction.y, direction.x); } ``` ### Interpolation d'angle (smooth rotation) Pour éviter les rotations brusques, on interpole vers l'angle cible : ```cpp float lerpAngle(float current, float target, float speed, float deltaTime) { // Normaliser les angles entre -π et π float diff = normalizeAngle(target - current); // Calculer le changement maximum pour cette frame float maxChange = speed * deltaTime; // Appliquer le changement (limité par la vitesse) float change = std::clamp(diff, -maxChange, maxChange); return current + change; } float normalizeAngle(float angle) { while (angle > PI) angle -= 2 * PI; while (angle < -PI) angle += 2 * PI; return angle; } ``` ## Extensions futures ### 1. Animations Ajouter un système d'animation pour les sprites : ```cpp struct AnimatedComponent { std::vector frames; float frameDuration; float currentTime; SpriteID getCurrentFrame() { int frameIndex = (int)(currentTime / frameDuration) % frames.size(); return frames[frameIndex]; } void update(float deltaTime) { currentTime += deltaTime; } }; ``` ### 2. Effets visuels Ajouter des effets visuels aux composants : ```cpp struct VisualEffect { Color tint; // Couleur de teinte float opacity; // Transparence Vector2 scale; // Échelle bool flipX, flipY; // Miroirs }; ``` ### 3. Attachments dynamiques Support pour des composants attachés dynamiquement : ```cpp class AttachmentSystem { void attachComponent(EntityID entity, MobileComponent component, const std::string& attachPoint); void detachComponent(EntityID entity, const std::string& attachPoint); }; // Exemple : attacher une tourelle sur un point de montage attachments.attachComponent(vehicleID, turretComponent, "mount_point_1"); ``` ## Performances ### Métriques cibles - **Entités à l'écran** : 500-1000 personnages/véhicules simultanés - **Draw calls** : < 100 par frame (via batching) - **FPS cible** : 60 fps (16.67ms par frame) - **Temps de rendu** : < 8ms par frame (50% du budget) ### Profiling Points de mesure critiques : ```cpp void renderAll() { PROFILE_SCOPE("Entity Rendering"); { PROFILE_SCOPE("Sprite Composition"); updateComposedSprites(); } { PROFILE_SCOPE("Culling"); cullEntities(camera); } { PROFILE_SCOPE("Sorting"); sortByTexture(visibleEntities); } { PROFILE_SCOPE("Drawing"); drawEntities(visibleEntities); } } ``` ## Intégration avec les autres systèmes ### Lien avec le système militaire Le système de rendu est découplé des systèmes de combat : - **Combat System** calcule les positions, rotations, cibles - **Rendering System** lit ces données et dessine les sprites ```cpp // Dans le module Combat void updateCombat(Entity& entity, float deltaTime) { // Calculer la cible Vector2 target = findClosestEnemy(entity.position); entity.setTarget(target); // Le rendering system lira cette cible plus tard } // Dans le module Rendering void render(const Entity& entity) { // Lire la cible depuis l'entité Vector2 target = entity.getTarget(); // Dessiner avec la cible entityRenderer.render(entity.position, entity.orientation, target); } ``` ### Lien avec le système de pathfinding Le système de rendu utilise les positions calculées par le pathfinding : ```cpp // Pathfinding calcule la position Vector2 newPosition = pathfinding.getNextPosition(entity); entity.setPosition(newPosition); // Rendering dessine à cette position render(entity); ``` --- **Statut** : ✅ **DESIGN COMPLETE** **Prochaines étapes** : 1. Implémenter `EntityRenderer` avec la pipeline fixe/mobile 2. Créer `SpriteComposer` pour la composition de sprites 3. Intégrer avec le système de combat pour les cibles 4. Optimiser avec batching et culling