Skip to content

new BatchedAnimationComponent  #3774

@s1r1m1r1

Description

@s1r1m1r1

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:

  1. 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
}
  1. 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?

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions