-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Problem to solve
Feature Proposal: BatchedAnimationComponent for High-Volume Animated Instances
Summary
This proposal introduces a new core component, BatchedAnimationComponent, designed to drastically reduce engine overhead when rendering thousands of animated sprites that share the same animation sequence (e.g., particles, ambient effects, large groups of identical enemies).
The implementation leverages manual rendering within a single component and incorporates built-in Frustum Culling, resulting in measured performance gains
The Problem: Component Overhead
When a user needs to display thousands of identical animated elements—such as a large swarm of insects, numerous embers, or a high-density particle field—the standard approach requires creating thousands of separate SpriteAnimationComponent instances.
This approach introduces significant overhead in the engine for every frame:
Component Tree Traversal: The engine must iterate over and manage thousands of entries in the component tree.
Individual update/render Calls: Each of the thousands of components incurs the cost of its own virtual method calls, even if its update method is empty.
Redundant Work: The animation ticker is advanced independently for each component, even though the animation frame should be identical across all instances.
The Solution: BatchedAnimationComponent
The BatchedAnimationComponent resolves this by treating the objects as lightweight data instances rather than heavy-weight components.
Proposal
Core Optimizations:
Manual Rendering and Single Animation Ticker: The component holds a single SpriteAnimation and advances its ticker only once per frame. Its render method then manually iterates over a simple List and calls currentSprite.render(...) for each instance. This bypasses the engine's component management overhead.
Built-in Frustum Culling: The component efficiently checks the bounding box of each instance against game.camera.visibleWorldRect. If an instance is outside the visible area, its render call is immediately skipped, providing massive performance gains on large maps or zoomed-in views.
Proposed Component Structure
Below are the classes that demonstrate the implemented solution:
- AnimatedInstanceData (Lightweight Data Container)
class AnimatedInstanceData {
AnimatedInstanceData({
required this.size,
required this.position,
this.anchor = Anchor.topLeft,
this.overridePaint,
});
Vector2 position;
Anchor anchor;
Vector2 size;
Paint? overridePaint; // Allows per-instance coloring/opacity
}
- BatchedAnimationComponent (The Engine Component)
class BatchedAnimationComponent<T extends FlameGame>
extends SpriteAnimationComponent
with HasGameReference<T> {
BatchedAnimationComponent({
required this.instances,
super.animation,
super.position,
super.size,
super.priority,
super.key,
});
List<AnimatedInstanceData> instances;
// 1. Declare reusable temporary variables outside the render method
// (as private fields of the component)
var _instanceRect = Rect.zero;
@override
void render(Canvas canvas) {
final currentSprite = animationTicker?.getSprite();
if (currentSprite == null) {
return;
}
// Frustum Culling Setup
final Rect visibleRect = game.camera.visibleWorldRect;
final anchorOffset = Vector2.zero(); // Avoid GC by reusing
for (final instance in instances) {
// Calculate bounding box start (minX, minY)
instance.anchor.getOffsetToTopLeft(instance.size).copyInto(anchorOffset);
final minX = instance.position.x - anchorOffset.x;
final minY = instance.position.y - anchorOffset.y;
_instanceRect = Rect.fromLTWH(
minX,
minY,
instance.size.x,
instance.size.y,
);
// Culling Check
if (!visibleRect.overlaps(_instanceRect)) {
continue;
}
// Render the single sprite instance
final renderPaint = instance.overridePaint ?? paint;
currentSprite.render(
canvas,
position: instance.position,
size: instance.size,
anchor: instance.anchor,
overridePaint: renderPaint,
);
}
}
}
More information
No response
Other
- Are you interested in working on a PR for this?