Removed core engine infrastructure from warfactoryracine: - Core interfaces: IEngine, IModule, IModuleSystem, IIO, ITaskScheduler, ICoordinationModule - Configuration system: IDataTree, IDataNode, DataTreeFactory - UI system: IUI, IUI_Enums, ImGuiUI (header + implementation) - Resource management: Resource, ResourceRegistry, SerializationRegistry - Serialization: ASerializable, ISerializable - World generation: IWorldGenerationStep (replaced by IWorldGenerationPhase) These components now live in the GroveEngine repository and are included via CMake add_subdirectory(../GroveEngine) for reusability across projects. warfactoryracine remains focused on game-specific logic and content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
517 lines
14 KiB
Markdown
517 lines
14 KiB
Markdown
# 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<FixedComponent> fixedComponents;
|
|
|
|
// Composants mobiles (têtes, armes, tourelles)
|
|
std::vector<MobileComponent> 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<CompositionKey, SpriteID> compositionCache;
|
|
|
|
SpriteID getComposedSprite(const std::vector<LayerID>& 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<LayerID> 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<RenderableComponent> 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<SpriteID> 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
|