Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8e2dcb2
perf!: Replace OrderedSet children container with flat sorted-array C…
spydon Jul 22, 2026
e38a2b4
docs: Update children container docs after OrderedSet removal
spydon Jul 22, 2026
de59d60
refactor!: Rename ComponentSet to ComponentList, replace childrenFact…
spydon Jul 22, 2026
b25dcf4
test: Add golden tests for lifecycle ordering, hit-test order, and eq…
spydon Jul 22, 2026
0b71600
perf: Early-return from processLifecycleEvents when the queue is empty
spydon Jul 22, 2026
764af8e
perf: Cache decorator render closures instead of allocating per frame
spydon Jul 22, 2026
51e20e8
perf: Make render contexts and debug caches lazily allocated
spydon Jul 22, 2026
8c5b31c
perf: Replace generator-based removal teardown with an explicit colle…
spydon Jul 22, 2026
d74ba81
perf: Track mounted pointer-event handlers so hit tests can early-out
spydon Jul 22, 2026
bcdc1b7
feat!: Make updateTree non-virtual behind a CustomTraversal seam
spydon Jul 22, 2026
d62a993
perf!: Drive the update pass from a root-owned flattened traversal list
spydon Jul 22, 2026
0884307
feat: Add updatePaused for pausing the update pass of a subtree
spydon Jul 22, 2026
8fabe08
docs: Document CustomTraversal, updatePaused, and the HasTimeScale re…
spydon Jul 22, 2026
800beda
perf: Rebuild the flat update list through internal arrays and cache …
spydon Jul 22, 2026
9fcefd1
perf: Fuse the flat-list rebuild into the update pass and remove hot-…
spydon Jul 22, 2026
845b90d
perf: Insertion-sort the sweep broadphase, cache camera render closur…
spydon Jul 22, 2026
ab24401
refactor!: Move updateSubtree onto Component so traversal mixins carr…
spydon Jul 22, 2026
8d53fb8
refactor: Use update and updatePaused in examples that do not need a …
spydon Jul 22, 2026
d652a6c
refactor!: Base Route.stopTime on updatePaused instead of zeroing tim…
spydon Jul 22, 2026
7d2a4e7
style: Spell out abbreviated identifiers introduced by this branch
spydon Jul 22, 2026
7e3569f
style: Finish the set-to-list rename in names and docs
spydon Jul 22, 2026
17a0b84
refactor!: Turn CustomTraversal into a marker interface instead of an…
spydon Jul 22, 2026
00bf07f
test: Pin that HasTimeScale scales the dt of the component it is mixe…
spydon Jul 22, 2026
fb203da
perf: Iterate query caches with indexed loops in clear and rebalance
spydon Jul 22, 2026
b083b5f
docs: Add the FCS core rewrite to the v2.0.0 migration guide
spydon Jul 22, 2026
e187790
Update doc/flame/migration.md
spydon Jul 23, 2026
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
34 changes: 33 additions & 1 deletion doc/flame/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,38 @@ class MyComponent extends PositionComponent with TapCallbacks {
```


### Custom update traversal and pausing

The engine drives the update pass through a flattened traversal list owned by the game, so
`updateTree` is non-virtual and cannot be overridden. Components that need to control how their
subtree is updated (changing the effective `dt`, skipping children, or updating them manually)
should implement the `CustomTraversal` marker and override the `updateSubtree` method:

```dart
class SlowMotionArea extends Component implements CustomTraversal {
@override
void updateSubtree(double dt) => super.updateSubtree(dt / 2);
}
```

The engine treats every `CustomTraversal` component as a traversal barrier: it appears in the
flattened list itself and its `updateSubtree` drives its subtree. `updateSubtree` lives on
`Component`, but it is only invoked for components carrying the marker. Mixins that provide a
custom traversal (like `HasTimeScale`) declare `implements CustomTraversal`, so their users do not
need to add the marker themselves, and chain via `super.updateSubtree`.

To temporarily stop updating a component and its whole subtree, set `updatePaused` to true. While
paused, no `update` calls happen in the subtree, but rendering and event handling continue, and
pending lifecycle events (adds and removes) are still processed:

```dart
enemySquad.updatePaused = true; // freeze the squad
enemySquad.updatePaused = false; // resume it
```

This is unrelated to `Game.paused`, which stops the whole game loop including rendering.


### Composability of components

Sometimes it is useful to wrap other components inside of your component. For example by grouping
Expand Down Expand Up @@ -331,7 +363,7 @@ flameGame.findByKeyName('player');

### Querying child components

The children that have been added to a component live in a `QueryableOrderedSet` called
The children that have been added to a component live in a `ComponentList` called
`children`. To query for a specific type of components in the set, the `query<T>()` function can be
used. By default `strictMode` is `false` in the children set, but if you set it to true, then the
queries will have to be registered with `children.register` before a query can be used.
Expand Down
86 changes: 86 additions & 0 deletions doc/flame/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,89 @@ GameWidget.managed(
gameFactory: MyGame.new,
);
```


### `children` is now a `ComponentList` instead of an `OrderedSet`
Comment thread
spydon marked this conversation as resolved.

The `ordered_set` package is no longer used; children live in a Flame-owned `ComponentList` that
is significantly faster. The iterable surface, `query<T>()`, `register<T>()`, and `reversed()` are
unchanged, so most code compiles as is. If you imported `package:ordered_set` types to annotate
variables, use `ComponentList` (from `package:flame/components.dart`) instead:

```dart
// Before
import 'package:ordered_set/ordered_set.dart';
OrderedSet<Component> children = component.children;

// After
ComponentList children = component.children;
```

Two behavioral notes:

- `query<T>()` results are now always in priority order.
- Mutating `children` while iterating it now tolerates removals and appends at the end; only
position-shifting operations (a mid-list insertion, a reorder, or tombstone compaction) throw
`ConcurrentModificationError`.


### `Component.childrenFactory` is removed

The global children-container factory is gone. Override `createComponentList()` on the component
instead. The constructor accepts an optional `Comparator<Component>` that replaces priority
ordering for that parent, which gives custom orderings such as y-sort a supported home:

```dart
// Before
Component.childrenFactory = () => OrderedSet.mapping<num, Component>((c) => c.priority);

// After
class YSortedWorld extends World {
@override
ComponentList createComponentList() {
return ComponentList(
comparator: (a, b) => (a as PositionComponent)
.position.y
.compareTo((b as PositionComponent).position.y),
);
}
}
```


### `Component.updateTree` is non-virtual

The update pass runs over a flattened traversal list owned by the game, so `updateTree` can no
longer be overridden. If you overrode it, implement the `CustomTraversal` marker interface and
override `Component.updateSubtree` instead; call `super.updateSubtree(dt)` to run the standard
traversal:

```dart
// Before
class SlowMotionArea extends Component {
@override
void updateTree(double dt) => super.updateTree(dt / 2);
}

// After
class SlowMotionArea extends Component implements CustomTraversal {
@override
void updateSubtree(double dt) => super.updateSubtree(dt / 2);
}
```

`HasTimeScale` usage is unchanged (`with HasTimeScale` still works; the mixin carries the marker
itself). To simply stop updating a subtree, the new `updatePaused` flag replaces the common
gating override: `component.updatePaused = true` pauses the update pass for the component and its
whole subtree while rendering, event handling, and lifecycle processing continue.


### `Route.stopTime()` no longer zeroes `timeScale`

A stopped route is now paused through `updatePaused` instead of `timeScale = 0`:

- While stopped, `timeScale` keeps its previous value, so a slow-motion factor survives a
stop/resume cycle.
- Assigning a new `timeScale` no longer resumes a stopped route; use `resumeTime()` or
`updatePaused = false`.
- Pending lifecycle events (adding or removing components) on a stopped route now still complete.
13 changes: 7 additions & 6 deletions examples/lib/stories/collision_detection/quadtree_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,16 @@ mixin GameCollidable on PositionComponent {

//#region Utils

/// Lets the subtree update once and then pauses it; set [updateOnce] back to
/// true to let it update once more.
mixin UpdateOnce on PositionComponent {
bool updateOnce = true;
bool get updateOnce => !updatePaused;
set updateOnce(bool value) => updatePaused = !value;

@override
void updateTree(double dt) {
if (updateOnce) {
super.updateTree(dt);
updateOnce = false;
}
void update(double dt) {
super.update(dt);
updatePaused = true;
}
}

Expand Down
4 changes: 2 additions & 2 deletions examples/lib/stories/components/time_scale_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ class _Chopper extends SpriteAnimationComponent
}

@override
void updateTree(double dt) {
void update(double dt) {
position.setFrom(position + _moveDirection * _speed * dt);
super.updateTree(dt);
super.update(dt);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class LayoutDemo1 extends LinearLayoutComponent {
_expandedMode = value;
removeAll(children.toList());
addAll(
createComponentList(
createLayoutChildren(
expandedMode: expandedMode,
padding: padding,
inflateChild: paddingInflateChild,
Expand All @@ -122,7 +122,7 @@ class LayoutDemo1 extends LinearLayoutComponent {
FutureOr<void> onLoad() {
super.onLoad();
addAll(
createComponentList(
createLayoutChildren(
expandedMode: expandedMode,
padding: padding,
inflateChild: paddingInflateChild,
Expand All @@ -137,7 +137,7 @@ class LayoutDemo1 extends LinearLayoutComponent {
/// This needs to be a method rather than a static list
/// because each of these components needs to be recreated.
/// Otherwise, they'll be operated on by reference and re-parented.
static List<Component> createComponentList({
static List<Component> createLayoutChildren({
required bool expandedMode,
required EdgeInsets padding,
required bool inflateChild,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class LayoutDemo2 extends LinearLayoutComponent {
FutureOr<void> onLoad() {
super.onLoad();
addAll(
createComponentList(
createLayoutChildren(
direction: direction,
),
);
Expand All @@ -95,7 +95,7 @@ class LayoutDemo2 extends LinearLayoutComponent {
/// This needs to be a method rather than a static list
/// because each of these components needs to be recreated.
/// Otherwise, they'll be operated on by reference and re-parented.
static List<Component> createComponentList({
static List<Component> createLayoutChildren({
required Direction direction,
}) {
return [
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export 'src/components/components_notifier.dart';
export 'src/components/core/component.dart';
export 'src/components/core/component_key.dart';
export 'src/components/core/component_render_context.dart';
export 'src/components/core/custom_traversal.dart';
export 'src/components/custom_painter_component.dart';
export 'src/components/fps_component.dart';
export 'src/components/fps_text_component.dart';
Expand Down
36 changes: 21 additions & 15 deletions packages/flame/lib/src/camera/camera_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,6 @@ class CameraComponent extends Component {
canvas.save();
try {
currentCameras.add(this);
void renderWorld(Canvas canvas) {
canvas.transform2D(viewfinder.transform);
world!.renderFromCamera(canvas);

// Render the viewfinder elements, which will be in front of
// the world,
// but with the same transforms applied to them.
viewfinder.renderTree(canvas);
}

final postProcessors = children.query<PostProcessComponent>();
if (postProcessors.isNotEmpty) {
assert(
Expand All @@ -223,13 +213,11 @@ class CameraComponent extends Component {
postProcessor.render(
canvas,
viewport.virtualSize,
renderWorld,
(context) {
renderContext.currentPostProcess = context;
},
_renderWorld,
_updatePostProcessContext,
);
} else {
renderWorld(canvas);
_renderWorld(canvas);
}
} finally {
currentCameras.removeLast();
Expand All @@ -242,6 +230,24 @@ class CameraComponent extends Component {
canvas.restore();
}

/// Renders the world and the viewfinder elements through the camera
/// transform. An instance method rather than a local function, so that the
/// render pass does not allocate a closure per camera per frame.
void _renderWorld(Canvas canvas) {
canvas.transform2D(viewfinder.transform);
world!.renderFromCamera(canvas);
// Render the viewfinder elements, which will be in front of the world,
// but with the same transforms applied to them.
viewfinder.renderTree(canvas);
}

// Not a setter: this is passed as a `ValueSetter` tear-off to
// `PostProcess.render`.
// ignore: use_setters_to_change_properties
void _updatePostProcessContext(PostProcess? context) {
renderContext.currentPostProcess = context;
}

/// Converts from the global (canvas) coordinate space to
/// local (camera = viewport + viewfinder).
///
Expand Down
21 changes: 19 additions & 2 deletions packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {

@override
void update() {
items.sort((a, b) => a.aabb.min.x.compareTo(b.aabb.min.x));
// Between two ticks the hitboxes only move a little, so [items] is
// always nearly sorted: an insertion sort runs in close to linear time
// here, where a general-purpose sort would pay its full O(n log n) on
// every tick. This also avoids allocating a comparator closure per tick.
final items = this.items;
for (var i = 1; i < items.length; i++) {
final item = items[i];
final minX = item.aabb.min.x;
var previousIndex = i - 1;
while (previousIndex >= 0 && items[previousIndex].aabb.min.x > minX) {
items[previousIndex + 1] = items[previousIndex];
previousIndex--;
}
items[previousIndex + 1] = item;
}
}

@override
Expand All @@ -44,7 +58,10 @@ class Sweep<T extends Hitbox<T>> extends Broadphase<T> {
_prospectPool.acquire(item, activeItem);
}
} else {
_active.remove(activeItem);
// The order of the active list does not matter, so the removal can
// swap in the last element instead of searching and shifting.
_active[i] = _active.last;
_active.removeLast();
}
}
_active.add(item);
Expand Down
Loading
Loading