Animation System Architecture
This document provides a technical deep-dive into the Olympe Engine 2D Animation System architecture, implementation details, and integration points.
System Architecture Overview
The Animation System consists of three primary components:
- AnimationManager (Singleton): Resource loading and management
- AnimationSystem (ECS System): Per-frame animation updates
- VisualAnimation_data (ECS Component): Animation state storage
Component Diagram
ECS Component Structure
VisualAnimation_data Fields Explained
struct VisualAnimation_data
{
// Resource References (Set at creation)
std::string bankId; // Links to AnimationBank in AnimationManager
std::string graphId; // Optional: Links to AnimationGraph for FSM
// Current State (Changes at runtime)
std::string currentAnimName; // Current animation name (e.g., "walk")
std::string currentStateName; // Current FSM state (if using graph)
int currentFrame; // Current frame index in sequence
float elapsedTime; // Time accumulated since last frame change
// Playback Control
bool isPlaying; // Whether animation is actively updating
bool loop; // Whether current animation should loop
float playbackSpeed; // Speed multiplier (1.0 = normal, 2.0 = 2x speed)
bool autoStart; // Start playing immediately on entity creation
// Cached Pointers (Performance optimization)
AnimationSequence* currentSequence; // Pointer to current animation sequence
SDL_Texture* cachedTexture; // Cached texture pointer (avoid lookup)
// Internal State
bool isAnimationComplete; // True when non-looping animation finishes
int totalFrames; // Total frame count in current sequence
};
Field Lifecycle
Initialization (Entity creation from prefab):
// 1. Load from prefab JSON
anim.bankId = "character";
anim.currentAnimName = "idle";
anim.autoStart = true;
// 2. AnimationSystem resolves pointers on first update
auto bank = AnimationManager::Get().GetBank(anim.bankId);
anim.currentSequence = bank->GetAnimation(anim.currentAnimName);
anim.cachedTexture = DataManager::Get().GetTexture(bank->spritesheetPath);
anim.totalFrames = anim.currentSequence->frames.size();
// 3. Start playing if autoStart
if (anim.autoStart)
{
anim.isPlaying = true;
anim.currentFrame = 0;
anim.elapsedTime = 0.0f;
}
Runtime Update (Every frame):
// AnimationSystem::Process() for each entity
if (anim.isPlaying)
{
anim.elapsedTime += deltaTime * anim.playbackSpeed;
float frameDuration = anim.currentSequence->frames[anim.currentFrame].duration;
if (anim.elapsedTime >= frameDuration)
{
anim.elapsedTime -= frameDuration;
anim.currentFrame++;
if (anim.currentFrame >= anim.totalFrames)
{
if (anim.loop)
{
anim.currentFrame = 0; // Loop back to start
}
else
{
anim.currentFrame = anim.totalFrames - 1; // Stay on last frame
anim.isAnimationComplete = true;
if (!anim.currentSequence->nextAnimation.empty())
{
PlayAnimation(entity, anim.currentSequence->nextAnimation);
}
}
}
}
// Update VisualSprite_data srcRect
sprite.srcRect = anim.currentSequence->frames[anim.currentFrame].srcRect;
}
Frame Timing Algorithm
The animation system uses delta-time accumulation for frame-rate independent playback.
Algorithm Details
void AnimationSystem::UpdateFrame(ECS_Entity entity, float deltaTime)
{
auto& anim = World::Get().GetComponent<VisualAnimation_data>(entity);
if (!anim.isPlaying || !anim.currentSequence)
return;
// Step 1: Accumulate time with playback speed
float effectiveDeltaTime = deltaTime * anim.playbackSpeed;
anim.elapsedTime += effectiveDeltaTime;
// Step 2: Get current frame duration
const AnimationFrame& currentFrame = anim.currentSequence->frames[anim.currentFrame];
float frameDuration = currentFrame.duration;
// Step 3: Check if frame should advance
while (anim.elapsedTime >= frameDuration)
{
// Subtract frame duration (carry over excess time)
anim.elapsedTime -= frameDuration;
// Advance frame
anim.currentFrame++;
// Step 4: Handle end of sequence
if (anim.currentFrame >= anim.currentSequence->frames.size())
{
if (anim.loop)
{
anim.currentFrame = 0;
}
else
{
// Clamp to last frame
anim.currentFrame = anim.currentSequence->frames.size() - 1;
anim.isAnimationComplete = true;
anim.isPlaying = false; // Stop playback
// Trigger nextAnimation if defined
if (!anim.currentSequence->nextAnimation.empty())
{
PlayAnimation(entity, anim.currentSequence->nextAnimation, true);
return;
}
}
break;
}
// Get next frame duration for next iteration
const AnimationFrame& nextFrame = anim.currentSequence->frames[anim.currentFrame];
frameDuration = nextFrame.duration;
}
}
Time Accumulation Benefits
- Frame-rate Independence: Works correctly at any FPS (30, 60, 144)
- No Frame Skipping: Excess time carries over to next frame
- Precise Timing: Accurate even with variable delta times
- Smooth Playback: No stuttering from rounding errors
Example Timeline
Frame durations: [0.1s, 0.1s, 0.1s, 0.1s]
Delta time: 0.016s (60 FPS)
Update 1: elapsedTime = 0.016s (frame 0)
Update 2: elapsedTime = 0.032s (frame 0)
Update 3: elapsedTime = 0.048s (frame 0)
Update 4: elapsedTime = 0.064s (frame 0)
Update 5: elapsedTime = 0.080s (frame 0)
Update 6: elapsedTime = 0.096s (frame 0)
Update 7: elapsedTime = 0.112s → advance to frame 1, elapsedTime = 0.012s
Update 8: elapsedTime = 0.028s (frame 1)
...
Spritesheet Calculation Math
Frame Index to Source Rectangle
Given:
frameWidth,frameHeight: Dimensions of each framecolumns: Number of frames per rowspacing: Pixels between framesmargin: Border pixels around gridframeIndex: Frame number (0-based)
Calculate srcRect:
SDL_Rect CalculateSrcRect(int frameIndex,
int frameWidth, int frameHeight,
int columns, int spacing, int margin)
{
// Calculate row and column
int row = frameIndex / columns;
int col = frameIndex % columns;
// Calculate pixel coordinates
int x = margin + col * (frameWidth + spacing);
int y = margin + row * (frameHeight + spacing);
return { x, y, frameWidth, frameHeight };
}
Example Calculations
Scenario 1: Simple Grid (no spacing/margin)
Spritesheet: 512×256
Frame size: 64×64
Columns: 8
Spacing: 0
Margin: 0
Frame 0: (0, 0, 64, 64)
Frame 1: (64, 0, 64, 64)
Frame 7: (448, 0, 64, 64)
Frame 8: (0, 64, 64, 64) // Next row
Frame 15: (448, 64, 64, 64)
Scenario 2: With Spacing
Frame size: 64×64
Columns: 8
Spacing: 2
Margin: 0
Frame 0: (0, 0, 64, 64)
Frame 1: (66, 0, 64, 64) // 64 + 2 spacing
Frame 2: (132, 0, 64, 64) // 64 + 2 + 64 + 2
Frame 8: (0, 66, 64, 64) // New row with 2px spacing
Scenario 3: With Margin and Spacing
Frame size: 32×32
Columns: 4
Spacing: 1
Margin: 4
Frame 0: (4, 4, 32, 32) // Starts at margin
Frame 1: (37, 4, 32, 32) // 4 + 32 + 1 spacing
Frame 4: (4, 37, 32, 32) // New row: 4 + 32 + 1
Performance Characteristics
Time Complexity
| Operation | Complexity | Notes |
|---|---|---|
| Load animation bank | O(n) | n = number of animations in bank |
| Get animation from bank | O(1) | Hash map lookup by name |
| Get bank from manager | O(1) | Hash map lookup by ID |
| Update single entity | O(1) | Direct component access |
| Update all entities | O(n) | n = number of animated entities |
| Play animation | O(1) | Pointer assignment + state update |
| Transition state | O(k) | k = number of transitions (typically < 10) |
Space Complexity
Per AnimationBank:
- ~100 bytes overhead
- ~500 bytes per animation
- ~50 bytes per frame
Example: Character with 10 animations, 8 frames each
Bank: 100 bytes
Animations: 10 × 500 = 5,000 bytes
Frames: 10 × 8 × 50 = 4,000 bytes
Total: ~9 KB
Per Entity:
- VisualAnimation_data: ~100 bytes
- No frame data duplication (shared pointers)
100 animated entities: ~10 KB (component data only)
Cache Locality
The system is designed for good cache performance:
- Component data is contiguous (ECS architecture)
- Hot path avoids pointer chasing (cached pointers in component)
- Animation sequences shared (const references, no copies)
- Texture pointers cached (avoid DataManager lookup every frame)
Thread Safety Considerations
Thread-Safe Operations
- AnimationManager resource loading (mutexes on map access)
- Read-only access to banks/graphs (const references)
Not Thread-Safe
- AnimationSystem::Process() (single-threaded ECS update)
- Component modification (main thread only)
- Texture access (SDL3 is not thread-safe)
Recommendations
- Load all animation resources before starting game loop
- Perform runtime loading on main thread only
- Use job system for animation blending (future feature)
Memory Management
Shared Pointers for Animation Sequences
class AnimationBank
{
std::vector<std::shared_ptr<AnimationSequence>> m_animations;
public:
std::shared_ptr<AnimationSequence> GetAnimation(const std::string& name) const
{
// Returns shared_ptr - reference counted
// Multiple entities can reference same sequence safely
}
};
Benefits
- Automatic cleanup: Sequences deleted when last reference removed
- Safe sharing: Multiple entities reference same data
- No double-free: Reference counting prevents errors
Pointer Resolution
// Component stores raw pointer for performance
struct VisualAnimation_data
{
AnimationSequence* currentSequence; // Raw pointer (fast access)
};
// Resolved from shared_ptr on animation change
void AnimationSystem::PlayAnimation(ECS_Entity entity, const std::string& animName)
{
auto bank = AnimationManager::Get().GetBank(anim.bankId);
auto sequence = bank->GetAnimation(animName); // Returns shared_ptr
anim.currentSequence = sequence.get(); // Store raw pointer
}
Safety: Raw pointer is valid as long as bank is loaded (guaranteed during gameplay).
Integration Points
1. DataManager (Texture Loading)
// AnimationManager loads textures via DataManager
SDL_Texture* texture = DataManager::Get().LoadTexture(bank.spritesheetPath);
Integration: Uses existing texture cache, no duplication.
2. PrefabFactory (Entity Creation)
// PrefabFactory reads VisualAnimation_data from prefab JSON
{
"VisualAnimation_data": {
"bankId": "character",
"currentAnimName": "idle",
"autoStart": true
}
}
// Factory creates component
VisualAnimation_data anim;
anim.bankId = json["bankId"];
anim.currentAnimName = json["currentAnimName"];
World::Get().AddComponent<VisualAnimation_data>(entity, anim);
3. World (ECS System Registration)
// In World::Init()
m_animationSystem = std::make_unique<AnimationSystem>();
m_animationSystem->RegisterComponent<VisualAnimation_data>();
m_animationSystem->RegisterComponent<VisualSprite_data>();
Update Order:
- Physics System (update positions)
- Animation System (update sprites)
- Rendering System (draw sprites)
4. Behavior Tree (AI Integration)
// Custom BT node
class PlayAnimationNode : public BT_Node
{
BT_Status Execute(ECS_Entity entity) override
{
AnimationSystem::Get().PlayAnimation(entity, m_animName);
return BT_Status::SUCCESS;
}
};
// In BT JSON
{
"type": "PlayAnimation",
"animationName": "attack"
}
5. Input System (Player Control)
void HandleInput()
{
if (InputManager::Get().IsActionPressed("move_right"))
{
AnimationSystem::Get().PlayAnimation(player, "walk_right");
}
}
Design Patterns Used
1. Singleton Pattern
- AnimationManager: Centralized resource management
- AnimationSystem: Single system instance
2. Component Pattern (ECS)
- VisualAnimation_data: Pure data component
- AnimationSystem: Behavior on components
3. Flyweight Pattern
- AnimationSequence: Shared across entities
- Textures: Shared via DataManager cache
4. State Pattern
- Animation Graphs: FSM for animation states
- State transitions: Validated state changes
5. Strategy Pattern
- Frame Definition: frameRange vs explicit frames
- Spritesheet Layout: Flexible frame calculation
Future Enhancements
Potential improvements to the system:
Animation Blending
// Blend between two animations
AnimationSystem::BlendAnimations(entity, "walk", "run", 0.5f);
Animation Layers
// Separate upper/lower body animations
layer1: "upper_body_attack"
layer2: "lower_body_walk"
Inverse Kinematics
// Procedural foot placement
AnimationSystem::SetIKTarget(entity, "left_foot", targetPosition);
Animation Curves
// Ease-in/ease-out for smooth transitions
"transitionCurve": "ease-in-out"
"transitionDuration": 0.2
See Also
- API Reference - Complete API documentation
- Animation Banks Reference - JSON format
- Animation Graphs Reference - FSM documentation
- Quick Start Guide - Tutorial