Skip to content
1 change: 1 addition & 0 deletions .github/.cspell/dart_dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ dartdoc # documentation tool for dart
dartdocs # plural of dartdoc
endtemplate # Use @endtemplate to close a @template block in dartdoc
pubspec # dependency and configuration file of every Dart project
unawaited # dart:async helper to mark a Future as intentionally not awaited
2 changes: 1 addition & 1 deletion doc/bridge_packages/flame_behaviors/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ For instance a `TimerComponent` can implement a time-based behavioral activity:
class MyBehavior extends Behavior {
@override
Future<void> onLoad() async {
await add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
}

void _onTick() {
Expand Down
6 changes: 3 additions & 3 deletions doc/bridge_packages/flame_bloc/bloc.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ We can do that by using `FlameBlocProvider` component:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(
add(
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
create: () => PlayerInventoryBloc(),
children: [
Expand All @@ -44,7 +44,7 @@ fashion:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(
add(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
Expand Down Expand Up @@ -72,7 +72,7 @@ By using `FlameBlocListener` component:
class Player extends PositionComponent {
@override
Future<void> onLoad() async {
await add(
add(
FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(
listener: (state) {
updateGear(state);
Expand Down
2 changes: 1 addition & 1 deletion doc/bridge_packages/flame_spine/flame_spine.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class FlameSpineExample extends FlameGame {

// Set the "walk" animation on track 0 in looping mode
spineboy.animationState.setAnimationByName(0, 'walk', true);
await add(spineboy);
add(spineboy);
}

@override
Expand Down
51 changes: 51 additions & 0 deletions doc/flame/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,57 @@ class MyGame extends FlameGame {
The two approaches can be combined freely: the children specified within the constructor will be
added first, and then any additional child components after.

The `add()`, `addAll()`, and `addToParent()` methods are synchronous: they return immediately
without waiting for the child to load or mount. This makes them safe to call from anywhere,
including inside `update()` or a loop that spawns many components, without having to `await` them
or wrap them in `unawaited`. If you need to wait until a child has reached a given lifecycle stage,
await its `loaded`, `mounted`, or `removed` future instead (see the lifecycle getters under
[Component lifecycle](#component-lifecycle)):

```dart
world.add(coin);
await coin.mounted;
Comment thread
luanpotter marked this conversation as resolved.
// The coin is now guaranteed to be mounted.
```

When you add a batch of children and only care that all of them made it into the tree, await
`game.lifecycleEventsProcessed` once instead of collecting the individual futures:

```dart
world.addAll(coins);
await game.lifecycleEventsProcessed;
// All the coins are now in world.children.
```

The same three getters are also available on any `Iterable<Component>`, for when you need a
specific stage for a specific group of children rather than for the whole tree:

```dart
world.addAll(coins);
await coins.loaded;
// Every coin has finished loading.
```

Awaiting `loaded` is safe from inside the parent's own `onLoad`, because the child starts loading as
soon as it is added:

```dart
class Inventory extends Component {
@override
Future<void> onLoad() async {
final coin = Coin();
add(coin);
await coin.loaded;
// Anything the coin's onLoad set up is now available here.
}
}
```

Awaiting `mounted` or `removed` there is not safe: a child can only be mounted after its parent has
been, and the parent is only mounted once its `onLoad` has completed, so those futures would
deadlock. The same goes for `game.lifecycleEventsProcessed`, since the parent's own pending mount is
part of the queue it waits for.

Note that the children added via either method are only guaranteed to be available eventually:
after they are loaded and mounted. We can only assure that they will appear in the children list
in the same order as they were scheduled for addition.
Expand Down
4 changes: 2 additions & 2 deletions doc/flame/examples/lib/anchor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class AnchorGame extends FlameGame {
paint: BasicPalette.blue.paint(),
);

await _redComponent.addAll([
_redComponent.addAll([
_blueComponent,
CircleComponent(radius: 2, anchor: Anchor.center),
]);

await addAll([
addAll([
_redComponent,
_parentAnchorText,
_childAnchorText,
Expand Down
2 changes: 1 addition & 1 deletion doc/flame/examples/lib/time_scale.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class TimeScaleGame extends FlameGame with HasTimeScale {

@override
Future<void> onLoad() async {
await add(
add(
EmberPlayer(
position: size / 2,
size: size / 4,
Expand Down
7 changes: 4 additions & 3 deletions doc/flame/game.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class MyCrate extends SpriteComponent {
class MyWorld extends World {
@override
Future<void> onLoad() async {
await add(MyCrate());
await super.onLoad();
add(MyCrate());
}
}

Expand Down Expand Up @@ -236,8 +237,8 @@ application. This is a common scenario when building games: there is a single fu

Adding this mixin provides performance advantages in certain scenarios. In particular, a component's
`onLoad` method is guaranteed to start when that component is added to its parent, even if the
parent is not yet mounted itself. Consequently, `await`-ing on `parent.add(component)` is guaranteed
to always finish loading the component.
parent is not yet mounted itself. Consequently, awaiting `component.loaded` after
`parent.add(component)` is guaranteed to finish loading the component.

Using this mixin is simple:

Expand Down
78 changes: 76 additions & 2 deletions doc/flame/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ would apply their action even though the drag never finished. This is not a rare
with `MultiDragScaleDispatcher` every two finger pinch cancels the individual pointer drags.

The default implementation now only resets `isDragged`, which means that `onDragEnd` is no longer
called when a drag is cancelled. If you were relying on the old behavior, override `onDragCancel` and
forward the event yourself with `DragCancelEvent.toDragEnd`:
called when a drag is cancelled. If you were relying on the old behavior, override `onDragCancel`
and forward the event yourself with `DragCancelEvent.toDragEnd`:

```dart
// Before
Expand Down Expand Up @@ -223,6 +223,80 @@ The equivalent field on the deprecated `*Info` event classes (`TapDownInfo.handl
been removed as well.


### `add`, `addAll` and `addToParent` are now synchronous

`Component.add`, `Component.addAll` and `Component.addToParent` used to return a future, which made
it look like you could await the addition. That future only covered the child's loading, never its
mounting, so awaiting it was misleading, and forgetting to await it (or to wrap it in `unawaited`)
tripped the `discarded_futures` lint in a lot of games. All three methods now return `void`.

Drop the `await`:

```dart
// Before
await add(MyComponent());
await addAll([MyComponent(), MyOtherComponent()]);

// After
add(MyComponent());
addAll([MyComponent(), MyOtherComponent()]);
```

If you were relying on the returned future to know when the child had loaded, await the child's
`loaded` future instead:

```dart
// Before
await add(crate);

// After
add(crate);
await crate.loaded;
```

For a batch of children, `loaded`, `mounted` and `removed` are also available on any
`Iterable<Component>`:

```dart
// Before
await addAll(crates);

// After
addAll(crates);
await crates.loaded;
```

Or, when you need them to be present in `children` rather than just loaded, await
`game.lifecycleEventsProcessed` once after adding them.


#### Load errors are no longer reported by `GameWidget.errorBuilder`

`GameWidget.errorBuilder` shows a widget when the *game's* loading fails, and it used to catch a
failing child's `onLoad` as well, because `await add(child)` chained the child's error onto the
game's own `onLoad` future. Since `add` no longer returns a future, that chain is gone: a child that
throws in `onLoad` no longer reaches `errorBuilder`.

The component itself is not added to the tree, and the rest of the game keeps running. The error is
reported through the child's `loaded` future, and if nothing is awaiting it, it is handed to the
current `Zone` as an uncaught error.

To get the old behavior for a specific child, await its `loaded` future inside the parent's
`onLoad`, which puts the error back onto the future `errorBuilder` watches:

```dart
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final level = Level();
world.add(level);
// Throws here if Level.onLoad fails, so errorBuilder is shown.
await level.loaded;
}
}
```


### `GameWidget.controlled` renamed to `GameWidget.managed`

The `GameWidget.controlled` constructor has been renamed to `GameWidget.managed`. The behavior is
Expand Down
2 changes: 1 addition & 1 deletion doc/tutorials/platformer/app/lib/overlays/hud.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Hud extends PositionComponent with HasGameReference<EmberQuestGame> {

for (var i = 1; i <= game.health; i++) {
final positionX = 40 * i;
await add(
add(
HeartHealthComponent(
heartNumber: i,
position: Vector2(positionX.toDouble(), 20),
Expand Down
2 changes: 1 addition & 1 deletion doc/tutorials/platformer/step_6.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class Hud extends PositionComponent with HasGameReference<EmberQuestGame> {

for (var i = 1; i <= game.health; i++) {
final positionX = 40 * i;
await add(
add(
HeartHealthComponent(
heartNumber: i,
position: Vector2(positionX.toDouble(), 20),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CameraTarget extends PositionComponent

@override
Future<void> onLoad() async {
await add(moveEffect);
add(moveEffect);
}

void go({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class InputHandler extends PositionComponent

@override
Future<void> onLoad() async {
await add(
add(
KeyboardListenerComponent(
keyDown: {
LogicalKeyboardKey.arrowLeft: (_) => onLeftStart(),
Expand Down
2 changes: 1 addition & 1 deletion examples/lib/stories/animations/benchmark_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ starts to drop in FPS, this is without any sprite batching and such.

@override
Future<void> onLoad() async {
await camera.viewport.addAll([
camera.viewport.addAll([
FpsTextComponent(
position: size - Vector2(10, 50),
anchor: Anchor.bottomRight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ class BlobWorld extends Forge2DWorld
..dampingRatio = 1.0
..collideConnected = false;

await addAll([
final blobParts = [
for (var i = 0; i < 20; i++)
BlobPart(i, jointDef, blobRadius, blobCenter),
]);
];
addAll(blobParts);
// The joint needs the body of every part, and those are created in the
// parts' onLoad, so wait for all of them to finish loading first.
await blobParts.loaded;
createJoint(ConstantVolumeJoint(physicsWorld, jointDef));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class RopeJointWorld extends Forge2DWorld
width: handleWidth,
height: 3,
);
await add(box);
add(box);
await box.loaded;

createPrismaticJoint(box.body, anchor);
return box.body;
Expand All @@ -49,7 +50,8 @@ class RopeJointWorld extends Forge2DWorld
for (var i = 0; i < length; i++) {
final newPosition = prevBody.worldCenter + Vector2(0, 1);
final ball = Ball(newPosition, radius: 0.5, color: Colors.white);
await add(ball);
add(ball);
await ball.loaded;

createRopeJoint(ball.body, prevBody);
prevBody = ball.body;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class WeldJointWorld extends Forge2DWorld
color: Colors.white,
);

await addAll([leftPillar, rightPillar]);
final pillars = [leftPillar, rightPillar];
addAll(pillars);
await pillars.loaded;

createBridge(leftPillar, rightPillar);
}
Expand Down Expand Up @@ -71,7 +73,8 @@ class WeldJointWorld extends Forge2DWorld
width: sectionWidth,
height: 1,
);
await add(section);
add(section);
await section.loaded;

if (prevSection != null) {
createWeldJoint(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DialogueBoxComponent extends SpriteComponent with HasGameReference {
'dialogue_box.png',
srcSize: spriteSize,
);
await addAll([buttonRow, textBox]);
addAll([buttonRow, textBox]);
return super.onLoad();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class FlameSpineExample extends FlameGame with TapCallbacks {

// Set the "walk" animation on track 0 in looping mode
spineboy.animationState.setAnimation(0, 'walk', true);
await add(spineboy);
add(spineboy);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class SharedDataSpineExample extends FlameGame with TapCallbacks {
spineboy.animationState.setAnimation(0, 'walk', true);
spineboys.add(spineboy);
}
await addAll(spineboys);
addAll(spineboys);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ class AntWorld extends World {
Future<void> onLoad() async {
final random = Random();
curve = DragonCurve();
await add(curve);
add(curve);
await curve.loaded;
bgRect = curve.boundingRect().inflate(100);

const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class CameraComponentPropertiesExample extends FlameGame {
..strokeWidth = 0.25
..color = const Color(0xaaffff00),
);
await world.add(_cullRect);
world.add(_cullRect);
camera.mounted.then((_) {
updateSize(canvasSize);
});
Expand Down
Loading
Loading