Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2e95135
feat!: Make updateTree non-virtual behind a CustomTraversal seam
spydon Jul 22, 2026
8c20184
perf!: Drive the update pass from a root-owned flattened traversal list
spydon Jul 22, 2026
df368b7
feat: Add updatePaused for pausing the update pass of a subtree
spydon Jul 22, 2026
5d937ba
docs: Document CustomTraversal, updatePaused, and the HasTimeScale re…
spydon Jul 22, 2026
88a36db
perf: Rebuild the flat update list through internal arrays and cache …
spydon Jul 22, 2026
906dd72
perf: Fuse the flat-list rebuild into the update pass and remove hot-…
spydon Jul 22, 2026
1fd0b71
refactor!: Move updateSubtree onto Component so traversal mixins carr…
spydon Jul 22, 2026
3689bb4
refactor: Use update and updatePaused in examples that do not need a …
spydon Jul 22, 2026
ad41cfa
refactor!: Base Route.stopTime on updatePaused instead of zeroing tim…
spydon Jul 22, 2026
dc774bf
style: Spell out abbreviated identifiers introduced by this branch
spydon Jul 22, 2026
218606a
style: Finish the set-to-list rename in names and docs
spydon Jul 22, 2026
bee9410
refactor!: Turn CustomTraversal into a marker interface instead of an…
spydon Jul 22, 2026
f21ed29
test: Pin that HasTimeScale scales the dt of the component it is mixe…
spydon Jul 22, 2026
8d73cbd
perf: Iterate query caches with indexed loops in clear and rebalance
spydon Jul 22, 2026
6d62c4c
docs: Add the FCS core rewrite to the v2.0.0 migration guide
spydon Jul 22, 2026
0e0ca0c
perf: Collect the removal teardown through the backing array
spydon Aug 5, 2026
659f50c
refactor: Remove updatePaused in favor of gating updateSubtree in Route
spydon Aug 26, 2026
27956f0
style: Keep the mid-list wording in the ComponentList docs
spydon Aug 28, 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
21 changes: 21 additions & 0 deletions doc/flame/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ 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`.


### Composability of components

Sometimes it is useful to wrap other components inside of your component. For example by grouping
Expand Down
74 changes: 74 additions & 0 deletions doc/flame/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,77 @@ if (game.isPaused) {
game.isPaused = false;
}
```


### `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 stop updating a subtree, gate `updateSubtree` in the same way, which is what
`Route.stopTime()` does.
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,13 @@ mixin GameCollidable on PositionComponent {

//#region Utils

mixin UpdateOnce on PositionComponent {
mixin UpdateOnce on PositionComponent implements CustomTraversal {
bool updateOnce = true;

@override
void updateTree(double dt) {
void updateSubtree(double dt) {
if (updateOnce) {
super.updateTree(dt);
super.updateSubtree(dt);
updateOnce = false;
}
}
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
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
98 changes: 91 additions & 7 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class Component {
int? priority,
this.key,
}) : _priority = priority ?? 0 {
_isTraversalBarrier = this is CustomTraversal;
if (children != null) {
addAll(children);
}
Expand Down Expand Up @@ -283,9 +284,9 @@ class Component {
///
/// ```dart
/// coin.parent = inventory;
/// // The inventory.children set does not include coin yet.
/// // The inventory.children list does not include coin yet.
/// await game.lifecycleEventsProcessed;
/// // The inventory.children set now includes coin.
/// // The inventory.children list now includes coin.
/// ```
Component? get parent => _parent;
Component? _parent;
Expand All @@ -298,7 +299,7 @@ class Component {
}

/// This field should be used internally for functionality when you don't need
/// to create a component set for the children if one doesn't already exist.
/// to create a children list if one doesn't already exist.
///
/// This makes it possible to have lighter components that don't have any
/// children.
Expand All @@ -321,7 +322,7 @@ class Component {
int _containerIndex = -1;

/// This field should be used internally for functionality when you need to
/// make sure that the component set is created if it doesn't already exist.
/// make sure that the children list is created if it doesn't already exist.
ComponentList get _internalChildren => _children ??= createComponentList();

/// Restores the priority ordering of the [children], after one or more of
Expand Down Expand Up @@ -539,7 +540,7 @@ class Component {
/// its [children] yet.
///
/// After this method completes, the component is added to the parent's
/// children set, and then the flag [isMounted] set to true.
/// children list, and then the flag [isMounted] set to true.
///
/// Example:
/// ```dart
Expand Down Expand Up @@ -587,7 +588,90 @@ class Component {
/// This method traverses the component tree and calls [update] on all its
/// children according to their [priority] order, relative to the
/// priority of the direct siblings, not the children or the ancestors.
///
/// This method is non-virtual: components that need to customize how their
/// subtree is traversed (changing the effective [dt], skipping children,
/// or updating them manually) should mix in `CustomTraversal` and override
/// its `updateSubtree` method instead. The marker mixin lets the engine's
/// flattened update pass treat such components as traversal barriers
/// instead of silently skipping their custom logic.
@nonVirtual
void updateTree(double dt) {
if (_isTraversalBarrier) {
updateSubtree(dt);
} else {
defaultUpdateSubtree(dt);
}
}

/// Updates this component and its subtree.
///
/// The engine only invokes this method for components that are marked with
/// the [CustomTraversal] mixin, either directly or through a mixin that
/// `implements` it (such as `HasTimeScale`). The marker is what makes the
/// flattened update pass treat the component as a traversal barrier;
/// overriding this method without the marker has no effect.
///
/// Call `super.updateSubtree` to run the surrounding traversal (the
/// standard one, or the next custom traversal in the mixin chain),
/// possibly with a modified time delta.
void updateSubtree(double dt) => defaultUpdateSubtree(dt);

/// Whether this component manages its own subtree traversal. Evaluated
/// once in the constructor, so that the per-frame traversal loops pay a
/// plain field load instead of a type check.
bool _isTraversalBarrier = false;

/// Runs one update pass over a flattened traversal list produced by
/// [updateAndFlattenInto].
@internal
static void updateFlatList(List<Component> list, double dt) {
for (var i = 0; i < list.length; i++) {
final component = list[i];
if (component._isTraversalBarrier) {
component.updateSubtree(dt);
} else {
component.update(dt);
}
}
}

/// Combined update pass and flatten: updates this component's subtree
/// recursively while appending the visited components to [out], in
/// pre-order with children in priority order, stopping at (but including)
/// `CustomTraversal` barriers. Used by the root on ticks where the
/// structure changed, so that the flat-list rebuild does not cost a
/// separate pass over the tree.
@internal
void updateAndFlattenInto(List<Component> out, double dt) {
final children = _children;
if (children == null) {
return;
}
children._compact();
final elements = children._elements;
for (var i = 0; i < elements.length; i++) {
final child = elements[i];
if (child == null) {
continue;
}
out.add(child);
if (child._isTraversalBarrier) {
child.updateSubtree(dt);
} else {
child.update(dt);
child.updateAndFlattenInto(out, dt);
}
}
}

/// The engine's standard update traversal: update this component, then
/// update the children in priority order.
///
/// This is the default behavior of `CustomTraversal.updateSubtree`;
/// custom traversals can call it to delegate to the standard behavior.
@protected
void defaultUpdateSubtree(double dt) {
update(dt);
final children = _children;
if (children != null) {
Expand Down Expand Up @@ -1229,8 +1313,8 @@ class Component {
out ??= [];
final children = _children;
if (children != null) {
for (final child in children.reversed) {
child._collectDescendants(out);
for (final child in children._elements.reversed) {
child?._collectDescendants(out);
}
}
out.add(this);
Expand Down
Loading
Loading