Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/.cspell/flame_dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Kenobi # Eminent Jedi Master, General of the Republic Army, Obi-Wan Kenobi
Nakama # An open-source server designed to power modern games and apps https://github.com/Allan-Nava/nakama-flutter
Overmind # A character in the game StarCraft
padracing # A pad racing game by BlueFire https://github.com/flame-engine/flame/tree/main/examples/games/padracing
Poolable # A mixin for components that can be used with ComponentPool
Prosser # A character from the book The Hitchhiker's Guide to the Galaxy
riverpod # A state management library for Flutter https://github.com/rrousselGit/riverpod
spineboy # Name of a famous character used as an example for Spine https://en.esotericsoftware.com/spine-examples-spineboy
Expand Down
150 changes: 150 additions & 0 deletions doc/flame/other/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,153 @@ It is up to the developers to decide which hitboxes can be made passive based on
the rules of the game. For example, the Rogue Shooter game in Flame's examples uses
passive hitbox for enemies instead of the bullets.
```


## Object Pooling

As mentioned in the "Object creation per frame" section, creating and destroying objects
frequently can impact performance. For components that are spawned and removed repeatedly
(like bullets, particles, or enemies), object pooling is an effective optimization technique.

Object pooling reuses objects instead of constantly creating and destroying them. Flame
provides the `ComponentPool` class and the `Poolable` mixin to make object pooling easy
and efficient.


### Poolable Mixin

The `Poolable` mixin is used to mark components that can be pooled and reused. Any component
that uses this mixin must implement the `reset()` method, which resets the component to its
initial state so it can be reused.

Example:

```dart
class Bullet extends SpriteComponent with Poolable {
Vector2 velocity = Vector2.zero();
double damage = 10.0;

@override
void reset() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we just tell the users to do this in onMount, then we don't need a new method?

position.setZero();
size.setZero();
velocity.setZero();
damage = 10.0;
// Reset any other properties to their initial state
}

@override
void update(double dt) {
super.update(dt);
position.add(velocity * dt);
}
}
```


### ComponentPool

The `ComponentPool` class manages a pool of reusable components. It handles the lifecycle of
pooled components, including acquiring them when needed and releasing them back to the pool
when they're no longer in use.

**Creating a pool:**

```dart
class MyGame extends FlameGame {
late final ComponentPool<Bullet> bulletPool;

@override
Future<void> onLoad() async {
bulletPool = ComponentPool<Bullet>(
factory: () => Bullet(),
maxSize: 50, // Maximum number of bullets to keep in the pool
initialSize: 10, // Pre-create 10 bullets for immediate use
);
}
}
```

**Acquiring components from the pool:**

When you need a component, use `acquire()` to get one from the pool. If the pool is empty,
a new component will be created automatically.

```dart
void spawnBullet(Vector2 position, Vector2 velocity) {
final bullet = bulletPool.acquire();
bullet.position.setFrom(position);
bullet.velocity.setFrom(velocity);
world.add(bullet);
}
```

**Releasing components back to the pool:**

When a component is no longer needed, release it back to the pool using `release()`. This
will automatically remove the component from the game tree if it's mounted, reset it using
the `reset()` method, and add it back to the pool for reuse.

```dart
class Bullet extends SpriteComponent with Poolable {
// ... other code ...

@override
void update(double dt) {
super.update(dt);
position.add(velocity * dt);

// Remove bullet if it goes off screen
if (position.x < -100 || position.x > game.size.x + 100) {
game.bulletPool.release(this);
}
}

void onCollision() {
// Release back to pool when bullet hits something
game.bulletPool.release(this);
}
}
```


### Pool Management

**Checking available components:**

You can check how many components are currently available in the pool:

```dart
print('Available bullets: ${bulletPool.availableCount}');
```

**Clearing the pool:**

If you need to free up memory or reset the pool, you can clear all available components:

```dart
bulletPool.clear();
```

```{note}
Clearing only affects components currently in the pool. Components that are in use
(acquired but not yet released) are not affected.
```


### Best Practices

1. **Always release components**: Make sure every acquired component is eventually released
back to the pool to prevent memory leaks.

2. **Reset thoroughly**: Implement `reset()` carefully to reset all component properties to
their initial state. Missing properties can cause bugs when components are reused.

3. **Set appropriate pool sizes**: Set `maxSize` based on your game's needs. Too small and
you'll create new objects frequently; too large and you'll waste memory.

4. **Use initialSize for warm-up**: Set `initialSize` to pre-create commonly used components,
reducing frame drops during gameplay.

5. **Pool behavior is LIFO**: The pool uses a stack (Last In First Out) internally, meaning
the most recently released component will be the next one acquired.
173 changes: 173 additions & 0 deletions examples/lib/stories/components/component_pool_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

class ComponentPoolExample extends FlameGame {
static const String description =
'Tap on the screen to spawn a burst of pooled bullets. '
'Watch the stats to see active vs pooled bullets and observe '
'how the pool efficiently reuses objects.';

static const gameWidth = 800.0;
static const gameHeight = 600.0;

ComponentPoolExample()
: super(
world: _BulletWorld(),
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
) {
camera.moveTo(Vector2(gameWidth / 2, gameHeight / 2));
}
}

class _BulletWorld extends World with TapCallbacks {
late final ComponentPool<_PooledBullet> bulletPool;
late final _StatsDisplay statsDisplay;
final Random _random = Random();

static const bulletsPerTap = 12;

@override
Future<void> onLoad() async {
// Add a background
add(
RectangleComponent(
size: Vector2(
ComponentPoolExample.gameWidth,
ComponentPoolExample.gameHeight,
),
paint: Paint()..color = Colors.green,
),
);

// Create a pool with a larger initial size to handle bursts
// and a maximum size to accommodate multiple bullet bursts
bulletPool = ComponentPool<_PooledBullet>(
factory: _PooledBullet.new,
initialSize: 30,
maxSize: 200,
);

// Add a stats display to show pool information
statsDisplay = _StatsDisplay(pool: bulletPool);
await add(statsDisplay);
}

@override
void onTapDown(TapDownEvent event) {
final tapPosition = event.localPosition;

for (var i = 0; i < bulletsPerTap; i++) {
final bullet = bulletPool.acquire();
bullet.position.setFrom(tapPosition);

// Create a spread pattern - bullets go out in all directions
final angle = (i / bulletsPerTap) * 2 * pi;
final speed = 150.0 + _random.nextDouble() * 100;
bullet.velocity = Vector2(cos(angle) * speed, sin(angle) * speed);

// Add slight random variation to velocity
bullet.velocity.add(
Vector2(
(_random.nextDouble() - 0.5) * 30,
(_random.nextDouble() - 0.5) * 30,
),
);

add(bullet);
}
}
}

/// A simple bullet component that can be pooled.
class _PooledBullet extends CircleComponent
with Poolable, HasGameReference, ParentIsA<_BulletWorld> {
Vector2 velocity = Vector2.zero();

_PooledBullet()
: super(
radius: 4,
paint: Paint()
..color = Colors.yellowAccent
..style = PaintingStyle.fill,
);

@override
void reset() {
// Reset all properties to their initial state
position.setZero();
velocity.setZero();
angle = 0;
}

@override
void update(double dt) {
position.add(velocity * dt);

// Check if bullet is outside the game bounds
final isOutOfBounds =
position.x < 0 ||
position.x > ComponentPoolExample.gameWidth ||
position.y < 0 ||
position.y > ComponentPoolExample.gameHeight;

if (isOutOfBounds) {
parent.bulletPool.release(this);
}
}

@override
void render(Canvas canvas) {
// Draw a glow effect
final glowPaint = Paint()
..color = Colors.black.withValues(alpha: 0.3)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3);
canvas.drawCircle(Offset.zero, radius * 2, glowPaint);

// Draw the main bullet
super.render(canvas);
}
}

/// Displays statistics about the bullet pool.
class _StatsDisplay extends TextComponent with ParentIsA<_BulletWorld> {
final ComponentPool<_PooledBullet> pool;
int _activeBullets = 0;

_StatsDisplay({required this.pool})
: super(
position: Vector2(10, 10),
textRenderer: TextPaint(
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2,
),
],
),
),
);

@override
void update(double dt) {
super.update(dt);

// Count active bullets (in the world but not in the pool)
_activeBullets = parent.children.whereType<_PooledBullet>().length;

text =
'Active Bullets: $_activeBullets\n'
'In Pool (Ready): ${pool.availableCount}\n'
'\nTap to spawn ${_BulletWorld.bulletsPerTap} bullets!';
}
}
9 changes: 9 additions & 0 deletions examples/lib/stories/components/components.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dashbook/dashbook.dart';
import 'package:examples/commons/commons.dart';
import 'package:examples/stories/components/clip_component_example.dart';
import 'package:examples/stories/components/component_pool_example.dart';
import 'package:examples/stories/components/components_notifier_example.dart';
import 'package:examples/stories/components/components_notifier_provider_example.dart';
import 'package:examples/stories/components/composability_example.dart';
Expand Down Expand Up @@ -41,6 +42,14 @@ void addComponentsStories(Dashbook dashbook) {
codeLink: baseLink('components/clip_component_example.dart'),
info: ClipComponentExample.description,
)
..add(
'Component Pool',
(_) => const GameWidget.controlled(
gameFactory: ComponentPoolExample.new,
),
codeLink: baseLink('components/component_pool_example.dart'),
info: ComponentPoolExample.description,
)
..add(
'Look At',
(_) => GameWidget(game: LookAtExample()),
Expand Down
2 changes: 2 additions & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export 'src/camera/world.dart' show World;
export 'src/collisions/has_collision_detection.dart';
export 'src/collisions/hitboxes/screen_hitbox.dart';
export 'src/components/clip_component.dart';
export 'src/components/component_pool.dart';
export 'src/components/components_notifier.dart';
export 'src/components/core/component.dart';
export 'src/components/core/component_key.dart';
Expand Down Expand Up @@ -35,6 +36,7 @@ export 'src/components/mixins/ignore_events.dart';
export 'src/components/mixins/keyboard_handler.dart';
export 'src/components/mixins/notifier.dart';
export 'src/components/mixins/parent_is_a.dart';
export 'src/components/mixins/poolable.dart';
export 'src/components/mixins/single_child_particle.dart';
export 'src/components/mixins/snapshot.dart';
export 'src/components/nine_tile_box_component.dart';
Expand Down
Loading