Skip to content
Merged
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
171 changes: 156 additions & 15 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -428,22 +428,21 @@ class Component {
/// traverse the component tree when required. This allows efficient chaining
/// of various iterable methods, such as filtering, early stopping, folding,
/// and so on -- see the documentation of the [Iterable] class for details.
/// The traversal is driven by a single explicit stack of children iterators,
/// so its cost does not grow with the depth of the tree beyond that stack.
///
/// Modifying the tree while iterating may throw a
/// [ConcurrentModificationError], exactly as iterating over [children] does.
/// Call `toList()` first to iterate over a snapshot instead.
Iterable<Component> descendants({
bool includeSelf = false,
bool reversed = false,
}) sync* {
if (includeSelf && !reversed) {
yield this;
}
if (hasChildren) {
final childrenIterable = reversed ? children.reversed : children;
for (final child in childrenIterable) {
yield* child.descendants(includeSelf: true, reversed: reversed);
}
}
if (includeSelf && reversed) {
yield this;
}
}) {
return _DescendantsIterable(
this,
includeSelf: includeSelf,
reversed: reversed,
);
}

/// This method first calls the passed handler on the leaves in the tree,
Expand Down Expand Up @@ -1395,8 +1394,7 @@ class Component {
/// order that `descendants(reversed: true, includeSelf: true)` would
/// produce: leaves first, siblings in reverse order, ancestors after their
/// subtrees. The snapshot allows [_remove] to run user callbacks that
/// mutate the tree while it walks the subtree, without allocating generator
/// frames per tree level the way [descendants] does.
/// mutate the tree while it walks the subtree.
///
/// The [out] parameter is only used by the recursive calls, so that the
/// whole subtree is collected into a single list.
Expand Down Expand Up @@ -1513,6 +1511,149 @@ class Component {

enum ChildrenChangeType { added, removed }

/// The lazy [Iterable] returned by [Component.descendants].
class _DescendantsIterable extends Iterable<Component> {
_DescendantsIterable(
this._root, {
required this.includeSelf,
required this.reversed,
});

final Component _root;
final bool includeSelf;
final bool reversed;

@override
Iterator<Component> get iterator => reversed
? _ReversedDescendantsIterator(_root, includeSelf: includeSelf)
: _DescendantsIterator(_root, includeSelf: includeSelf);
}

/// Walks a subtree depth-first in preorder: every component is emitted before
/// its children, and siblings are visited in the order of their container.
///
/// The walk keeps one children iterator per level on an explicit stack, so the
/// only allocations are the stack itself and the container iterators, no
/// matter how deep the tree is. The children iterator of an emitted component
/// is only created on the following [moveNext], after the caller has processed
/// the component itself, so a caller may still reorder the children of the
/// component it just received.
class _DescendantsIterator implements Iterator<Component> {
_DescendantsIterator(Component root, {required bool includeSelf})
: _pendingRoot = includeSelf ? root : null,
_pendingPush = includeSelf ? null : root;

/// The root, while it still has to be emitted.
Component? _pendingRoot;

/// The component whose children iterator has not been pushed yet.
Component? _pendingPush;

final List<Iterator<Component>> _stack = [];
Component? _current;

@override
Component get current => _current!;

@override
bool moveNext() {
final pendingRoot = _pendingRoot;
if (pendingRoot != null) {
_pendingRoot = null;
_pendingPush = pendingRoot;
_current = pendingRoot;
return true;
}
final pendingPush = _pendingPush;
if (pendingPush != null) {
_pendingPush = null;
final children = pendingPush._children;
if (children != null && children.isNotEmpty) {
_stack.add(children.iterator);
}
}
final stack = _stack;
while (stack.isNotEmpty) {
final iterator = stack.last;
if (iterator.moveNext()) {
final component = iterator.current;
_current = component;
_pendingPush = component;
return true;
}
stack.removeLast();
}
_current = null;
return false;
}
}

/// Walks a subtree in the exact reverse of [_DescendantsIterator]: siblings
/// are visited in reverse container order, and every component is emitted
/// after its children.
///
/// A parent is emitted when its children iterator is exhausted, so the walk
/// keeps the parent of every open iterator on a parallel stack. The root's
/// children iterator is only created on the first [moveNext], so the tree is
/// not touched before iteration starts.
class _ReversedDescendantsIterator implements Iterator<Component> {
_ReversedDescendantsIterator(this._root, {required bool includeSelf})
: _pendingRoot = includeSelf;

final Component _root;
final List<Component> _parents = [];
final List<Iterator<Component>> _stack = [];
bool _pendingRoot;
bool _started = false;
Component? _current;

@override
Component get current => _current!;

bool _push(Component component) {
final children = component._children;
if (children != null && children.isNotEmpty) {
_parents.add(component);
_stack.add(children.reversed.iterator);
return true;
}
return false;
}

@override
bool moveNext() {
if (!_started) {
_started = true;
_push(_root);
}
final stack = _stack;
while (stack.isNotEmpty) {
final iterator = stack.last;
if (iterator.moveNext()) {
final component = iterator.current;
if (!_push(component)) {
_current = component;
return true;
}
} else {
stack.removeLast();
final parent = _parents.removeLast();
if (!identical(parent, _root)) {
_current = parent;
return true;
}
}
}
if (_pendingRoot) {
_pendingRoot = false;
_current = _root;
return true;
}
_current = null;
return false;
}
}

/// Lifecycle futures for a group of components, mirroring the ones available on
/// a single [Component].
extension ComponentIterableExtension on Iterable<Component> {
Expand Down
120 changes: 120 additions & 0 deletions packages/flame/test/components/component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1679,6 +1679,126 @@ void main() {
expect(componentE.visited, false);
});

testWithFlameGame(
'descendants() of a component without children',
(game) async {
final leaf = Component();
await game.world.ensureAdd(leaf);

expect(leaf.descendants().toList(), isEmpty);
expect(leaf.descendants(reversed: true).toList(), isEmpty);
expect(leaf.descendants(includeSelf: true).toList(), [leaf]);
expect(
leaf.descendants(includeSelf: true, reversed: true).toList(),
[leaf],
);
},
);

testWithFlameGame(
'descendants() order matches on a deep chain with side branches',
(game) async {
final world = game.world;
final expectedOrder = <Component>[];
Component parent = world;
for (var level = 0; level < 50; level++) {
final sideBranch = Component()..addToParent(parent);
final next = Component()..addToParent(parent);
expectedOrder
..add(sideBranch)
..add(next);
parent = next;
}
await game.ready();

expect(world.descendants().toList(), expectedOrder);
expect(
world.descendants(reversed: true).toList(),
expectedOrder.reversed.toList(),
);
},
);

testWithFlameGame(
'descendants() iterator can be resumed after exhaustion',
(game) async {
final componentA = Component()..addToParent(game.world);
final componentB = Component()..addToParent(componentA);
await game.ready();

final iterator = game.world.descendants().iterator;
expect(iterator.moveNext(), isTrue);
expect(iterator.current, componentA);
expect(iterator.moveNext(), isTrue);
expect(iterator.current, componentB);
expect(iterator.moveNext(), isFalse);
expect(iterator.moveNext(), isFalse);
},
);

testWithFlameGame(
'descendants() throws when the container being walked is reordered',
(game) async {
final parent = Component();
await game.world.ensureAdd(parent);
final firstChild = Component();
await parent.ensureAddAll([firstChild, Component()]);

expect(
() {
for (final component in game.world.descendants()) {
if (component == firstChild) {
parent.add(Component(priority: -1));
game.update(0);
}
}
},
throwsConcurrentModificationError,
);
},
);

testWithFlameGame(
'descendants() allows reordering the children of the component that '
'was just emitted',
(game) async {
final parent = Component();
await game.world.ensureAdd(parent);
await parent.ensureAddAll([Component(), Component()]);
final inserted = Component(priority: -1);

final visited = <Component>[];
for (final component in game.world.descendants()) {
visited.add(component);
if (component == parent) {
parent.add(inserted);
game.update(0);
}
}
expect(visited, [parent, inserted, ...parent.children.skip(1)]);
},
);

testWithFlameGame(
'descendants() does not touch the tree before iteration starts',
(game) async {
final parent = Component();
await game.world.ensureAdd(parent);
await parent.ensureAddAll([Component(), Component()]);

for (final reversed in [false, true]) {
final iterator = parent.descendants(reversed: reversed).iterator;
parent.add(Component(priority: -1));
game.update(0);
final visited = <Component>[];
while (iterator.moveNext()) {
visited.add(iterator.current);
}
expect(visited.length, parent.children.length);
}
},
);

testWithFlameGame(
'firstChild returns the first child on the matching type',
(game) async {
Expand Down
Loading