From d898b539f734d3e14c47990ef0727043a0e32efb Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 23 Dec 2022 12:30:40 -0800 Subject: [PATCH] refactor!: The method `onLoad()` now returns `FutureOr` (#2228) Before this PR, the return type of onLoad() was Future?, after this PR the return type is FutureOr -- 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 onLoad() async { ... } or @override void onLoad() { ... } Of course, it can also be overridden as @override FutureOr 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. --- doc/flame/components.md | 177 ++++++++++-------- .../flame_isolate/simple_isolate_example.dart | 3 +- .../camera_component_example.dart | 2 +- .../bouncing_ball_example.dart | 3 +- .../raycast_max_distance_example.dart | 4 +- .../lib/src/components/core/component.dart | 23 +-- packages/flame/lib/src/game/game.dart | 10 +- .../lib/brains/worker_overmind_hud.dart | 3 +- .../example/lib/units/actions/movable.dart | 4 +- packages/flame_lottie/example/pubspec.yaml | 2 +- packages/flame_rive/example/lib/main.dart | 3 +- 11 files changed, 122 insertions(+), 112 deletions(-) diff --git a/doc/flame/components.md b/doc/flame/components.md index 5da3d89ca..f93aa3d8d 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -88,7 +88,7 @@ Example: ```dart class MyGame extends FlameGame { @override - Future onLoad() { + void onLoad() { final myComponent = PositionComponent(priority: 5); add(myComponent); } @@ -135,7 +135,7 @@ class GameOverPanel extends PositionComponent { GameOverPanel(this.spriteImage); @override - Future onLoad() async { + void onLoad() { final gameOverText = GameOverText(spriteImage); // GameOverText is a Component final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent @@ -163,7 +163,7 @@ constructor. This approach more closely resembles the standard Flutter API: ```dart class MyGame extends FlameGame { @override - Future onLoad() async { + void onLoad() { add( PositionComponent( position: Vector2(30, 0), @@ -198,7 +198,7 @@ Example: ```dart class MyComponent extends Component with ParentIsA { @override - Future onLoad() async { + void onLoad() { // parent is of type MyParentComponent print(parent.myValue); } @@ -221,7 +221,7 @@ Example: ```dart class MyComponent extends Component with HasAncestor { @override - Future onLoad() async { + void onLoad() { // ancestor is of type MyAncestorComponent. print(ancestor.myValue); } @@ -247,7 +247,7 @@ Example: ```dart @override -Future onLoad() async { +void onLoad() { children.register(); } ``` @@ -442,10 +442,10 @@ class MyGame extends FlameGame { final player = SpriteComponent(size: size, sprite: sprite); // 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 - player.angle = ... + player.angle = 0; // Adds the component 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: ```dart -final sprites = [0, 1, 2] - .map((i) => Sprite.load('player_$i.png')); -final animation = SpriteAnimation.spriteList( - await Future.wait(sprites), - stepTime: 0.01, -); -this.player = SpriteAnimationComponent( - animation: animation, - size: Vector2.all(64.0), -); +Future onLoad() async { + final sprites = [0, 1, 2] + .map((i) => Sprite.load('player_$i.png')); + final animation = SpriteAnimation.spriteList( + await Future.wait(sprites), + stepTime: 0.01, + ); + this.player = SpriteAnimationComponent( + animation: animation, + size: Vector2.all(64.0), + ); +} ``` If you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData` class (check more details on [Images > Animation](rendering/images.md#animation)): ```dart -final size = Vector2.all(64.0); -final data = SpriteAnimationData.sequenced( - textureSize: size, - amount: 2, - stepTime: 0.1, -); -this.player = SpriteAnimationComponent.fromFrameData( - await images.load('player.png'), - data, -); +Future onLoad() async { + final size = Vector2.all(64.0); + final data = SpriteAnimationData.sequenced( + textureSize: size, + amount: 2, + stepTime: 0.1, + ); + this.player = SpriteAnimationComponent.fromFrameData( + await images.load('player.png'), + data, + ); +} ``` 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 @override Future? onLoad() async { final pressedSprite = await gameRef.loadSprite(/* omitted */); - final unpressedSprite = await gameRef.loadSprite(/* omitted /*); + final unpressedSprite = await gameRef.loadSprite(/* omitted */); sprites = { 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: ```dart -final svg = await Svg.load('android.svg'); -final android = SvgComponent.fromSvg( - svg, - position: Vector2.all(100), - size: Vector2.all(100), -); +Future onLoad() async { + final svg = await Svg.load('android.svg'); + final android = SvgComponent.fromSvg( + svg, + position: Vector2.all(100), + size: Vector2.all(100), + ); +} ``` @@ -711,7 +717,7 @@ class MyParallaxComponent extends ParallaxComponent { class MyGame extends FlameGame { @override - Future onLoad() async { + void onLoad() { 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: ```dart -final parallaxComponent = await loadParallaxComponent( - _dataList, - baseVelocity: Vector2(20, 0), - velocityMultiplierDelta: Vector2(1.8, 1.0), -); +Future onLoad() async { + final parallaxComponent = await loadParallaxComponent( + _dataList, + 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 game speeds up. ```dart -final parallax = parallaxComponent.parallax; -parallax.baseSpeed = Vector2(100, 0); -parallax.velocityMultiplierDelta = Vector2(2.0, 1.0); +Future onLoad() async { + final parallax = parallaxComponent.parallax; + 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 @@ -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. ```dart - final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'}); - stack.add( - SequenceEffect( - [ - MoveEffect.by( - Vector2(5, 0), - NoiseEffectController(duration: 1, frequency: 20), - ), - MoveEffect.by(Vector2.zero(), LinearEffectController(2)), - ], - repeatCount: 3, - )..onComplete = () => stack.removeFromParent(), - ); - map.add(stack); +void onLoad() { + final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'}); + stack.add( + SequenceEffect( + [ + MoveEffect.by( + Vector2(5, 0), + NoiseEffectController(duration: 1, frequency: 20), + ), + MoveEffect.by(Vector2.zero(), LinearEffectController(2)), + ], + repeatCount: 3, + ) + ..onComplete = () => stack.removeFromParent(), + ); + map.add(stack); +} ``` @@ -1111,19 +1124,20 @@ be used: class MyGame extends FlameGame { int lives = 2; - Future onLoad() { - final playerNotifier = componentsNotifier() - ..addListener(() { - final player = playerNotifier.single; - if (player == null) { - lives--; - if (lives == 0) { - add(GameOverComponent()); - } else { - add(Player()); - } - } - }); + @override + void onLoad() { + final playerNotifier = componentsNotifier() + ..addListener(() { + final player = playerNotifier.single; + if (player == null) { + lives--; + if (lives == 0) { + add(GameOverComponent()); + } else { + add(Player()); + } + } + }); } } ``` @@ -1152,16 +1166,17 @@ Then our hud component could look like: ```dart class Hud extends PositionComponent with HasGameRef { - Future onLoad() { - final playerNotifier = gameRef.componentsNotifier() - ..addListener(() { - final player = playerNotifier.single; - if (player != null) { - if (player.health <= .5) { - add(BlinkEffect()); - } - } - }); + @override + void onLoad() { + final playerNotifier = gameRef.componentsNotifier() + ..addListener(() { + final player = playerNotifier.single; + if (player != null) { + if (player.health <= .5) { + add(BlinkEffect()); + } + } + }); } } ``` diff --git a/examples/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart b/examples/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart index c83ecccfe..9ade280f1 100644 --- a/examples/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart @@ -79,10 +79,9 @@ class CalculatePrimeNumber extends PositionComponent DiscardNewBackPressureStrategy(); @override - Future? onLoad() { + void onLoad() { width = 200; height = 70; - return super.onLoad(); } @override diff --git a/examples/lib/stories/camera_and_viewport/camera_component_example.dart b/examples/lib/stories/camera_and_viewport/camera_component_example.dart index 7f94c1cd1..0ed63b27c 100644 --- a/examples/lib/stories/camera_and_viewport/camera_component_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera_component_example.dart @@ -97,7 +97,7 @@ class Bezel extends PositionComponent { late final Paint specularPaint; @override - Future onLoad() async { + void onLoad() { rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius)); final outer = radius + rimWidth / 2; final inner = radius - rimWidth / 2; diff --git a/examples/lib/stories/collision_detection/bouncing_ball_example.dart b/examples/lib/stories/collision_detection/bouncing_ball_example.dart index 4e94845ac..bbabba8fb 100644 --- a/examples/lib/stories/collision_detection/bouncing_ball_example.dart +++ b/examples/lib/stories/collision_detection/bouncing_ball_example.dart @@ -12,12 +12,11 @@ class BouncingBallExample extends FlameGame with HasCollisionDetection { collides with the screen boundaries and then update it to bounce off these boundaries. '''; @override - Future? onLoad() { + void onLoad() { addAll([ ScreenHitbox(), Ball(), ]); - return super.onLoad(); } } diff --git a/examples/lib/stories/collision_detection/raycast_max_distance_example.dart b/examples/lib/stories/collision_detection/raycast_max_distance_example.dart index e5ab5e19f..483bd3253 100644 --- a/examples/lib/stories/collision_detection/raycast_max_distance_example.dart +++ b/examples/lib/stories/collision_detection/raycast_max_distance_example.dart @@ -30,7 +30,7 @@ This examples showcases how raycast APIs can be used to detect hits within certa )..positionType = PositionType.viewport; @override - Future? onLoad() { + void onLoad() { camera.viewport = FixedResolutionViewport(Vector2(320, 180)); _addMovingWall(); @@ -49,8 +49,6 @@ This examples showcases how raycast APIs can be used to detect hits within certa origin: _character.absolutePosition, direction: Vector2(1, 0), ); - - return super.onLoad(); } void _addMovingWall() { diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index df5d572d1..1a6c69f88 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -409,18 +409,17 @@ class Component { /// ``` /// /// 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 /// @override - /// Future? onLoad() { + /// void onLoad() { /// // your code here - /// return null; /// } /// ``` /// /// The engine ensures that this method will be called exactly once during /// the lifetime of the [Component] object. Do not call this method manually. - Future? onLoad() => null; + FutureOr onLoad() => null; /// 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 /// times. If you need to change the parent of a component, use the /// [changeParent] method. - Future? add(Component component) => component.addToParent(this); + FutureOr add(Component component) => component.addToParent(this); /// A convenience method to [add] multiple children at once. Future addAll(Iterable components) { final futures = >[]; for (final component in components) { final future = add(component); - if (future != null) { + if (future is Future) { futures.add(future); } } @@ -545,7 +544,7 @@ class Component { } /// Adds this component as a child of [parent] (see [add] for details). - Future? addToParent(Component parent) { + FutureOr addToParent(Component parent) { assert( _parent == null, '$this cannot be added to $parent because it already has a parent: ' @@ -556,7 +555,6 @@ class Component { if (!isLoaded && (parent.findGame()?.hasLayout ?? false)) { return _startLoading(); } - return null; } /// Removes a component from the component tree. @@ -760,7 +758,7 @@ class Component { }); } - Future? _startLoading() { + FutureOr _startLoading() { assert(_state == _initial); assert(_parent != null); assert(_parent!.findGame() != null); @@ -768,11 +766,10 @@ class Component { _setLoadingBit(); onGameResize(_parent!.findGame()!.canvasSize); final onLoadFuture = onLoad(); - if (onLoadFuture == null) { - _finishLoading(); - return null; + if (onLoadFuture is Future) { + return onLoadFuture.then((dynamic _) => _finishLoading()); } else { - return onLoadFuture.then((_) => _finishLoading()); + _finishLoading(); } } diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/game.dart index f4e9ac449..f40f8c8ef 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/game.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flame/cache.dart'; import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; @@ -51,12 +53,12 @@ abstract class Game { Vector2? _size; /// This variable ensures that Game's [onLoad] is called no more than once. - late final Future? _onLoadFuture = onLoad(); + late final FutureOr _onLoadFuture = onLoad(); bool _debugOnLoadStarted = false; @internal - Future? get onLoadFuture { + FutureOr get onLoadFuture { assert( () { _debugOnLoadStarted = true; @@ -69,7 +71,7 @@ abstract class Game { /// To be used for tests that needs to evaluate the game after it has been /// loaded by the game widget. @visibleForTesting - Future? toBeLoaded() { + FutureOr toBeLoaded() { assert( _debugOnLoadStarted, '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 lifetime of the [Game] instance. Do not call this method manually. - Future? onLoad() => null; + FutureOr onLoad() => null; void onMount() {} diff --git a/packages/flame_isolate/example/lib/brains/worker_overmind_hud.dart b/packages/flame_isolate/example/lib/brains/worker_overmind_hud.dart index ddaec02d4..58162f062 100644 --- a/packages/flame_isolate/example/lib/brains/worker_overmind_hud.dart +++ b/packages/flame_isolate/example/lib/brains/worker_overmind_hud.dart @@ -16,13 +16,12 @@ class WorkerOvermindHud extends PositionComponent with Tappable { ComputeType computeType = ComputeType.isolate; @override - Future? onLoad() { + void onLoad() { x = 10; y = 10; width = 210; height = 80; positionType = PositionType.viewport; - return super.onLoad(); } @override diff --git a/packages/flame_isolate/example/lib/units/actions/movable.dart b/packages/flame_isolate/example/lib/units/actions/movable.dart index 07618a90d..057d87e95 100755 --- a/packages/flame_isolate/example/lib/units/actions/movable.dart +++ b/packages/flame_isolate/example/lib/units/actions/movable.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flutter/material.dart'; @@ -40,7 +42,7 @@ mixin Movable on PositionComponent, HasGameRef { @override @mustCallSuper - Future? onLoad() { + FutureOr onLoad() { anchor = Anchor.center; return super.onLoad(); } diff --git a/packages/flame_lottie/example/pubspec.yaml b/packages/flame_lottie/example/pubspec.yaml index b56761547..48901d7a0 100644 --- a/packages/flame_lottie/example/pubspec.yaml +++ b/packages/flame_lottie/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' version: 0.0.1+1 environment: - sdk: '>=2.18.2 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: flame: ^1.5.0 diff --git a/packages/flame_rive/example/lib/main.dart b/packages/flame_rive/example/lib/main.dart index 223bb70ba..5fa1901cd 100644 --- a/packages/flame_rive/example/lib/main.dart +++ b/packages/flame_rive/example/lib/main.dart @@ -50,7 +50,7 @@ class SkillsAnimationComponent extends RiveComponent with Tappable { SMIInput? _levelInput; @override - Future? onLoad() { + void onLoad() { final controller = StateMachineController.fromArtboard( artboard, "Designer's Test", @@ -60,7 +60,6 @@ class SkillsAnimationComponent extends RiveComponent with Tappable { _levelInput = controller.findInput('Level'); _levelInput?.value = 0; } - return super.onLoad(); } @override