refactor!: The method onLoad() now returns FutureOr<void> (#2228)

Before this PR, the return type of onLoad() was Future<void>?, after this PR the return type is FutureOr<void> -- both for classes Component and Game.

Reasons:

The use of FutureOr is more idiomatic in Dart, this class was specifically created in order to be used in situations like ours.
This makes learning Flame easier for beginners, since you no longer need to explain what asynchronous programming is from the start (and onLoad() is one of the first methods the user encounters in Flame).
The code can be cleaner in case when onLoad doesn't need to be async.
With new approach, the onLoad() method can be overridden as either

@override
Future<void> onLoad() async { ... }
or

@override
void onLoad() { ... }
Of course, it can also be overridden as

@override
FutureOr<void> onLoad() { ... }
but this is rare, only for components that are designed to be further subclassed, or for mixins.

The documentation was updated to show the new recommended usage.
This commit is contained in:
Pasha Stetsenko
2022-12-23 12:30:40 -08:00
committed by GitHub
parent 19a1f09acc
commit d898b539f7
11 changed files with 122 additions and 112 deletions

View File

@ -88,7 +88,7 @@ Example:
```dart ```dart
class MyGame extends FlameGame { class MyGame extends FlameGame {
@override @override
Future<void> onLoad() { void onLoad() {
final myComponent = PositionComponent(priority: 5); final myComponent = PositionComponent(priority: 5);
add(myComponent); add(myComponent);
} }
@ -135,7 +135,7 @@ class GameOverPanel extends PositionComponent {
GameOverPanel(this.spriteImage); GameOverPanel(this.spriteImage);
@override @override
Future<void> onLoad() async { void onLoad() {
final gameOverText = GameOverText(spriteImage); // GameOverText is a Component final gameOverText = GameOverText(spriteImage); // GameOverText is a Component
final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent
@ -163,7 +163,7 @@ constructor. This approach more closely resembles the standard Flutter API:
```dart ```dart
class MyGame extends FlameGame { class MyGame extends FlameGame {
@override @override
Future<void> onLoad() async { void onLoad() {
add( add(
PositionComponent( PositionComponent(
position: Vector2(30, 0), position: Vector2(30, 0),
@ -198,7 +198,7 @@ Example:
```dart ```dart
class MyComponent extends Component with ParentIsA<MyParentComponent> { class MyComponent extends Component with ParentIsA<MyParentComponent> {
@override @override
Future<void> onLoad() async { void onLoad() {
// parent is of type MyParentComponent // parent is of type MyParentComponent
print(parent.myValue); print(parent.myValue);
} }
@ -221,7 +221,7 @@ Example:
```dart ```dart
class MyComponent extends Component with HasAncestor<MyAncestorComponent> { class MyComponent extends Component with HasAncestor<MyAncestorComponent> {
@override @override
Future<void> onLoad() async { void onLoad() {
// ancestor is of type MyAncestorComponent. // ancestor is of type MyAncestorComponent.
print(ancestor.myValue); print(ancestor.myValue);
} }
@ -247,7 +247,7 @@ Example:
```dart ```dart
@override @override
Future<void> onLoad() async { void onLoad() {
children.register<PositionComponent>(); children.register<PositionComponent>();
} }
``` ```
@ -442,10 +442,10 @@ class MyGame extends FlameGame {
final player = SpriteComponent(size: size, sprite: sprite); final player = SpriteComponent(size: size, sprite: sprite);
// Vector2(0.0, 0.0) by default, can also be set in the constructor // Vector2(0.0, 0.0) by default, can also be set in the constructor
player.position = ... player.position = Vector2(10, 20);
// 0 by default, can also be set in the constructor // 0 by default, can also be set in the constructor
player.angle = ... player.angle = 0;
// Adds the component // Adds the component
add(player); add(player);
@ -461,32 +461,36 @@ This class is used to represent a Component that has sprites that run in a singl
This will create a simple three frame animation using 3 different images: This will create a simple three frame animation using 3 different images:
```dart ```dart
final sprites = [0, 1, 2] Future<void> onLoad() async {
.map((i) => Sprite.load('player_$i.png')); final sprites = [0, 1, 2]
final animation = SpriteAnimation.spriteList( .map((i) => Sprite.load('player_$i.png'));
await Future.wait(sprites), final animation = SpriteAnimation.spriteList(
stepTime: 0.01, await Future.wait(sprites),
); stepTime: 0.01,
this.player = SpriteAnimationComponent( );
animation: animation, this.player = SpriteAnimationComponent(
size: Vector2.all(64.0), animation: animation,
); size: Vector2.all(64.0),
);
}
``` ```
If you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData` If you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData`
class (check more details on [Images &gt; Animation](rendering/images.md#animation)): class (check more details on [Images &gt; Animation](rendering/images.md#animation)):
```dart ```dart
final size = Vector2.all(64.0); Future<void> onLoad() async {
final data = SpriteAnimationData.sequenced( final size = Vector2.all(64.0);
textureSize: size, final data = SpriteAnimationData.sequenced(
amount: 2, textureSize: size,
stepTime: 0.1, amount: 2,
); stepTime: 0.1,
this.player = SpriteAnimationComponent.fromFrameData( );
await images.load('player.png'), this.player = SpriteAnimationComponent.fromFrameData(
data, await images.load('player.png'),
); data,
);
}
``` ```
If you are not using `FlameGame`, don't forget this component needs to be updated, because the If you are not using `FlameGame`, don't forget this component needs to be updated, because the
@ -580,7 +584,7 @@ class ButtonComponent extends SpriteGroupComponent<ButtonState>
@override @override
Future<void>? onLoad() async { Future<void>? onLoad() async {
final pressedSprite = await gameRef.loadSprite(/* omitted */); final pressedSprite = await gameRef.loadSprite(/* omitted */);
final unpressedSprite = await gameRef.loadSprite(/* omitted /*); final unpressedSprite = await gameRef.loadSprite(/* omitted */);
sprites = { sprites = {
ButtonState.pressed: pressedSprite, ButtonState.pressed: pressedSprite,
@ -604,12 +608,14 @@ This component uses an instance of `Svg` class to represent a Component that has
rendered in the game: rendered in the game:
```dart ```dart
final svg = await Svg.load('android.svg'); Future<void> onLoad() async {
final android = SvgComponent.fromSvg( final svg = await Svg.load('android.svg');
svg, final android = SvgComponent.fromSvg(
position: Vector2.all(100), svg,
size: Vector2.all(100), position: Vector2.all(100),
); size: Vector2.all(100),
);
}
``` ```
@ -711,7 +717,7 @@ class MyParallaxComponent extends ParallaxComponent<MyGame> {
class MyGame extends FlameGame { class MyGame extends FlameGame {
@override @override
Future<void> onLoad() async { void onLoad() {
add(MyParallaxComponent()); add(MyParallaxComponent());
} }
} }
@ -726,20 +732,24 @@ They simplest way is to set the named optional parameters `baseVelocity` and
background images along the X-axis with a faster speed the "closer" the image is: background images along the X-axis with a faster speed the "closer" the image is:
```dart ```dart
final parallaxComponent = await loadParallaxComponent( Future<void> onLoad() async {
_dataList, final parallaxComponent = await loadParallaxComponent(
baseVelocity: Vector2(20, 0), _dataList,
velocityMultiplierDelta: Vector2(1.8, 1.0), baseVelocity: Vector2(20, 0),
); velocityMultiplierDelta: Vector2(1.8, 1.0),
);
}
``` ```
You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your
game speeds up. game speeds up.
```dart ```dart
final parallax = parallaxComponent.parallax; Future<void> onLoad() async {
parallax.baseSpeed = Vector2(100, 0); final parallax = parallaxComponent.parallax;
parallax.velocityMultiplierDelta = Vector2(2.0, 1.0); parallax.baseSpeed = Vector2(100, 0);
parallax.velocityMultiplierDelta = Vector2(2.0, 1.0);
}
``` ```
By default, the images are aligned to the bottom left, repeated along the X-axis and scaled By default, the images are aligned to the bottom left, repeated along the X-axis and scaled
@ -997,20 +1007,23 @@ then add animation. Removing the stack will not remove the tiles from the map.
> **Note**: This currently only supports position based effects. > **Note**: This currently only supports position based effects.
```dart ```dart
final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'}); void onLoad() {
stack.add( final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'});
SequenceEffect( stack.add(
[ SequenceEffect(
MoveEffect.by( [
Vector2(5, 0), MoveEffect.by(
NoiseEffectController(duration: 1, frequency: 20), Vector2(5, 0),
), NoiseEffectController(duration: 1, frequency: 20),
MoveEffect.by(Vector2.zero(), LinearEffectController(2)), ),
], MoveEffect.by(Vector2.zero(), LinearEffectController(2)),
repeatCount: 3, ],
)..onComplete = () => stack.removeFromParent(), repeatCount: 3,
); )
map.add(stack); ..onComplete = () => stack.removeFromParent(),
);
map.add(stack);
}
``` ```
@ -1111,19 +1124,20 @@ be used:
class MyGame extends FlameGame { class MyGame extends FlameGame {
int lives = 2; int lives = 2;
Future<void> onLoad() { @override
final playerNotifier = componentsNotifier<Player>() void onLoad() {
..addListener(() { final playerNotifier = componentsNotifier<Player>()
final player = playerNotifier.single; ..addListener(() {
if (player == null) { final player = playerNotifier.single;
lives--; if (player == null) {
if (lives == 0) { lives--;
add(GameOverComponent()); if (lives == 0) {
} else { add(GameOverComponent());
add(Player()); } else {
} add(Player());
} }
}); }
});
} }
} }
``` ```
@ -1152,16 +1166,17 @@ Then our hud component could look like:
```dart ```dart
class Hud extends PositionComponent with HasGameRef { class Hud extends PositionComponent with HasGameRef {
Future<void> onLoad() { @override
final playerNotifier = gameRef.componentsNotifier<Player>() void onLoad() {
..addListener(() { final playerNotifier = gameRef.componentsNotifier<Player>()
final player = playerNotifier.single; ..addListener(() {
if (player != null) { final player = playerNotifier.single;
if (player.health <= .5) { if (player != null) {
add(BlinkEffect()); if (player.health <= .5) {
} add(BlinkEffect());
} }
}); }
});
} }
} }
``` ```

View File

@ -79,10 +79,9 @@ class CalculatePrimeNumber extends PositionComponent
DiscardNewBackPressureStrategy(); DiscardNewBackPressureStrategy();
@override @override
Future<void>? onLoad() { void onLoad() {
width = 200; width = 200;
height = 70; height = 70;
return super.onLoad();
} }
@override @override

View File

@ -97,7 +97,7 @@ class Bezel extends PositionComponent {
late final Paint specularPaint; late final Paint specularPaint;
@override @override
Future<void> onLoad() async { void onLoad() {
rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius)); rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius));
final outer = radius + rimWidth / 2; final outer = radius + rimWidth / 2;
final inner = radius - rimWidth / 2; final inner = radius - rimWidth / 2;

View File

@ -12,12 +12,11 @@ class BouncingBallExample extends FlameGame with HasCollisionDetection {
collides with the screen boundaries and then update it to bounce off these boundaries. collides with the screen boundaries and then update it to bounce off these boundaries.
'''; ''';
@override @override
Future<void>? onLoad() { void onLoad() {
addAll([ addAll([
ScreenHitbox(), ScreenHitbox(),
Ball(), Ball(),
]); ]);
return super.onLoad();
} }
} }

View File

@ -30,7 +30,7 @@ This examples showcases how raycast APIs can be used to detect hits within certa
)..positionType = PositionType.viewport; )..positionType = PositionType.viewport;
@override @override
Future<void>? onLoad() { void onLoad() {
camera.viewport = FixedResolutionViewport(Vector2(320, 180)); camera.viewport = FixedResolutionViewport(Vector2(320, 180));
_addMovingWall(); _addMovingWall();
@ -49,8 +49,6 @@ This examples showcases how raycast APIs can be used to detect hits within certa
origin: _character.absolutePosition, origin: _character.absolutePosition,
direction: Vector2(1, 0), direction: Vector2(1, 0),
); );
return super.onLoad();
} }
void _addMovingWall() { void _addMovingWall() {

View File

@ -409,18 +409,17 @@ class Component {
/// ``` /// ```
/// ///
/// Alternatively, if your [onLoad] function doesn't use any `await`ing, then /// Alternatively, if your [onLoad] function doesn't use any `await`ing, then
/// you can declare it as a regular method and then return `null`: /// you can declare it as a regular method returning `void`:
/// ```dart /// ```dart
/// @override /// @override
/// Future<void>? onLoad() { /// void onLoad() {
/// // your code here /// // your code here
/// return null;
/// } /// }
/// ``` /// ```
/// ///
/// The engine ensures that this method will be called exactly once during /// The engine ensures that this method will be called exactly once during
/// the lifetime of the [Component] object. Do not call this method manually. /// the lifetime of the [Component] object. Do not call this method manually.
Future<void>? onLoad() => null; FutureOr<void> onLoad() => null;
/// Called when the component is added to its parent. /// Called when the component is added to its parent.
/// ///
@ -530,14 +529,14 @@ class Component {
/// try to add it to multiple parents, or even to the same parent multiple /// try to add it to multiple parents, or even to the same parent multiple
/// times. If you need to change the parent of a component, use the /// times. If you need to change the parent of a component, use the
/// [changeParent] method. /// [changeParent] method.
Future<void>? add(Component component) => component.addToParent(this); FutureOr<void> add(Component component) => component.addToParent(this);
/// A convenience method to [add] multiple children at once. /// A convenience method to [add] multiple children at once.
Future<void> addAll(Iterable<Component> components) { Future<void> addAll(Iterable<Component> components) {
final futures = <Future<void>>[]; final futures = <Future<void>>[];
for (final component in components) { for (final component in components) {
final future = add(component); final future = add(component);
if (future != null) { if (future is Future) {
futures.add(future); futures.add(future);
} }
} }
@ -545,7 +544,7 @@ class Component {
} }
/// Adds this component as a child of [parent] (see [add] for details). /// Adds this component as a child of [parent] (see [add] for details).
Future<void>? addToParent(Component parent) { FutureOr<void> addToParent(Component parent) {
assert( assert(
_parent == null, _parent == null,
'$this cannot be added to $parent because it already has a parent: ' '$this cannot be added to $parent because it already has a parent: '
@ -556,7 +555,6 @@ class Component {
if (!isLoaded && (parent.findGame()?.hasLayout ?? false)) { if (!isLoaded && (parent.findGame()?.hasLayout ?? false)) {
return _startLoading(); return _startLoading();
} }
return null;
} }
/// Removes a component from the component tree. /// Removes a component from the component tree.
@ -760,7 +758,7 @@ class Component {
}); });
} }
Future<void>? _startLoading() { FutureOr<void> _startLoading() {
assert(_state == _initial); assert(_state == _initial);
assert(_parent != null); assert(_parent != null);
assert(_parent!.findGame() != null); assert(_parent!.findGame() != null);
@ -768,11 +766,10 @@ class Component {
_setLoadingBit(); _setLoadingBit();
onGameResize(_parent!.findGame()!.canvasSize); onGameResize(_parent!.findGame()!.canvasSize);
final onLoadFuture = onLoad(); final onLoadFuture = onLoad();
if (onLoadFuture == null) { if (onLoadFuture is Future) {
_finishLoading(); return onLoadFuture.then((dynamic _) => _finishLoading());
return null;
} else { } else {
return onLoadFuture.then((_) => _finishLoading()); _finishLoading();
} }
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flame/cache.dart'; import 'package:flame/cache.dart';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
@ -51,12 +53,12 @@ abstract class Game {
Vector2? _size; Vector2? _size;
/// This variable ensures that Game's [onLoad] is called no more than once. /// This variable ensures that Game's [onLoad] is called no more than once.
late final Future<void>? _onLoadFuture = onLoad(); late final FutureOr<void> _onLoadFuture = onLoad();
bool _debugOnLoadStarted = false; bool _debugOnLoadStarted = false;
@internal @internal
Future<void>? get onLoadFuture { FutureOr<void> get onLoadFuture {
assert( assert(
() { () {
_debugOnLoadStarted = true; _debugOnLoadStarted = true;
@ -69,7 +71,7 @@ abstract class Game {
/// To be used for tests that needs to evaluate the game after it has been /// To be used for tests that needs to evaluate the game after it has been
/// loaded by the game widget. /// loaded by the game widget.
@visibleForTesting @visibleForTesting
Future<void>? toBeLoaded() { FutureOr<void> toBeLoaded() {
assert( assert(
_debugOnLoadStarted, _debugOnLoadStarted,
'Make sure the game has passed to a mounted ' 'Make sure the game has passed to a mounted '
@ -159,7 +161,7 @@ abstract class Game {
/// ///
/// The engine ensures that this method will be called exactly once during /// The engine ensures that this method will be called exactly once during
/// the lifetime of the [Game] instance. Do not call this method manually. /// the lifetime of the [Game] instance. Do not call this method manually.
Future<void>? onLoad() => null; FutureOr<void> onLoad() => null;
void onMount() {} void onMount() {}

View File

@ -16,13 +16,12 @@ class WorkerOvermindHud extends PositionComponent with Tappable {
ComputeType computeType = ComputeType.isolate; ComputeType computeType = ComputeType.isolate;
@override @override
Future<void>? onLoad() { void onLoad() {
x = 10; x = 10;
y = 10; y = 10;
width = 210; width = 210;
height = 80; height = 80;
positionType = PositionType.viewport; positionType = PositionType.viewport;
return super.onLoad();
} }
@override @override

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -40,7 +42,7 @@ mixin Movable on PositionComponent, HasGameRef<ColonistsGame> {
@override @override
@mustCallSuper @mustCallSuper
Future<void>? onLoad() { FutureOr<void> onLoad() {
anchor = Anchor.center; anchor = Anchor.center;
return super.onLoad(); return super.onLoad();
} }

View File

@ -5,7 +5,7 @@ publish_to: 'none'
version: 0.0.1+1 version: 0.0.1+1
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.17.0 <3.0.0'
dependencies: dependencies:
flame: ^1.5.0 flame: ^1.5.0

View File

@ -50,7 +50,7 @@ class SkillsAnimationComponent extends RiveComponent with Tappable {
SMIInput<double>? _levelInput; SMIInput<double>? _levelInput;
@override @override
Future<void>? onLoad() { void onLoad() {
final controller = StateMachineController.fromArtboard( final controller = StateMachineController.fromArtboard(
artboard, artboard,
"Designer's Test", "Designer's Test",
@ -60,7 +60,6 @@ class SkillsAnimationComponent extends RiveComponent with Tappable {
_levelInput = controller.findInput<double>('Level'); _levelInput = controller.findInput<double>('Level');
_levelInput?.value = 0; _levelInput?.value = 0;
} }
return super.onLoad();
} }
@override @override