Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ComponentTreeRoot.lifecycleEventsProcessed future #3308

Merged
merged 5 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 21 additions & 1 deletion packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,20 @@ class Component {
/// This can be null if the component hasn't been added to the component tree
/// yet, or if it is the root of component tree.
///
/// Setting this property to null is equivalent to [removeFromParent].
/// Setting this property to `null` is equivalent to [removeFromParent].
/// Setting it to a new parent component is equivalent to calling
/// [addToParent] and will properly remove this component from its current
/// parent, if any.
///
/// Note that the [parent] setter, like [add] and similar methods,
/// merely enqueues the move from one parent to another. For example:
///
/// ```dart
/// coin.parent = inventory;
/// // The inventory.children set does not include coin yet.
/// await game.lifecycleEventsProcessed;
spydon marked this conversation as resolved.
Show resolved Hide resolved
/// // The inventory.children set now includes coin.
/// ```
Component? get parent => _parent;
Component? _parent;
set parent(Component? newParent) {
Expand Down Expand Up @@ -543,6 +556,13 @@ class Component {
/// The cost of this flexibility is that the component won't be added right
/// away. Instead, it will be placed into a queue, and then added later, after
/// it has finished loading, but no sooner than on the next game tick.
/// You can await [FlameGame.lifecycleEventsProcessed] like so:
///
/// ```dart
/// world.add(coin);
/// await game.lifecycleEventsProcessed;
/// // The coin is now guaranteed to be added.
/// ```
///
/// When multiple children are scheduled to be added to the same parent, we
/// start loading all of them as soon as possible. Nevertheless, the children
Expand Down
35 changes: 35 additions & 0 deletions packages/flame/lib/src/components/core/component_tree_root.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame/src/components/core/recycled_queue.dart';
import 'package:meta/meta.dart';
Expand All @@ -20,6 +22,7 @@ class ComponentTreeRoot extends Component {
final Set<int> _blocked;
final Set<Component> _componentsToRebalance;
late final Map<ComponentKey, Component> _index = {};
Completer<void>? _lifecycleEventsCompleter;

@internal
void enqueueAdd(Component child, Component parent) {
Expand Down Expand Up @@ -76,6 +79,33 @@ class ComponentTreeRoot extends Component {

bool get hasLifecycleEvents => _queue.isNotEmpty;

/// A future that will complete once all lifecycle events have been
/// processed.
///
/// If there are no lifecycle events to be processed ([hasLifecycleEvents]
/// is `false`), then this future returns immediately.
///
/// This is useful when you modify the component tree
/// (by adding, moving or removing a component) and you want to make sure
/// you react to the changed state, not the current one.
/// Remember, methods like [Component.add] don't act immediately and instead
/// enqueue their action. This action also cannot be awaited
/// with something like `await world.add(something)` since that future
/// completes _before_ the lifecycle events are processed.
///
/// Example usage:
///
/// ```dart
/// player.inventory.addAll(enemy.inventory.children);
/// await game.lifecycleEventsProcessed;
/// updateUi(player.inventory);
/// ```
Future<void> get lifecycleEventsProcessed {
return !hasLifecycleEvents
? Future.value()
: (_lifecycleEventsCompleter ??= Completer<void>()).future;
}

void processLifecycleEvents() {
assert(_blocked.isEmpty);
var repeatLoop = true;
Expand Down Expand Up @@ -109,6 +139,11 @@ class ComponentTreeRoot extends Component {
}
_blocked.clear();
}

if (!hasLifecycleEvents && _lifecycleEventsCompleter != null) {
spydon marked this conversation as resolved.
Show resolved Hide resolved
_lifecycleEventsCompleter!.complete();
_lifecycleEventsCompleter = null;
}
}

void processRebalanceEvents() {
Expand Down
32 changes: 32 additions & 0 deletions packages/flame/test/components/component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,38 @@ void main() {
expect(child.isMounted, true);
},
);

group('lifecycleEventsProcessed', () {
testWithFlameGame('waits for unprocessed events', (game) async {
await game.ready();
final component = _LifecycleComponent();
await game.world.add(component);
expect(game.hasLifecycleEvents, isTrue);

Future.delayed(Duration.zero).then((_) => game.update(0));
await game.lifecycleEventsProcessed;
expect(game.hasLifecycleEvents, isFalse);
});

testWithFlameGame("doesn't block when there are no events",
(game) async {
await game.ready();
expect(game.hasLifecycleEvents, isFalse);
await game.lifecycleEventsProcessed;
expect(game.hasLifecycleEvents, isFalse);
});
});

testWithFlameGame('Can wait for lifecycleEventsProcessed', (game) async {
await game.ready();
final component = _LifecycleComponent();
await game.world.add(component);
expect(game.hasLifecycleEvents, isTrue);

Future.delayed(Duration.zero).then((_) => game.update(0));
await game.lifecycleEventsProcessed;
expect(game.hasLifecycleEvents, isFalse);
});
});

group('onGameResize', () {
Expand Down