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>
14 KiB
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
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
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 :
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
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
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.
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 :
// 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 :
// 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 :
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 :
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
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 :
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 :
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 :
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 :
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 :
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
// 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 :
// Pathfinding calcule la position
Vector2 newPosition = pathfinding.getNextPosition(entity);
entity.setPosition(newPosition);
// Rendering dessine à cette position
render(entity);
Statut : ✅ DESIGN COMPLETE Prochaines étapes :
- Implémenter
EntityRendereravec la pipeline fixe/mobile - Créer
SpriteComposerpour la composition de sprites - Intégrer avec le système de combat pour les cibles
- Optimiser avec batching et culling