diff --git a/FAQ.md b/FAQ.md index d15308c41..ee1b6ea97 100644 --- a/FAQ.md +++ b/FAQ.md @@ -62,21 +62,21 @@ If you want a more full-fledged game, please check: - [Bob Box](https://github.com/bluefireteam/bounce_box): which is easy to grasp and a good display of our core features. -## What is the difference between Game and BaseGame? +## What is the difference between Game and FlameGame? -`Game` is the most barebone interface that Flame exposes. If you extend `Game` instead of `BaseGame` +`Game` is the most barebone interface that Flame exposes. If you extend `Game` instead of `FlameGame` , you will need to implement a lot of things yourself. Flame will hook you up on the game loop, so you will have to implement `render` and `update` yourself from scratch. If you want to use the component system, you can, but you don't need to. -`BaseGame` extends `Game` and adds lots of functionality on top of that. Just add your components to +`FlameGame` extends `Game` and adds lots of functionality on top of that. Just add your components to it and it works. They are rendered and updated automatically. You might still want to override some -of `BaseGame`'s methods to add custom functionality, but you will probably be calling the super -method to let `BaseGame` do its work. +of `FlameGame`'s methods to add custom functionality, but you will probably be calling the super +method to let `FlameGame` do its work. ## How does the Camera work? -If you are using `BaseGame`, you have a `camera` attribute that allows you to off-set all non-HUD +If you are using `FlameGame`, you have a `camera` attribute that allows you to off-set all non-HUD components by a certain amount. You can add custom logic to your `update` method to have the camera position be tracked based on the player position, for example, so the player can be always on the center or on the bottom. diff --git a/doc/camera_and_viewport.md b/doc/camera_and_viewport.md index 714a981c6..c5436a2dc 100644 --- a/doc/camera_and_viewport.md +++ b/doc/camera_and_viewport.md @@ -17,17 +17,17 @@ The Viewport is an attempt to unify multiple screen (or, rather, game widget) si configuration for your game by translating and resizing the canvas. The `Viewport` interface has multiple implementations and can be used from scratch on your `Game` -or, if you are using `BaseGame` instead, it's already built-in (with a default no-op viewport). +or, if you are using `FlameGame` instead, it's already built-in (with a default no-op viewport). These are the viewports available to pick from (or you can implement the interface yourself to suit your needs): - * `DefaultViewport`: this is the no-op viewport that is associated by default with any `BaseGame`. + * `DefaultViewport`: this is the no-op viewport that is associated by default with any `FlameGame`. * `FixedResolutionViewport`: this viewport transforms your Canvas so that, from the game perspective, the dimensions are always set to a fixed pre-defined value. This means it will scale the game as much as possible and add black bars if needed. -When using `BaseGame`, the operations performed by the viewport are done automatically to every +When using `FlameGame`, the operations performed by the viewport are done automatically to every render operation, and the `size` property in the game, instead of the logical widget size, becomes the size as seen through the viewport together with the zoom of the camera. If for some reason you need to access the original real logical pixel size, you can use `canvasSize`. For a more in depth @@ -43,7 +43,7 @@ dependent on: * User controlled zooming in and out. There is only one Camera implementation but it allows for many different configurations. Again, you -can use it standalone on your `Game` but it's already included and wired into `BaseGame`. +can use it standalone on your `Game` but it's already included and wired into `FlameGame`. One important thing to note about the Camera is that since (unlike the Viewport) it's intended to be dynamic, most camera movements won't immediately happen. Instead, the camera has a configurable @@ -87,7 +87,7 @@ When dealing with input events, it is imperative to convert screen coordinates t ### Using the camera with the Game class -If you are not using `BaseGame`, but instead are using the `Game` class, then you need to manage +If you are not using `FlameGame`, but instead are using the `Game` class, then you need to manage calling certain camera methods yourself. Let's say we have the following game structure, and we want to add the camera functionality: diff --git a/doc/collision_detection.md b/doc/collision_detection.md index 76746969a..007223e23 100644 --- a/doc/collision_detection.md +++ b/doc/collision_detection.md @@ -138,7 +138,7 @@ to your game so that the game knows that it should keep track of which component Example: ```dart -class MyGame extends BaseGame with HasCollidables { +class MyGame extends FlameGame with HasCollidables { ... } ``` diff --git a/doc/components.md b/doc/components.md index 05b0e3d88..6b2dda562 100644 --- a/doc/components.md +++ b/doc/components.md @@ -1,6 +1,6 @@ # Components -![Component Diagram](images/diagram.png) +![Components Diagram](images/diagram.png) This diagram might look intimidating, but don't worry, it is not as complex as it looks. @@ -11,46 +11,54 @@ If you want to skip reading about abstract classes you can jump directly to [PositionComponent](./components.md#PositionComponent). Every `Component` has a few methods that you can optionally implement, which are used by the -`BaseGame` class. If you are not using `BaseGame`, you can alternatively use these methods on your -own game loop. +`FlameGame` class. If you are not using `FlameGame`, you can use these methods on your own game loop +if you wish. -The `resize` method is called whenever the screen is resized, and in the beginning once when the -component is added to the game via the `add` method. +![Component Lifecycle Diagram](images/diagram.png) -The `shouldRemove` variable can be overridden or set to true and `BaseGame` will remove the +The `onGameResize` method is called whenever the screen is resized, and once in the beginning when +the component is added to the game via the `add` method. + +The `shouldRemove` variable can be overridden or set to true and `FlameGame` will remove the component before the next update loop. It will then no longer be rendered or updated. Note that -`game.remove(Component c)` can also be used to remove components from the game. +`game.remove(Component c)` and `component.removeFromParent()` also can be used to remove components +from its parent. -The `isHUD` variable can be overridden or set to true (defaults to `false`) to make the `BaseGame` -ignore the `camera` for this element, make it static in relation to the screen that is. - -The `onMount` method can be overridden to run initialization code for the component. When this -method is called, BaseGame ensures that all the mixins which would change this component's behavior -are already resolved. +The `isHUD` variable can be overridden or set to true (defaults to `false`) to make the `FlameGame` +ignore the `camera` for this element, making it static in relation to the screen that is. +Do note that this currently only works if the component is added directly to the root `FlameGame`. The `onRemove` method can be overridden to run code before the component is removed from the game, -it is only run once even if the component is removed both by using the `BaseGame` remove method and +it is only run once even if the component is removed both by using the parents remove method and the `Component` remove method. The `onLoad` method can be overridden to run asynchronous initialization code for the component, like loading an image for example. This method is executed after the initial "preparation" of the -component is run, meaning that this method is executed after `onMount` and just before the inclusion -of the component in the `BaseGame`'s list of components. +component has finished the first time, meaning that this method is executed after the first +`onGameResize` call and just before the inclusion of the component in the `FlameGame`'s (or another +`Component`'s) list of components. + +The `onMount` method can be overridden to run asynchronous initialization code that should +run every time the component is added to a new parent. This means that you should not initialize +`late` variables here, since this method might run several times throughout the component's +lifetime. This method is executed after the initial "preparation" of the component is done and after +`onGameResize` and `onLoad`, but before the inclusion of the component in the parent's list of +components. ## BaseComponent Usually if you are going to make your own component you want to extend `PositionComponent`, but if you want to be able to handle effects and child components but handle the positioning differently -you can extend the `BaseComponent`. +you can extend the `Component` directly. -It is used by `SpriteBodyComponent`, `PositionBodyComponent`, and `BodyComponent` in Forge2D since -those components doesn't have their position in relation to the screen, but in relation to the -Forge2D world. +`Component` is used by `SpriteBodyComponent`, `PositionBodyComponent`, and `BodyComponent` in +`flame_forge2d` since those components doesn't have their position in relation to the screen, but in +relation to the Forge2D world. ### Composability of components Sometimes it is useful to wrap other components inside of your component. For example by grouping -visual components through a hierarchy. You can do this by having child components on any component -that extends `BaseComponent`, for example `PositionComponent` or `BodyComponent`. +visual components through a hierarchy. You can do this by adding child components to any component, +for example `PositionComponent`. When you have child components on a component every time the parent is updated and rendered, all the children are rendered and updated with the same conditions. @@ -68,8 +76,8 @@ class GameOverPanel extends PositionComponent with HasGameRef { final gameOverText = GameOverText(spriteImage); // GameOverText is a Component final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent - addChild(gameRef, gameOverText); - addChild(gameRef, gameOverButton); + add(gameOverText); + add(gameOverButton); } @override @@ -113,7 +121,7 @@ created with a `Sprite`: ```dart import 'package:flame/components/component.dart'; -class MyGame extends BaseGame { +class MyGame extends FlameGame { late final SpriteComponent player; @override @@ -165,7 +173,7 @@ this.player = SpriteAnimationComponent.fromFrameData( ); ``` -If you are not using `BaseGame`, 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 animation object needs to be ticked to move the frames. ## SpriteAnimationGroup @@ -339,7 +347,7 @@ class MyParallaxComponent extends ParallaxComponent with HasGameRef { } } -class MyGame extends BaseGame { +class MyGame extends FlameGame { @override Future onLoad() async { add(MyParallaxComponent()); diff --git a/doc/debug.md b/doc/debug.md index eca3112dd..39f61b6df 100644 --- a/doc/debug.md +++ b/doc/debug.md @@ -19,13 +19,13 @@ class MyGame extends Game with FPSCounter { } ``` -## BaseGame features +## FlameGame features -Flame provides some debugging features for the `BaseGame` class. These features are enabled when +Flame provides some debugging features for the `FlameGame` class. These features are enabled when the `debugMode` property is set to `true` (or overridden to be `true`). When `debugMode` is enabled, each `PositionComponent` will be rendered with their bounding size, and have their positions written on the screen. This way, you can visually verify the components boundaries and positions. -To see a working example of the debugging features of the `BaseGame`, check this +To see a working example of the debugging features of the `FlameGame`, check this [example](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/components/debug.dart). diff --git a/doc/effects.md b/doc/effects.md index 7434da88b..9eae18faf 100644 --- a/doc/effects.md +++ b/doc/effects.md @@ -1,12 +1,10 @@ # Effects -An effect can be applied to any `Component` that the effect supports. +An effect can be added to any `Component` that the effect supports. -At the moment there are only `PositionComponentEffect`s, which are applied to `PositionComponent`s, -which are presented below. - -If you want to create an effect for another component just extend the `ComponentEffect` class and -add your created effect to the component by calling `component.addEffect(yourEffect)`. +If you want to create an effect for another component than the ones that already exist, just extend +the `ComponentEffect` class and add your created effect to the component by calling +`component.add(yourEffect)`. ## Common for all effects @@ -76,7 +74,7 @@ Usage example: import 'package:flame/effects.dart'; // Square is a PositionComponent -square.addEffect(MoveEffect( +square.add(MoveEffect( path: [Vector2(200, 200), Vector2(200, 100), Vector(0, 50)], speed: 250.0, curve: Curves.bounceInOut, @@ -108,7 +106,7 @@ Usage example: import 'package:flame/effects.dart'; // Square is a PositionComponent -square.addEffect(ScaleEffect( +square.add(ScaleEffect( scale: Vector2.all(2.0), speed: 1.0, curve: Curves.bounceInOut, @@ -128,7 +126,7 @@ Usage example: import 'package:flame/effects.dart'; // Square is a PositionComponent -square.addEffect(SizeEffect( +square.add(SizeEffect( size: Vector2.all(300), speed: 250.0, curve: Curves.bounceInOut, @@ -154,7 +152,7 @@ import 'dart:math'; import 'package:flame/effects.dart'; // Square is a PositionComponent -square.addEffect(RotateEffect( +square.add(RotateEffect( radians: 2 * pi, // In radians speed: 1.0, // Radians per second curve: Curves.easeInOut, @@ -167,7 +165,7 @@ This effect is a combination of other effects. You provide it with a list of you effects. The effects in the list should only be passed to the `SequenceEffect`, never added to a -`PositionComponent` with `addEffect`. +`PositionComponent` with `add`. **Note**: No effect (except the last) added to the sequence should have their `isInfinite` property set to `true`, because then naturally the sequence will get stuck once it gets to that effect. @@ -180,7 +178,7 @@ final sequence = SequenceEffect( effects: [move1, size, move2, rotate], isInfinite: true, isAlternating: true); -myComponent.addEffect(sequence); +myComponent.add(sequence); ``` An example of how to use the `SequenceEffect` can be found @@ -188,25 +186,21 @@ An example of how to use the `SequenceEffect` can be found ## CombinedEffect -This effect runs several different type of effects simultaneously. You provide it with a list of -your predefined effects and an offset in time which should pass in between starting each effect. +This effect runs several different type of effects simultaneously on the component that it is added +to. You provide it with a list of your predefined effects and if you don't want them to start or end +at the same time you can utilize the `initialDelay` and `peakDelay` to add time before or after the +effect runs. The effects in the list should only be passed to the `CombinedEffect`, never added to a -`PositionComponent` with `addEffect`. +`PositionComponent` with `add` at the same time. -**Note**: No effects should be of the same type since they will clash when trying to modify the -`PositionComponent`. - -You can make the combined effect go in a loop by setting both `isInfinite: true` and -`isAlternating: true`. +**Note**: No effects should be of the same type since they will clash when trying to modify for +example a `PositionComponent`. Usage example: ```dart -final combination = CombinedEffect( - effects: [move, size, rotate], - isInfinite: true, - isAlternating: true); -myComponent.addEffect(combination); +final combination = CombinedEffect(effects: [move, size, rotate]); +myComponent.add(combination); ``` An example of how to use the `CombinedEffect` can be found @@ -238,7 +232,7 @@ well. Usage example: ```dart -myComponent.addEffect( +myComponent.add( OpacityEffect( opacity: 0, duration: 0.5, @@ -251,13 +245,13 @@ An example of how to use the `OpacityEffect` can be found ## ColorEffect -This effect will change the base color of the paint, causing the rendered component to be tinted by the provided color. - +This effect will change the base color of the paint, causing the rendered component to be tinted by +the provided color. Usage example: ```dart -myComponent.addEffect( +myComponent.add( ColorEffect( color: const Color(0xFF00FF00), duration: 0.5, diff --git a/doc/forge2d.md b/doc/forge2d.md index 74a6acb0c..59d320f6d 100644 --- a/doc/forge2d.md +++ b/doc/forge2d.md @@ -13,10 +13,10 @@ in the [Forge2D example](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example) and in the pub.dev [installation instructions](https://pub.dev/packages/flame_forge2d). -## Forge2DGame (BaseGame extension) +## Forge2DGame (FlameGame extension) If you are going to use Forge2D in your project it can be a good idea to use the Forge2D specific -extension of the `BaseGame` class. +extension of the `FlameGame` class. It is called `Forge2DGame` and it will control the adding and removal of Forge2D's `BodyComponents` as well as your normal components. diff --git a/doc/game.md b/doc/game.md index 2d9340f02..ba5b20519 100644 --- a/doc/game.md +++ b/doc/game.md @@ -1,20 +1,20 @@ -# BaseGame +# FlameGame -`BaseGame` is the most basic and most commonly used `Game` class in Flame. +`FlameGame` is the most basic and most commonly used `Game` class in Flame. -The `BaseGame` class implements a `Component` based `Game`. Basically it has a list of `Component`s +The `FlameGame` class implements a `Component` based `Game`. Basically it has a list of `Component`s and passes the `update` and `render` calls to all `Component`s that have been added to the game. We refer to this component based system as the Flame Component System, FCS for short. Every time the game needs to be resized, for example when the orientation is changed, -`BaseGame` will call all of the `Component`s `resize` methods and it will also pass this information +`FlameGame` will call all of the `Component`s `resize` methods and it will also pass this information to the camera and viewport. -The `BaseGame.camera` controls which point in the coordinate space should be the top-left of the +The `FlameGame.camera` controls which point in the coordinate space should be the top-left of the screen (it defaults to [0,0] like a regular `Canvas`). -A `BaseGame` implementation example can be seen below: +A `FlameGame` implementation example can be seen below: ```dart class MyCrate extends SpriteComponent { @@ -34,7 +34,7 @@ class MyCrate extends SpriteComponent { } } -class MyGame extends BaseGame { +class MyGame extends FlameGame { MyGame() { add(MyCrate()); } @@ -55,16 +55,27 @@ main() { instead create an instance of your game first and reference it within your widget structure, like it is done in the example above. -To remove components from the list on a `BaseGame` the `remove` or `removeAll` methods can be used. +To remove components from the list on a `FlameGame` the `remove` or `removeAll` methods can be used. The first can be used if you just want to remove one component, and the second can be used when you want to remove a list of components. Any component on which the `remove()` method has been called will also be removed. You can do this simply by doing `yourComponent.remove();`. +## Lifecycle + +![Game Lifecycle Diagram](images/component_lifecycle.png) + +When a game first is added to a Flutter widget tree the following lifecycle methods will be called +in order: `onGameResize`, `onLoad` and `onMount`. After that it goes on to call `update` and +`render` back and forth every tick, until the widget is removed from the tree. +Once the `GameWidget` is removed from the tree, `onRemove` is called, just like when a normal +component is removed from the component tree. + ## Changing component priorities (render/update order) -To update a component with a new priority you have to call either `BaseGame.changePriority`, or -`BaseGame.changePriorities` if you want to change the priorities of many components at once. + +To update a component with a new priority you have to call either `FlameGame.changePriority`, or +`FlameGame.changePriorities` if you want to change the priorities of many components at once. This design is due to the fact that the components doesn't always have access to the component list and because rebalancing the component list is a fairly computationally expensive operation, so you would rather reorder the list once after all the priorities have been changed and not once for each @@ -76,26 +87,37 @@ before it. ## Debug mode -Flame's `BaseGame` class provides a variable called `debugMode`, which by default is `false`. It can +Flame's `FlameGame` class provides a variable called `debugMode`, which by default is `false`. It can however, be set to `true` to enable debug features for the components of the game. **Be aware** that the value of this variable is passed through to its components when they are added to the game, so if you change the `debugMode` at runtime, it will not affect already added components by default. To read more about the `debugMode` on Flame, please refer to the [Debug Docs](debug.md) -# Game +# Low-level Game API -The `Game` class is a low-level API that can be used when you want to implement the functionality of +![Game low-level API](images/game_mixin.png) + +The `Game` mixin is a low-level API that can be used when you want to implement the functionality of how the game engine should be structured. `Game` does not implement any `update` or -`render` function for example and is therefore marked as abstract. +`render` function for example. -**Note**: The `Game` class allows for more freedom of how to implement things, but you are also -missing out on a lot of the built-in features in Flame if you use it. +As you can see in the image above you'll have to use the `Loadable` and `Game` mixins if you want to +create your own game class, which is what is done with `OxygenGame`. + +The `Loadable` mixin has the lifecycle methods `onLoad`, `onMount` and `onRemove` in it, which are +called from the `GameWidget` (or another parent) when the game is loaded + mounted, or removed. +`onLoad` is only called the first time the class is added to a parent, but `onMount` (which is +called after `onLoad`) is called every time it is added to a new parent. `onRemove` is called when +the class is removed from a parent. + +**Note**: The `Game` mixin allows for more freedom of how to implement things, but you are also +missing out on all of the built-in features in Flame if you use it. An example of how a `Game` implementation could look like: ```dart -class MyGameSubClass extends Game { +class MyGameSubClass with Loadable, Game { @override void render(Canvas canvas) { // ... diff --git a/doc/gesture-input.md b/doc/gesture-input.md index bab74c853..c3d5c99b8 100644 --- a/doc/gesture-input.md +++ b/doc/gesture-input.md @@ -147,13 +147,15 @@ class MyGame extends Game with TapDetector { // Other methods omitted @override - void onTapDown(TapDownInfo event) { + bool onTapDown(TapDownInfo event) { print("Player tap down on ${event.eventPosition.game}"); + return true; } @override - void onTapUp(TapUpInfo event) { + bool onTapUp(TapUpInfo event) { print("Player tap up on ${event.eventPosition.game}"); + return true; } } ``` @@ -181,9 +183,9 @@ By adding the `HasTappableComponents` mixin to your game, and using the mixin `T components, you can override the following methods on your components: ```dart -void onTapCancel() {} -void onTapDown(TapDownInfo event) {} -void onTapUp(TapUpInfo event) {} +bool onTapCancel(); +bool onTapDown(TapDownInfo event); +bool onTapUp(TapUpInfo event); ``` Minimal component example: @@ -196,22 +198,25 @@ class TappableComponent extends PositionComponent with Tappable { // update and render omitted @override - void onTapUp(TapUpInfo event) { + bool onTapUp(TapUpInfo event) { print("tap up"); + return true; } @override - void onTapDown(TapDownInfo event) { + bool onTapDown(TapDownInfo event) { print("tap down"); + return true; } @override - void onTapCancel() { + bool onTapCancel() { print("tap cancel"); + return true; } } -class MyGame extends BaseGame with HasTappableComponents { +class MyGame extends FlameGame with HasTappableComponents { MyGame() { add(TappableComponent()); } @@ -230,10 +235,10 @@ your components, they can override the simple methods that enable an easy to use components. ```dart - void onDragStart(int pointerId, Vector2 startPosition) {} - void onDragUpdate(int pointerId, DragUpdateInfo event) {} - void onDragEnd(int pointerId, DragEndInfo event) {} - void onDragCancel(int pointerId) {} + bool onDragStart(int pointerId, Vector2 startPosition); + bool onDragUpdate(int pointerId, DragUpdateInfo event); + bool onDragEnd(int pointerId, DragEndInfo event); + bool onDragCancel(int pointerId); ``` Note that all events take a uniquely generated pointer id so you can, if desired, distinguish @@ -286,7 +291,7 @@ class DraggableComponent extends PositionComponent with Draggable { } } -class MyGame extends BaseGame with HasDraggableComponents { +class MyGame extends FlameGame with HasDraggableComponents { MyGame() { add(DraggableComponent()); } diff --git a/doc/images.md b/doc/images.md index 9bb2566c1..70b5a4587 100644 --- a/doc/images.md +++ b/doc/images.md @@ -214,7 +214,7 @@ svgInstance.renderPosition(canvas, position, size); or use the [SvgComponent]: ```dart -class MyGame extends BaseGame { +class MyGame extends FlameGame { Future onLoad() async { final svgInstance = await Svg.load('android.svg'); final size = Vector2.all(100); @@ -345,7 +345,7 @@ class MyGame extends Game { } ``` -FlareAnimations are normally used inside `FlareComponent`s, that way `BaseGame` will handle calling +FlareAnimations are normally used inside `FlareComponent`s, that way `FlameGame` will handle calling `render` and `update` automatically. You can see a full example of how to use Flare together with Flame in the example diff --git a/doc/images/component_lifecycle.png b/doc/images/component_lifecycle.png new file mode 100644 index 000000000..624caea24 Binary files /dev/null and b/doc/images/component_lifecycle.png differ diff --git a/doc/images/diagram.png b/doc/images/diagram.png index 1352975af..c80ef1d52 100644 Binary files a/doc/images/diagram.png and b/doc/images/diagram.png differ diff --git a/doc/images/game_mixin.png b/doc/images/game_mixin.png new file mode 100644 index 000000000..4e4b51516 Binary files /dev/null and b/doc/images/game_mixin.png differ diff --git a/doc/other-inputs.md b/doc/other-inputs.md index 9ae1c2048..2ae3a95df 100644 --- a/doc/other-inputs.md +++ b/doc/other-inputs.md @@ -16,7 +16,7 @@ add it to your game. Check this example to get a better understanding: ```dart -class MyGame extends BaseGame with HasDraggableComponents { +class MyGame extends FlameGame with HasDraggableComponents { MyGame() { joystick.addObserver(player); diff --git a/doc/particles.md b/doc/particles.md index 81bc90e7f..4f66326b0 100644 --- a/doc/particles.md +++ b/doc/particles.md @@ -3,7 +3,7 @@ Flame offers a basic, yet robust and extendable particle system. The core concept of this system is the `Particle` class, which is very similar in its behavior to the `ParticleComponent`. -The most basic usage of a `Particle` with `BaseGame` would look as following: +The most basic usage of a `Particle` with `FlameGame` would look as following: ```dart import 'package:flame/components.dart'; diff --git a/doc/text.md b/doc/text.md index f214885c3..7f065c452 100644 --- a/doc/text.md +++ b/doc/text.md @@ -62,7 +62,7 @@ Example usage: ```dart TextPaint regular = TextPaint(color: BasicPalette.white.color); -class MyGame extends BaseGame { +class MyGame extends FlameGame { @override Future onLoad() async { add(TextComponent('Hello, Flame', textRenderer: regular) diff --git a/doc/util.md b/doc/util.md index 647c71256..f299b0382 100644 --- a/doc/util.md +++ b/doc/util.md @@ -122,7 +122,7 @@ class MyGame extends Game { ``` -`Timer` instances can also be used inside a `BaseGame` game by using the `TimerComponent` class. +`Timer` instances can also be used inside a `FlameGame` game by using the `TimerComponent` class. `TimerComponent` example: @@ -131,8 +131,8 @@ import 'package:flame/timer.dart'; import 'package:flame/components/timer_component.dart'; import 'package:flame/game.dart'; -class MyBaseGame extends BaseGame { - MyBaseGame() { +class MyFlameGame extends FlameGame { + MyFlameGame() { add( TimerComponent( Timer( diff --git a/doc/widgets.md b/doc/widgets.md index d3e87c008..549b8f7fb 100644 --- a/doc/widgets.md +++ b/doc/widgets.md @@ -22,7 +22,7 @@ is expanded both ways. The `NineTileBox` widget implements a `Container` using that standard. This pattern is also implemented as a component in the `NineTileBoxComponent` so that you can add this feature directly -to your `BaseGame`. To get to know more about this, check the component docs +to your `FlameGame`. To get to know more about this, check the component docs [here](components.md#nine-tile-box-component). Here you can find an example of how to use it (without using the `NineTileBoxComponent`): diff --git a/examples/lib/commons/square_component.dart b/examples/lib/commons/square_component.dart index 02aad3c96..5b5c7a2d0 100644 --- a/examples/lib/commons/square_component.dart +++ b/examples/lib/commons/square_component.dart @@ -1,9 +1,10 @@ import 'dart:ui'; import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/palette.dart'; -class SquareComponent extends PositionComponent { +class SquareComponent extends PositionComponent with EffectsHelper { Paint paint = BasicPalette.white.paint(); SquareComponent({int priority = 0}) diff --git a/examples/lib/main.dart b/examples/lib/main.dart index cb0db4775..ce68f00ca 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -31,7 +31,6 @@ void main() async { addUtilsStories(dashbook); addCameraAndViewportStories(dashbook); addParallaxStories(dashbook); - addWidgetsStories(dashbook); runApp(dashbook); diff --git a/examples/lib/stories/animations/animation_group.dart b/examples/lib/stories/animations/animation_group.dart index abb97206b..a6632d10d 100644 --- a/examples/lib/stories/animations/animation_group.dart +++ b/examples/lib/stories/animations/animation_group.dart @@ -9,11 +9,12 @@ enum RobotState { running, } -class AnimationGroupExample extends BaseGame with TapDetector { +class AnimationGroupExample extends FlameGame with TapDetector { late SpriteAnimationGroupComponent robot; @override Future onLoad() async { + await super.onLoad(); final running = await loadSpriteAnimation( 'animations/robot.png', SpriteAnimationData.sequenced( diff --git a/examples/lib/stories/animations/animations.dart b/examples/lib/stories/animations/animations.dart index 30b556e6a..c26d007dd 100644 --- a/examples/lib/stories/animations/animations.dart +++ b/examples/lib/stories/animations/animations.dart @@ -7,11 +7,11 @@ import 'aseprite.dart'; import 'basic.dart'; const basicInfo = ''' -Basic example of `SpriteAnimation`s use in Flame's `BaseGame` +Basic example of `SpriteAnimation`s use in Flame's `FlameGame` The snippet shows how an animation can be loaded and added to the game ``` -class MyGame extends BaseGame { +class MyGame extends FlameGame { @override Future onLoad() async { final animation = await loadSpriteAnimation( diff --git a/examples/lib/stories/animations/aseprite.dart b/examples/lib/stories/animations/aseprite.dart index d588785c8..9366f4ef2 100644 --- a/examples/lib/stories/animations/aseprite.dart +++ b/examples/lib/stories/animations/aseprite.dart @@ -1,9 +1,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -class Aseprite extends BaseGame { +class Aseprite extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final image = await images.load('animations/chopper.png'); final jsonData = await assets.readJson('images/animations/chopper.json'); final animation = SpriteAnimation.fromAsepriteData(image, jsonData); diff --git a/examples/lib/stories/animations/basic.dart b/examples/lib/stories/animations/basic.dart index c64b6d27e..bc7d2592a 100644 --- a/examples/lib/stories/animations/basic.dart +++ b/examples/lib/stories/animations/basic.dart @@ -4,11 +4,12 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -class BasicAnimations extends BaseGame with TapDetector { +class BasicAnimations extends FlameGame with TapDetector { late Image creature; @override Future onLoad() async { + await super.onLoad(); creature = await images.load('animations/creature.png'); final animation = await loadSpriteAnimation( diff --git a/examples/lib/stories/camera_and_viewport/coordinate_systems.dart b/examples/lib/stories/camera_and_viewport/coordinate_systems.dart index b017456f2..fe7211eaf 100644 --- a/examples/lib/stories/camera_and_viewport/coordinate_systems.dart +++ b/examples/lib/stories/camera_and_viewport/coordinate_systems.dart @@ -90,7 +90,7 @@ class _CoordinateSystemsState extends State { /// A game that allows for camera control and displays Tap, Drag & Scroll /// events information on the screen, to allow exploration of the 3 coordinate /// systems of Flame (global, widget, game). -class CoordinateSystemsGame extends BaseGame +class CoordinateSystemsGame extends FlameGame with MultiTouchTapDetector, MultiTouchDragDetector, @@ -113,6 +113,7 @@ class CoordinateSystemsGame extends BaseGame @override Future onLoad() async { + await super.onLoad(); camera.followVector2(cameraPosition, relativeOffset: Anchor.topLeft); } diff --git a/examples/lib/stories/camera_and_viewport/coordinate_systems.dart.bak b/examples/lib/stories/camera_and_viewport/coordinate_systems.dart.bak index f33e62aca..df1c8f38a 100644 --- a/examples/lib/stories/camera_and_viewport/coordinate_systems.dart.bak +++ b/examples/lib/stories/camera_and_viewport/coordinate_systems.dart.bak @@ -90,7 +90,7 @@ class _CoordinateSystemsState extends State { /// A game that allows for camera control and displays Tap, Drag & Scroll /// events information on the screen, to allow exploration of the 3 coordinate /// systems of Flame (global, widget, game). -class CoordinateSystemsGame extends BaseGame +class CoordinateSystemsGame extends FlameGame with MultiTouchTapDetector, MultiTouchDragDetector, diff --git a/examples/lib/stories/camera_and_viewport/fixed_resolution.dart b/examples/lib/stories/camera_and_viewport/fixed_resolution.dart index 89add4f93..d41dba25e 100644 --- a/examples/lib/stories/camera_and_viewport/fixed_resolution.dart +++ b/examples/lib/stories/camera_and_viewport/fixed_resolution.dart @@ -5,7 +5,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/palette.dart'; -class FixedResolutionGame extends BaseGame with ScrollDetector, ScaleDetector { +class FixedResolutionGame extends FlameGame with ScrollDetector, ScaleDetector { static const info = ''' This example shows how to create a viewport with a fixed resolution. It is useful when you want the visible part of the game to be the same on all @@ -21,6 +21,7 @@ class FixedResolutionGame extends BaseGame with ScrollDetector, ScaleDetector { @override Future onLoad() async { + await super.onLoad(); final flameSprite = await loadSprite('layers/player.png'); camera.viewport = FixedResolutionViewport(viewportResolution); @@ -49,6 +50,7 @@ class Background extends PositionComponent with HasGameRef { @override Future onLoad() async { + await super.onLoad(); white = BasicPalette.white.paint(); hugeRect = size.toRect(); } diff --git a/examples/lib/stories/camera_and_viewport/follow_object.dart b/examples/lib/stories/camera_and_viewport/follow_object.dart index 6c4e3d4c2..cb648bcb7 100644 --- a/examples/lib/stories/camera_and_viewport/follow_object.dart +++ b/examples/lib/stories/camera_and_viewport/follow_object.dart @@ -4,7 +4,6 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/geometry.dart'; import 'package:flame/input.dart'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -137,7 +136,7 @@ class Rock extends SquareComponent with Hitbox, Collidable, Tappable { } } -class CameraAndViewportGame extends BaseGame +class CameraAndViewportGame extends FlameGame with HasCollidables, HasTappableComponents, HasKeyboardHandlerComponents { late MovableSquare square; @@ -149,6 +148,7 @@ class CameraAndViewportGame extends BaseGame @override Future onLoad() async { + await super.onLoad(); camera.viewport = FixedResolutionViewport(viewportResolution); add(Map()); diff --git a/examples/lib/stories/camera_and_viewport/zoom.dart b/examples/lib/stories/camera_and_viewport/zoom.dart index 530a7ce0a..6dbc13a7b 100644 --- a/examples/lib/stories/camera_and_viewport/zoom.dart +++ b/examples/lib/stories/camera_and_viewport/zoom.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -class ZoomGame extends BaseGame with ScrollDetector, ScaleDetector { +class ZoomGame extends FlameGame with ScrollDetector, ScaleDetector { final Vector2 viewportResolution; late SpriteComponent flame; @@ -14,6 +14,7 @@ class ZoomGame extends BaseGame with ScrollDetector, ScaleDetector { @override Future onLoad() async { + await super.onLoad(); final flameSprite = await loadSprite('flame.png'); camera.viewport = FixedResolutionViewport(viewportResolution); diff --git a/examples/lib/stories/collision_detection/circles.dart b/examples/lib/stories/collision_detection/circles.dart index 7f103d03f..b2592dd32 100644 --- a/examples/lib/stories/collision_detection/circles.dart +++ b/examples/lib/stories/collision_detection/circles.dart @@ -7,6 +7,12 @@ import 'package:flame/geometry.dart'; import 'package:flame/input.dart'; import 'package:flutter/material.dart' hide Image, Draggable; +const circlesInfo = ''' +This example will create a circle every time you tap on the screen. It will have +the initial velocity towards the center of the screen and if it touches another +circle both of them will change color. +'''; + class MyCollidable extends PositionComponent with HasGameRef, Hitbox, Collidable { late Vector2 velocity; @@ -26,6 +32,7 @@ class MyCollidable extends PositionComponent @override Future onLoad() async { + await super.onLoad(); final center = gameRef.size / 2; velocity = (center - position)..scaleTo(150); } @@ -34,7 +41,7 @@ class MyCollidable extends PositionComponent void update(double dt) { super.update(dt); if (_isWallHit) { - remove(); + removeFromParent(); return; } debugColor = _isCollision ? _collisionColor : _defaultColor; @@ -58,9 +65,10 @@ class MyCollidable extends PositionComponent } } -class Circles extends BaseGame with HasCollidables, TapDetector { +class Circles extends FlameGame with HasCollidables, TapDetector { @override Future onLoad() async { + super.onLoad(); add(ScreenCollidable()); } diff --git a/examples/lib/stories/collision_detection/collision_detection.dart b/examples/lib/stories/collision_detection/collision_detection.dart index 1e247deed..14489b078 100644 --- a/examples/lib/stories/collision_detection/collision_detection.dart +++ b/examples/lib/stories/collision_detection/collision_detection.dart @@ -6,35 +6,24 @@ import 'circles.dart'; import 'multiple_shapes.dart'; import 'only_shapes.dart'; -const basicInfo = ''' -An example with many hitboxes that move around on the screen and during -collisions they change color depending on what it is that they have collided -with. - -The snowman, the component built with three circles on top of each other, works -a little bit differently than the other components to show that you can have -multiple hitboxes within one component. - -On this example, you can "throw" the components by dragging them quickly in any -direction. -'''; - void addCollisionDetectionStories(Dashbook dashbook) { dashbook.storiesOf('Collision Detection') ..add( 'Circles', (_) => GameWidget(game: Circles()), codeLink: baseLink('collision_detection/circles.dart'), + info: circlesInfo, ) ..add( 'Multiple shapes', (_) => GameWidget(game: MultipleShapes()), codeLink: baseLink('collision_detection/multiple_shapes.dart'), - info: basicInfo, + info: multipleShapesInfo, ) ..add( - 'Shapes without components', + 'Simple Shapes', (_) => GameWidget(game: OnlyShapes()), codeLink: baseLink('collision_detection/only_shapes.dart'), + info: onlyShapesInfo, ); } diff --git a/examples/lib/stories/collision_detection/multiple_shapes.dart b/examples/lib/stories/collision_detection/multiple_shapes.dart index 9addedf79..8fd9821ea 100644 --- a/examples/lib/stories/collision_detection/multiple_shapes.dart +++ b/examples/lib/stories/collision_detection/multiple_shapes.dart @@ -9,6 +9,19 @@ import 'package:flame/input.dart'; import 'package:flame/palette.dart'; import 'package:flutter/material.dart' hide Image, Draggable; +const multipleShapesInfo = ''' +An example with many hitboxes that move around on the screen and during +collisions they change color depending on what it is that they have collided +with. + +The snowman, the component built with three circles on top of each other, works +a little bit differently than the other components to show that you can have +multiple hitboxes within one component. + +On this example, you can "throw" the components by dragging them quickly in any +direction. +'''; + enum Shapes { circle, rectangle, polygon } abstract class MyCollidable extends PositionComponent @@ -36,6 +49,7 @@ abstract class MyCollidable extends PositionComponent @override Future onLoad() async { + await super.onLoad(); _activePaint = Paint()..color = _defaultColor; } @@ -202,7 +216,7 @@ class CollidableSnowman extends MyCollidable { } } -class MultipleShapes extends BaseGame +class MultipleShapes extends FlameGame with HasCollidables, HasDraggableComponents, FPSCounter { final TextPaint fpsTextPaint = TextPaint( config: TextPaintConfig( diff --git a/examples/lib/stories/collision_detection/only_shapes.dart b/examples/lib/stories/collision_detection/only_shapes.dart index b48ced1f4..c898e0773 100644 --- a/examples/lib/stories/collision_detection/only_shapes.dart +++ b/examples/lib/stories/collision_detection/only_shapes.dart @@ -9,9 +9,15 @@ import 'package:flame/input.dart'; import 'package:flame/palette.dart'; import 'package:flutter/material.dart' hide Image, Draggable; +const onlyShapesInfo = ''' +An example which adds random shapes on the screen when you tap it, if you tap on +an already existing shape it will remove that shape and replace it with a new +one. +'''; + enum Shapes { circle, rectangle, polygon } -class OnlyShapes extends BaseGame with HasTappableComponents { +class OnlyShapes extends FlameGame with HasTappableComponents { final shapePaint = BasicPalette.red.paint()..style = PaintingStyle.stroke; final _rng = Random(); @@ -53,11 +59,16 @@ class OnlyShapes extends BaseGame with HasTappableComponents { } class MyShapeComponent extends ShapeComponent with Tappable { - MyShapeComponent(Shape shape, Paint shapePaint) : super(shape, shapePaint); + MyShapeComponent(Shape shape, Paint shapePaint) + : super( + shape, + shapePaint, + anchor: Anchor.center, + ); @override bool onTapDown(TapDownInfo info) { - remove(); + removeFromParent(); return true; } } diff --git a/examples/lib/stories/components/components.dart b/examples/lib/stories/components/components.dart index 705aa212c..7d0765130 100644 --- a/examples/lib/stories/components/components.dart +++ b/examples/lib/stories/components/components.dart @@ -4,13 +4,9 @@ import 'package:flame/game.dart'; import '../../commons/commons.dart'; import 'composability.dart'; import 'debug.dart'; +import 'game_in_game.dart'; import 'priority.dart'; -const priorityInfo = ''' -On this example, click on the square to bring them to the front by changing the -priority. -'''; - void addComponentsStories(Dashbook dashbook) { dashbook.storiesOf('Components') ..add( @@ -28,5 +24,11 @@ void addComponentsStories(Dashbook dashbook) { 'Debug', (_) => GameWidget(game: DebugGame()), codeLink: baseLink('components/debug.dart'), + ) + ..add( + 'Game-in-game', + (_) => GameWidget(game: GameInGame()), + codeLink: baseLink('components/game_in_game.dart'), + info: gameInGameInfo, ); } diff --git a/examples/lib/stories/components/composability.dart b/examples/lib/stories/components/composability.dart index f1696c960..411d4f1d5 100644 --- a/examples/lib/stories/components/composability.dart +++ b/examples/lib/stories/components/composability.dart @@ -15,8 +15,8 @@ class ParentSquare extends Square with HasGameRef { ParentSquare(Vector2 position, Vector2 size) : super(position, size); @override - void onMount() { - super.onMount(); + Future onLoad() async { + super.onLoad(); createChildren(); } @@ -29,32 +29,29 @@ class ParentSquare extends Square with HasGameRef { Square(Vector2(70, 200), Vector2(50, 50), angle: 5), ]; - children.forEach((c) => addChild(c, gameRef: gameRef)); + addAll(children); } } -class Composability extends BaseGame with TapDetector { - late ParentSquare _parent; +// This class only has `HasDraggableComponents` since the game-in-game example +// moves a draggable component to this game. +class Composability extends FlameGame with HasDraggableComponents { + late ParentSquare parentSquare; @override bool debugMode = true; @override Future onLoad() async { - _parent = ParentSquare(Vector2.all(400), Vector2.all(250)) + await super.onLoad(); + parentSquare = ParentSquare(Vector2.all(200), Vector2.all(300)) ..anchor = Anchor.center; - add(_parent); + add(parentSquare); } @override void update(double dt) { super.update(dt); - _parent.angle += dt / 2; - } - - @override - void onTap() { - super.onTap(); - _parent.scale = Vector2.all(1.5); + parentSquare.angle += dt; } } diff --git a/examples/lib/stories/components/debug.dart b/examples/lib/stories/components/debug.dart index 916041354..8ef95d36c 100644 --- a/examples/lib/stories/components/debug.dart +++ b/examples/lib/stories/components/debug.dart @@ -33,7 +33,7 @@ class LogoCompomnent extends SpriteComponent with HasGameRef { } } -class DebugGame extends BaseGame with FPSCounter { +class DebugGame extends FlameGame with FPSCounter { static final fpsTextPaint = TextPaint( config: const TextPaintConfig( color: Color(0xFFFFFFFF), @@ -45,6 +45,7 @@ class DebugGame extends BaseGame with FPSCounter { @override Future onLoad() async { + await super.onLoad(); final flameLogo = await loadSprite('flame.png'); final flame1 = LogoCompomnent(flameLogo); diff --git a/examples/lib/stories/components/game_in_game.dart b/examples/lib/stories/components/game_in_game.dart new file mode 100644 index 000000000..ee7ddf299 --- /dev/null +++ b/examples/lib/stories/components/game_in_game.dart @@ -0,0 +1,43 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +import '../input/draggables.dart'; +import 'composability.dart'; + +const gameInGameInfo = ''' +This example shows two games having another game as a parent. +One game contains draggable components and the other is a rotating square with +other square children. +After 5 seconds, one of the components from the game with draggable squares +changes its parent from its original game to the component that is rotating. +After another 5 seconds it changes back to its original parent, and so on. +'''; + +class GameInGame extends FlameGame with HasDraggableComponents { + @override + bool debugMode = true; + late final Composability composedGame; + late final DraggablesGame draggablesGame; + + @override + Future onLoad() async { + await super.onLoad(); + composedGame = Composability(); + draggablesGame = DraggablesGame(zoom: 1.0); + await add(composedGame); + await add(draggablesGame); + final child = draggablesGame.square; + final timer = Timer( + 5, + callback: () { + final newParent = child.parent == draggablesGame + ? composedGame.parentSquare + : draggablesGame; + child.changeParent(newParent); + }, + repeat: true, + ); + timer.start(); + add(TimerComponent(timer)); + } +} diff --git a/examples/lib/stories/components/priority.dart b/examples/lib/stories/components/priority.dart index 6c4e67aa4..3c89bb316 100644 --- a/examples/lib/stories/components/priority.dart +++ b/examples/lib/stories/components/priority.dart @@ -7,6 +7,11 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/palette.dart'; +const priorityInfo = ''' +On this example, click on the square to bring them to the front by changing the +priority. +'''; + class Square extends PositionComponent with HasGameRef, Tappable { late final Paint paint; @@ -18,7 +23,7 @@ class Square extends PositionComponent with HasGameRef, Tappable { @override bool onTapDown(TapDownInfo info) { - final topComponent = gameRef.components.last; + final topComponent = gameRef.children.last; if (topComponent != this) { gameRef.changePriority(this, topComponent.priority + 1); } @@ -43,9 +48,10 @@ class Square extends PositionComponent with HasGameRef, Tappable { } } -class Priority extends BaseGame with HasTappableComponents { +class Priority extends FlameGame with HasTappableComponents { @override Future onLoad() async { + await super.onLoad(); final squares = [ Square(Vector2(100, 100)), Square(Vector2(160, 100)), diff --git a/examples/lib/stories/effects/color_effect.dart b/examples/lib/stories/effects/color_effect.dart index ee15badf2..be30d657f 100644 --- a/examples/lib/stories/effects/color_effect.dart +++ b/examples/lib/stories/effects/color_effect.dart @@ -5,9 +5,10 @@ import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -class ColorEffectGame extends BaseGame with TapDetector { +class ColorEffectGame extends FlameGame with TapDetector { @override Future onLoad() async { + await super.onLoad(); final flameSprite = await loadSprite('flame.png'); add( @@ -15,7 +16,7 @@ class ColorEffectGame extends BaseGame with TapDetector { sprite: flameSprite, position: Vector2(300, 100), size: Vector2(149, 211), - )..addEffect( + )..add( ColorEffect( color: const Color(0xFF00FF00), duration: 0.5, diff --git a/examples/lib/stories/effects/combined_effect.dart b/examples/lib/stories/effects/combined_effect.dart index 430785a58..9f38e628e 100644 --- a/examples/lib/stories/effects/combined_effect.dart +++ b/examples/lib/stories/effects/combined_effect.dart @@ -9,11 +9,12 @@ import '../../commons/square_component.dart'; final green = Paint()..color = const Color(0xAA338833); final red = Paint()..color = const Color(0xAA883333); -class CombinedEffectGame extends BaseGame with TapDetector { +class CombinedEffectGame extends FlameGame with TapDetector { late SquareComponent greenSquare, redSquare; @override Future onLoad() async { + await super.onLoad(); greenSquare = SquareComponent() ..paint = green ..position.setValues(100, 100); @@ -34,11 +35,14 @@ class CombinedEffectGame extends BaseGame with TapDetector { final move = MoveEffect( path: [ currentTap, - currentTap - Vector2(50, 20), - currentTap + Vector2.all(30), + currentTap + Vector2(-20, 50), + currentTap + Vector2(-50, -50), + currentTap + Vector2(50, 0), ], + isAlternating: true, duration: 4.0, curve: Curves.bounceInOut, + initialDelay: 0.6, ); final scale = SizeEffect( @@ -46,20 +50,21 @@ class CombinedEffectGame extends BaseGame with TapDetector { speed: 200.0, curve: Curves.linear, isAlternating: true, + peakDelay: 1.0, ); final rotate = RotateEffect( angle: currentTap.angleTo(Vector2.all(100)), duration: 3, curve: Curves.decelerate, + isAlternating: true, + initialDelay: 1.0, + peakDelay: 1.0, ); final combination = CombinedEffect( effects: [move, rotate, scale], - isAlternating: true, - offset: 0.5, - onComplete: () => print('onComplete callback'), ); - greenSquare.addEffect(combination); + greenSquare.add(combination); } } diff --git a/examples/lib/stories/effects/effects.dart b/examples/lib/stories/effects/effects.dart index 79e6a86fe..43f4444dc 100644 --- a/examples/lib/stories/effects/effects.dart +++ b/examples/lib/stories/effects/effects.dart @@ -12,17 +12,26 @@ import 'scale_effect.dart'; import 'sequence_effect.dart'; import 'size_effect.dart'; +const scaleInfo = ''' +The `ScaleEffect` scales up the canvas before drawing the components and its +children. +In this example you can tap the screen and the component will scale up or down, +depending on its current state. +'''; + void addEffectsStories(Dashbook dashbook) { dashbook.storiesOf('Effects') ..add( 'Size Effect', (_) => GameWidget(game: SizeEffectGame()), codeLink: baseLink('effects/size_effect.dart'), + info: sizeInfo, ) ..add( 'Scale Effect', (_) => GameWidget(game: ScaleEffectGame()), codeLink: baseLink('effects/scale_effect.dart'), + info: scaleInfo, ) ..add( 'Move Effect', diff --git a/examples/lib/stories/effects/infinite_effect.dart b/examples/lib/stories/effects/infinite_effect.dart index b7cef901f..905cb5a8e 100644 --- a/examples/lib/stories/effects/infinite_effect.dart +++ b/examples/lib/stories/effects/infinite_effect.dart @@ -18,13 +18,14 @@ SquareComponent makeSquare(Paint paint) { ..position.setValues(100, 100); } -class InfiniteEffectGame extends BaseGame with TapDetector { +class InfiniteEffectGame extends FlameGame with TapDetector { late SquareComponent greenSquare; late SquareComponent redSquare; late SquareComponent orangeSquare; @override Future onLoad() async { + await super.onLoad(); add(greenSquare = makeSquare(green)); add(redSquare = makeSquare(red)); add(orangeSquare = makeSquare(orange)); @@ -38,7 +39,7 @@ class InfiniteEffectGame extends BaseGame with TapDetector { redSquare.clearEffects(); orangeSquare.clearEffects(); - greenSquare.addEffect( + greenSquare.add( MoveEffect( path: [p], speed: 250.0, @@ -48,7 +49,7 @@ class InfiniteEffectGame extends BaseGame with TapDetector { ), ); - redSquare.addEffect( + redSquare.add( SizeEffect( size: p, speed: 250.0, @@ -58,7 +59,7 @@ class InfiniteEffectGame extends BaseGame with TapDetector { ), ); - orangeSquare.addEffect( + orangeSquare.add( RotateEffect( angle: (p.x + p.y) % (2 * pi), speed: 1.0, // Radians per second diff --git a/examples/lib/stories/effects/move_effect.dart b/examples/lib/stories/effects/move_effect.dart index d0739d045..eb84dee51 100644 --- a/examples/lib/stories/effects/move_effect.dart +++ b/examples/lib/stories/effects/move_effect.dart @@ -6,18 +6,19 @@ import 'package:flutter/material.dart'; import '../../commons/square_component.dart'; -class MoveEffectGame extends BaseGame with TapDetector { +class MoveEffectGame extends FlameGame with TapDetector { late SquareComponent square; @override Future onLoad() async { + await super.onLoad(); square = SquareComponent()..position.setValues(100, 100); add(square); } @override void onTapUp(TapUpInfo info) { - square.addEffect( + square.add( MoveEffect( path: [ info.eventPosition.game, @@ -30,6 +31,7 @@ class MoveEffectGame extends BaseGame with TapDetector { speed: 250.0, curve: Curves.bounceInOut, isAlternating: true, + peakDelay: 2.0, ), ); } diff --git a/examples/lib/stories/effects/opacity_effect.dart b/examples/lib/stories/effects/opacity_effect.dart index 535032272..c165e56f4 100644 --- a/examples/lib/stories/effects/opacity_effect.dart +++ b/examples/lib/stories/effects/opacity_effect.dart @@ -3,11 +3,12 @@ import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -class OpacityEffectGame extends BaseGame with TapDetector { +class OpacityEffectGame extends FlameGame with TapDetector { late final SpriteComponent sprite; @override Future onLoad() async { + await super.onLoad(); final flameSprite = await loadSprite('flame.png'); add( sprite = SpriteComponent( @@ -22,7 +23,7 @@ class OpacityEffectGame extends BaseGame with TapDetector { sprite: flameSprite, position: Vector2(300, 100), size: Vector2(149, 211), - )..addEffect( + )..add( OpacityEffect( opacity: 0, duration: 0.5, @@ -37,9 +38,9 @@ class OpacityEffectGame extends BaseGame with TapDetector { void onTap() { final opacity = sprite.paint.color.opacity; if (opacity == 1) { - sprite.addEffect(OpacityEffect.fadeOut()); + sprite.add(OpacityEffect.fadeOut()); } else if (opacity == 0) { - sprite.addEffect(OpacityEffect.fadeIn()); + sprite.add(OpacityEffect.fadeIn()); } } } diff --git a/examples/lib/stories/effects/rotate_effect.dart b/examples/lib/stories/effects/rotate_effect.dart index b5aa1122a..c93dca136 100644 --- a/examples/lib/stories/effects/rotate_effect.dart +++ b/examples/lib/stories/effects/rotate_effect.dart @@ -8,11 +8,12 @@ import 'package:flutter/material.dart'; import '../../commons/square_component.dart'; -class RotateEffectGame extends BaseGame with TapDetector { +class RotateEffectGame extends FlameGame with TapDetector { late SquareComponent square; @override Future onLoad() async { + await super.onLoad(); square = SquareComponent() ..position.setValues(200, 200) ..anchor = Anchor.center; @@ -21,7 +22,7 @@ class RotateEffectGame extends BaseGame with TapDetector { @override void onTap() { - square.addEffect( + square.add( RotateEffect( angle: 2 * pi, isRelative: true, diff --git a/examples/lib/stories/effects/scale_effect.dart b/examples/lib/stories/effects/scale_effect.dart index f9bffbe2e..a49bcf1b4 100644 --- a/examples/lib/stories/effects/scale_effect.dart +++ b/examples/lib/stories/effects/scale_effect.dart @@ -8,12 +8,13 @@ import 'package:flutter/material.dart'; import '../../commons/square_component.dart'; -class ScaleEffectGame extends BaseGame with TapDetector { +class ScaleEffectGame extends FlameGame with TapDetector { late SquareComponent square; bool grow = true; @override Future onLoad() async { + await super.onLoad(); square = SquareComponent() ..position.setValues(200, 200) ..anchor = Anchor.center; @@ -23,7 +24,7 @@ class ScaleEffectGame extends BaseGame with TapDetector { ..size = Vector2.all(20) ..anchor = Anchor.center; - square.addChild(childSquare); + square.add(childSquare); add(square); } @@ -32,7 +33,7 @@ class ScaleEffectGame extends BaseGame with TapDetector { final s = grow ? 3.0 : 1.0; grow = !grow; - square.addEffect( + square.add( ScaleEffect( scale: Vector2.all(s), speed: 2.0, diff --git a/examples/lib/stories/effects/sequence_effect.dart b/examples/lib/stories/effects/sequence_effect.dart index 9dc786c09..887904ef4 100644 --- a/examples/lib/stories/effects/sequence_effect.dart +++ b/examples/lib/stories/effects/sequence_effect.dart @@ -8,11 +8,12 @@ import '../../commons/square_component.dart'; final green = Paint()..color = const Color(0xAA338833); -class SequenceEffectGame extends BaseGame with TapDetector { +class SequenceEffectGame extends FlameGame with TapDetector { late SquareComponent greenSquare; @override Future onLoad() async { + await super.onLoad(); greenSquare = SquareComponent() ..paint = green ..position.setValues(100, 100); @@ -24,16 +25,10 @@ class SequenceEffectGame extends BaseGame with TapDetector { final currentTap = info.eventPosition.game; greenSquare.clearEffects(); - final move1 = MoveEffect( - path: [currentTap], - speed: 250.0, - curve: Curves.bounceInOut, - isAlternating: true, - ); - - final move2 = MoveEffect( + final move = MoveEffect( path: [ - currentTap + Vector2(0, 50), + currentTap, + currentTap + Vector2(-20, 50), currentTap + Vector2(-50, -50), currentTap + Vector2(50, 0), ], @@ -41,8 +36,8 @@ class SequenceEffectGame extends BaseGame with TapDetector { curve: Curves.easeIn, ); - final scale = SizeEffect( - size: currentTap, + final size = SizeEffect( + size: currentTap - greenSquare.position, speed: 100.0, curve: Curves.easeInCubic, ); @@ -53,17 +48,11 @@ class SequenceEffectGame extends BaseGame with TapDetector { curve: Curves.decelerate, ); - final combination = CombinedEffect( - effects: [move2, rotate], - isAlternating: true, - onComplete: () => print('combination complete'), - ); - final sequence = SequenceEffect( - effects: [move1, scale, combination], + effects: [size, rotate, move], isAlternating: true, ); sequence.onComplete = () => print('sequence complete'); - greenSquare.addEffect(sequence); + greenSquare.add(sequence); } } diff --git a/examples/lib/stories/effects/size_effect.dart b/examples/lib/stories/effects/size_effect.dart index 28fa1f3a9..69bba58b6 100644 --- a/examples/lib/stories/effects/size_effect.dart +++ b/examples/lib/stories/effects/size_effect.dart @@ -3,19 +3,35 @@ import 'package:flame/effects.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; import '../../commons/square_component.dart'; -class SizeEffectGame extends BaseGame with TapDetector { +const sizeInfo = ''' +The `SizeEffect` changes the size of the component, the sizes of the children +will stay the same. +In this example you can tap the screen and the component will size up or down, +depending on its current state. +'''; + +class SizeEffectGame extends FlameGame with TapDetector { late SquareComponent square; bool grow = true; @override Future onLoad() async { + await super.onLoad(); square = SquareComponent() ..position.setValues(200, 200) ..anchor = Anchor.center; + square.paint = BasicPalette.white.paint()..style = PaintingStyle.stroke; + final childSquare = SquareComponent() + ..position = Vector2.all(70) + ..size = Vector2.all(20) + ..anchor = Anchor.center; + + square.add(childSquare); add(square); } @@ -24,7 +40,7 @@ class SizeEffectGame extends BaseGame with TapDetector { final s = grow ? 300.0 : 100.0; grow = !grow; - square.addEffect( + square.add( SizeEffect( size: Vector2.all(s), speed: 250.0, diff --git a/examples/lib/stories/input/draggables.dart b/examples/lib/stories/input/draggables.dart index fef8e7170..fc35340c2 100644 --- a/examples/lib/stories/input/draggables.dart +++ b/examples/lib/stories/input/draggables.dart @@ -6,8 +6,7 @@ import 'package:flutter/material.dart' show Colors; // Note: this component does not consider the possibility of multiple // simultaneous drags with different pointerIds. -class DraggableSquare extends PositionComponent - with Draggable, HasGameRef { +class DraggableSquare extends PositionComponent with Draggable, HasGameRef { @override bool debugMode = true; @@ -18,12 +17,13 @@ class DraggableSquare extends PositionComponent ); Vector2? dragDeltaPosition; - bool get isDragging => dragDeltaPosition != null; @override void update(double dt) { super.update(dt); - debugColor = isDragging ? Colors.greenAccent : Colors.purple; + debugColor = isDragged && parent is DraggablesGame + ? Colors.greenAccent + : Colors.purple; } @override @@ -34,6 +34,9 @@ class DraggableSquare extends PositionComponent @override bool onDragUpdate(int pointerId, DragUpdateInfo info) { + if (parent is! DraggablesGame) { + return true; + } final dragDeltaPosition = this.dragDeltaPosition; if (dragDeltaPosition == null) { return false; @@ -56,15 +59,17 @@ class DraggableSquare extends PositionComponent } } -class DraggablesGame extends BaseGame with HasDraggableComponents { +class DraggablesGame extends FlameGame with HasDraggableComponents { final double zoom; + late final DraggableSquare square; DraggablesGame({required this.zoom}); @override Future onLoad() async { + await super.onLoad(); camera.zoom = zoom; - add(DraggableSquare()); + add(square = DraggableSquare()); add(DraggableSquare()..y = 350); } } diff --git a/examples/lib/stories/input/hoverables.dart b/examples/lib/stories/input/hoverables.dart index 5919eff77..f1b765cc9 100644 --- a/examples/lib/stories/input/hoverables.dart +++ b/examples/lib/stories/input/hoverables.dart @@ -20,9 +20,11 @@ class HoverableSquare extends PositionComponent with Hoverable { } } -class HoverablesGame extends BaseGame with HasHoverableComponents, TapDetector { +class HoverablesGame extends FlameGame + with HasHoverableComponents, TapDetector { @override Future onLoad() async { + await super.onLoad(); add(HoverableSquare(Vector2(200, 500))); add(HoverableSquare(Vector2(700, 300))); } diff --git a/examples/lib/stories/input/input.dart b/examples/lib/stories/input/input.dart index 798cdc9d6..809e0e010 100644 --- a/examples/lib/stories/input/input.dart +++ b/examples/lib/stories/input/input.dart @@ -22,6 +22,7 @@ void addInputStories(Dashbook dashbook) { 'Keyboard', (_) => GameWidget(game: KeyboardGame()), codeLink: baseLink('input/keyboard.dart'), + info: keyboardInfo, ) ..add( 'Mouse Movement', diff --git a/examples/lib/stories/input/joystick.dart b/examples/lib/stories/input/joystick.dart index e2891d731..3b1a66360 100644 --- a/examples/lib/stories/input/joystick.dart +++ b/examples/lib/stories/input/joystick.dart @@ -7,12 +7,13 @@ import 'package:flutter/painting.dart'; import 'joystick_player.dart'; -class JoystickGame extends BaseGame with HasDraggableComponents { +class JoystickGame extends FlameGame with HasDraggableComponents { late final JoystickPlayer player; late final JoystickComponent joystick; @override Future onLoad() async { + await super.onLoad(); final knobPaint = BasicPalette.blue.withAlpha(200).paint(); final backgroundPaint = BasicPalette.blue.withAlpha(100).paint(); joystick = JoystickComponent( diff --git a/examples/lib/stories/input/joystick_advanced.dart b/examples/lib/stories/input/joystick_advanced.dart index 01888f291..c303a631b 100644 --- a/examples/lib/stories/input/joystick_advanced.dart +++ b/examples/lib/stories/input/joystick_advanced.dart @@ -13,7 +13,7 @@ import 'package:flutter/painting.dart'; import 'joystick_player.dart'; -class JoystickAdvancedGame extends BaseGame +class JoystickAdvancedGame extends FlameGame with HasDraggableComponents, HasTappableComponents { late final JoystickPlayer player; late final JoystickComponent joystick; @@ -22,6 +22,7 @@ class JoystickAdvancedGame extends BaseGame @override Future onLoad() async { + await super.onLoad(); final image = await images.load('joystick.png'); final sheet = SpriteSheet.fromColumnsAndRows( image: image, @@ -95,7 +96,7 @@ class JoystickAdvancedGame extends BaseGame right: 85, bottom: 150, ), - onPressed: () => player.addEffect( + onPressed: () => player.add( rotateEffect..angle = 8 * rng.nextDouble(), ), ); @@ -116,14 +117,14 @@ class JoystickAdvancedGame extends BaseGame top: 80, left: 80, ), - )..addChild(speedText); + )..add(speedText); final directionWithMargin = HudMarginComponent( margin: const EdgeInsets.only( top: 110, left: 80, ), - )..addChild(directionText); + )..add(directionText); add(player); add(joystick); diff --git a/examples/lib/stories/input/keyboard.dart b/examples/lib/stories/input/keyboard.dart index b2598fbf1..6c610d4ff 100644 --- a/examples/lib/stories/input/keyboard.dart +++ b/examples/lib/stories/input/keyboard.dart @@ -1,13 +1,18 @@ import 'dart:ui'; import 'package:flame/game.dart'; - import 'package:flame/input.dart'; import 'package:flame/palette.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -class KeyboardGame extends Game with KeyboardEvents { +const keyboardInfo = ''' +Example showcasing how to act on keyboard events. +It also briefly showcases how to create a game without the FlameGame. +Usage: Use A S D F to steer the rectangle. +'''; + +class KeyboardGame with Loadable, Game, KeyboardEvents { static final Paint white = BasicPalette.white.paint(); static const int speed = 200; diff --git a/examples/lib/stories/input/mouse_cursor.dart b/examples/lib/stories/input/mouse_cursor.dart index b66414d07..69e6e0fa9 100644 --- a/examples/lib/stories/input/mouse_cursor.dart +++ b/examples/lib/stories/input/mouse_cursor.dart @@ -5,7 +5,7 @@ import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -class MouseCursorGame extends Game with MouseMovementDetector { +class MouseCursorGame extends FlameGame with MouseMovementDetector { static const speed = 200; static final Paint _blue = BasicPalette.blue.paint(); static final Paint _white = BasicPalette.white.paint(); @@ -25,6 +25,7 @@ class MouseCursorGame extends Game with MouseMovementDetector { @override void render(Canvas canvas) { + super.render(canvas); canvas.drawRect( _toRect(), onTarget ? _blue : _white, @@ -33,6 +34,7 @@ class MouseCursorGame extends Game with MouseMovementDetector { @override void update(double dt) { + super.update(dt); final target = this.target; if (target != null) { final hovering = _toRect().contains(target.toOffset()); diff --git a/examples/lib/stories/input/mouse_movement.dart b/examples/lib/stories/input/mouse_movement.dart index cd20853c9..8bd550b36 100644 --- a/examples/lib/stories/input/mouse_movement.dart +++ b/examples/lib/stories/input/mouse_movement.dart @@ -4,7 +4,7 @@ import 'package:flame/input.dart'; import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; -class MouseMovementGame extends BaseGame with MouseMovementDetector { +class MouseMovementGame extends FlameGame with MouseMovementDetector { static const speed = 200; static final Paint _blue = BasicPalette.blue.paint(); static final Paint _white = BasicPalette.white.paint(); diff --git a/examples/lib/stories/input/multitap.dart b/examples/lib/stories/input/multitap.dart index 395fb13a0..cfd3260a6 100644 --- a/examples/lib/stories/input/multitap.dart +++ b/examples/lib/stories/input/multitap.dart @@ -5,7 +5,7 @@ import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; /// Includes an example including advanced detectors -class MultitapGame extends BaseGame with MultiTouchTapDetector { +class MultitapGame extends FlameGame with MultiTouchTapDetector { static final whitePaint = BasicPalette.white.paint(); static final tapSize = Vector2.all(50); diff --git a/examples/lib/stories/input/multitap_advanced.dart b/examples/lib/stories/input/multitap_advanced.dart index 35e97a928..ddc16fe07 100644 --- a/examples/lib/stories/input/multitap_advanced.dart +++ b/examples/lib/stories/input/multitap_advanced.dart @@ -5,7 +5,7 @@ import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; /// Showcases how to mix two advanced detectors -class MultitapAdvancedGame extends BaseGame +class MultitapAdvancedGame extends FlameGame with MultiTouchTapDetector, MultiTouchDragDetector { static final whitePaint = BasicPalette.white.paint(); static final tapSize = Vector2.all(50); diff --git a/examples/lib/stories/input/overlapping_tappables.dart b/examples/lib/stories/input/overlapping_tappables.dart index c7a77d002..b3b684a21 100644 --- a/examples/lib/stories/input/overlapping_tappables.dart +++ b/examples/lib/stories/input/overlapping_tappables.dart @@ -49,9 +49,10 @@ class TappableSquare extends PositionComponent with Tappable { } } -class OverlappingTappablesGame extends BaseGame with HasTappableComponents { +class OverlappingTappablesGame extends FlameGame with HasTappableComponents { @override Future onLoad() async { + await super.onLoad(); add(TappableSquare(position: Vector2(100, 100))); add(TappableSquare(position: Vector2(150, 150))); add(TappableSquare(position: Vector2(100, 200))); diff --git a/examples/lib/stories/input/scroll.dart b/examples/lib/stories/input/scroll.dart index 25e30a380..bed254a38 100644 --- a/examples/lib/stories/input/scroll.dart +++ b/examples/lib/stories/input/scroll.dart @@ -4,7 +4,7 @@ import 'package:flame/input.dart'; import 'package:flame/palette.dart'; import 'package:flutter/material.dart'; -class ScrollGame extends BaseGame with ScrollDetector { +class ScrollGame extends FlameGame with ScrollDetector { static const speed = 2000.0; final _size = Vector2.all(50); final _paint = BasicPalette.white.paint(); diff --git a/examples/lib/stories/input/tappables.dart b/examples/lib/stories/input/tappables.dart index a4c0c758d..915c8c3ab 100644 --- a/examples/lib/stories/input/tappables.dart +++ b/examples/lib/stories/input/tappables.dart @@ -41,9 +41,10 @@ class TappableSquare extends PositionComponent with Tappable { } } -class TappablesGame extends BaseGame with HasTappableComponents { +class TappablesGame extends FlameGame with HasTappableComponents { @override Future onLoad() async { + await super.onLoad(); add(TappableSquare()..anchor = Anchor.center); add(TappableSquare()..y = 350); } diff --git a/examples/lib/stories/parallax/advanced.dart b/examples/lib/stories/parallax/advanced.dart index 897d0fafc..37020ee0c 100644 --- a/examples/lib/stories/parallax/advanced.dart +++ b/examples/lib/stories/parallax/advanced.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; -class AdvancedParallaxGame extends BaseGame { +class AdvancedParallaxGame extends FlameGame { final _layersMeta = { 'parallax/bg.png': 1.0, 'parallax/mountain-far.png': 1.5, @@ -13,6 +13,7 @@ class AdvancedParallaxGame extends BaseGame { @override Future onLoad() async { + await super.onLoad(); final layers = _layersMeta.entries.map( (e) => loadParallaxLayer( ParallaxImageData(e.key), diff --git a/examples/lib/stories/parallax/animation.dart b/examples/lib/stories/parallax/animation.dart index 41ffad74d..73d14d739 100644 --- a/examples/lib/stories/parallax/animation.dart +++ b/examples/lib/stories/parallax/animation.dart @@ -3,9 +3,10 @@ import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; import 'package:flutter/painting.dart'; -class AnimationParallaxGame extends BaseGame { +class AnimationParallaxGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final cityLayer = await loadParallaxLayer( ParallaxImageData('parallax/city.png'), ); diff --git a/examples/lib/stories/parallax/basic.dart b/examples/lib/stories/parallax/basic.dart index 65acf4e97..dbe705bc1 100644 --- a/examples/lib/stories/parallax/basic.dart +++ b/examples/lib/stories/parallax/basic.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; -class BasicParallaxGame extends BaseGame { +class BasicParallaxGame extends FlameGame { final _imageNames = [ ParallaxImageData('parallax/bg.png'), ParallaxImageData('parallax/mountain-far.png'), @@ -13,6 +13,7 @@ class BasicParallaxGame extends BaseGame { @override Future onLoad() async { + await super.onLoad(); final parallax = await loadParallaxComponent( _imageNames, baseVelocity: Vector2(20, 0), diff --git a/examples/lib/stories/parallax/component.dart b/examples/lib/stories/parallax/component.dart index 06b99f8a7..83ddd9d42 100644 --- a/examples/lib/stories/parallax/component.dart +++ b/examples/lib/stories/parallax/component.dart @@ -2,9 +2,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; -class ComponentParallaxGame extends BaseGame { +class ComponentParallaxGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); add(MyParallaxComponent()); } } @@ -12,6 +13,7 @@ class ComponentParallaxGame extends BaseGame { class MyParallaxComponent extends ParallaxComponent { @override Future onLoad() async { + await super.onLoad(); parallax = await gameRef.loadParallax( [ ParallaxImageData('parallax/bg.png'), diff --git a/examples/lib/stories/parallax/no_fcs.dart b/examples/lib/stories/parallax/no_fcs.dart index 5f39322eb..d08c407b4 100644 --- a/examples/lib/stories/parallax/no_fcs.dart +++ b/examples/lib/stories/parallax/no_fcs.dart @@ -7,9 +7,9 @@ import 'package:flutter/material.dart'; /// This examples serves to test the Parallax feature outside of the /// Flame Component System (FCS), use the other files in this folder /// for examples on how to use parallax with FCS -/// FCS is only used when you extend BaseGame, not Game, +/// FCS is only used when you extend FlameGame, not Game, /// like we do in this example. -class NoFCSParallaxGame extends Game { +class NoFCSParallaxGame with Loadable, Game { late Parallax parallax; @override diff --git a/examples/lib/stories/parallax/sandbox_layer.dart b/examples/lib/stories/parallax/sandbox_layer.dart index 270a8ca0a..f15a1ed22 100644 --- a/examples/lib/stories/parallax/sandbox_layer.dart +++ b/examples/lib/stories/parallax/sandbox_layer.dart @@ -3,7 +3,7 @@ import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; import 'package:flutter/painting.dart'; -class SandBoxLayerParallaxGame extends BaseGame { +class SandBoxLayerParallaxGame extends FlameGame { final Vector2 planeSpeed; final ImageRepeat planeRepeat; final LayerFill planeFill; @@ -18,6 +18,7 @@ class SandBoxLayerParallaxGame extends BaseGame { @override Future onLoad() async { + await super.onLoad(); final bgLayer = await loadParallaxLayer( ParallaxImageData('parallax/bg.png'), ); diff --git a/examples/lib/stories/parallax/small_parallax.dart b/examples/lib/stories/parallax/small_parallax.dart index 4aedeb9c7..dd1bb1fde 100644 --- a/examples/lib/stories/parallax/small_parallax.dart +++ b/examples/lib/stories/parallax/small_parallax.dart @@ -2,9 +2,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/parallax.dart'; -class SmallParallaxGame extends BaseGame { +class SmallParallaxGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final component = await loadParallaxComponent( [ ParallaxImageData('parallax/bg.png'), diff --git a/examples/lib/stories/rendering/flip.dart b/examples/lib/stories/rendering/flip.dart index 46f20c793..5127b1a71 100644 --- a/examples/lib/stories/rendering/flip.dart +++ b/examples/lib/stories/rendering/flip.dart @@ -3,9 +3,10 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; -class FlipSpriteGame extends BaseGame { +class FlipSpriteGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final image = await images.load('animations/chopper.png'); final regular = buildAnimationComponent(image); diff --git a/examples/lib/stories/rendering/layers.dart b/examples/lib/stories/rendering/layers.dart index 1003fbad8..49f472572 100644 --- a/examples/lib/stories/rendering/layers.dart +++ b/examples/lib/stories/rendering/layers.dart @@ -44,12 +44,13 @@ class BackgroundLayer extends PreRenderedLayer { } } -class LayerGame extends Game { +class LayerGame extends FlameGame { late Layer gameLayer; late Layer backgroundLayer; @override Future onLoad() async { + await super.onLoad(); final playerSprite = Sprite(await images.load('layers/player.png')); final enemySprite = Sprite(await images.load('layers/enemy.png')); final backgroundSprite = Sprite(await images.load('layers/background.png')); @@ -58,11 +59,9 @@ class LayerGame extends Game { backgroundLayer = BackgroundLayer(backgroundSprite); } - @override - void update(double dt) {} - @override void render(Canvas canvas) { + super.render(canvas); gameLayer.render(canvas); backgroundLayer.render(canvas); } diff --git a/examples/lib/stories/rendering/text.dart b/examples/lib/stories/rendering/text.dart index e851a09cd..a3e74d534 100644 --- a/examples/lib/stories/rendering/text.dart +++ b/examples/lib/stories/rendering/text.dart @@ -41,9 +41,10 @@ class MyTextBox extends TextBoxComponent { } } -class TextGame extends BaseGame { +class TextGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); add( TextComponent('Hello, Flame', textRenderer: _regular) ..anchor = Anchor.topCenter diff --git a/examples/lib/stories/sprites/base64.dart b/examples/lib/stories/sprites/base64.dart index bc8483a04..5463a8d86 100644 --- a/examples/lib/stories/sprites/base64.dart +++ b/examples/lib/stories/sprites/base64.dart @@ -1,9 +1,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -class Base64SpriteGame extends BaseGame { +class Base64SpriteGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); const exampleUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAxElEQVQ4jYWTMQ7DIAxFIeoNuAGK1K1ISL0DMwOHzNC5p6iUPeoNOEM7GZnPJ/EUbP7Lx7KtIfH91B/L++gs5m5M9NreTN/dEZiVghatwbXvY68UlksyPjprRaxFGAJZg+uAuSSzzC7rEDirDYAz2wg0RjWRFa/EUwdnQnQ37QFe1Odjrw04AKTTaBXPAlx8dDaXdNk4rMsc0B7ge/UcYLTZxoFizxCQ/L0DMAhaX4Mzj/uzW6phu3AvtHUUU4BAWJ6t8x9N/HHcruXjwQAAAABJRU5ErkJggg=='; final image = await images.fromBase64('shield.png', exampleUrl); diff --git a/examples/lib/stories/sprites/basic.dart b/examples/lib/stories/sprites/basic.dart index 92a539f94..ad5d4326b 100644 --- a/examples/lib/stories/sprites/basic.dart +++ b/examples/lib/stories/sprites/basic.dart @@ -1,9 +1,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -class BasicSpriteGame extends BaseGame { +class BasicSpriteGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final sprite = await loadSprite('flame.png'); add( SpriteComponent( diff --git a/examples/lib/stories/sprites/sprite_group.dart b/examples/lib/stories/sprites/sprite_group.dart index 28d550b58..64b5c1f0f 100644 --- a/examples/lib/stories/sprites/sprite_group.dart +++ b/examples/lib/stories/sprites/sprite_group.dart @@ -6,7 +6,8 @@ enum ButtonState { unpressed, pressed } class ButtonComponent extends SpriteGroupComponent with HasGameRef, Tappable { @override - Future? onLoad() async { + Future onLoad() async { + await super.onLoad(); final pressedSprite = await gameRef.loadSprite( 'buttons.png', srcPosition: Vector2(0, 20), @@ -44,9 +45,10 @@ class ButtonComponent extends SpriteGroupComponent } } -class SpriteGroupExample extends BaseGame with HasTappableComponents { +class SpriteGroupExample extends FlameGame with HasTappableComponents { @override Future onLoad() async { + await super.onLoad(); add( ButtonComponent() ..position = Vector2(100, 100) diff --git a/examples/lib/stories/sprites/spritebatch.dart b/examples/lib/stories/sprites/spritebatch.dart index ef493db6d..20c05540f 100644 --- a/examples/lib/stories/sprites/spritebatch.dart +++ b/examples/lib/stories/sprites/spritebatch.dart @@ -5,9 +5,10 @@ import 'package:flame/game.dart'; import 'package:flame/sprite.dart'; import 'package:flutter/material.dart'; -class SpritebatchGame extends BaseGame { +class SpritebatchGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final spriteBatch = await SpriteBatch.load('boom.png'); spriteBatch.add( diff --git a/examples/lib/stories/sprites/spritebatch_auto_load.dart b/examples/lib/stories/sprites/spritebatch_auto_load.dart index 9e8e8f37f..eaea18f73 100644 --- a/examples/lib/stories/sprites/spritebatch_auto_load.dart +++ b/examples/lib/stories/sprites/spritebatch_auto_load.dart @@ -9,6 +9,7 @@ class MySpriteBatchComponent extends SpriteBatchComponent with HasGameRef { @override Future onLoad() async { + await super.onLoad(); final spriteBatch = await gameRef.loadSpriteBatch('boom.png'); this.spriteBatch = spriteBatch; @@ -42,9 +43,10 @@ class MySpriteBatchComponent extends SpriteBatchComponent } } -class SpritebatchAutoLoadGame extends BaseGame { +class SpritebatchAutoLoadGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); add(MySpriteBatchComponent()); } } diff --git a/examples/lib/stories/sprites/spritesheet.dart b/examples/lib/stories/sprites/spritesheet.dart index 6cf8baa26..029c7b60e 100644 --- a/examples/lib/stories/sprites/spritesheet.dart +++ b/examples/lib/stories/sprites/spritesheet.dart @@ -2,9 +2,10 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/sprite.dart'; -class SpritesheetGame extends BaseGame { +class SpritesheetGame extends FlameGame { @override Future onLoad() async { + await super.onLoad(); final spriteSheet = SpriteSheet( image: await images.load('spritesheet.png'), srcSize: Vector2(16.0, 18.0), diff --git a/examples/lib/stories/tile_maps/isometric_tile_map.dart b/examples/lib/stories/tile_maps/isometric_tile_map.dart index 7fe1c5990..9a2d27b79 100644 --- a/examples/lib/stories/tile_maps/isometric_tile_map.dart +++ b/examples/lib/stories/tile_maps/isometric_tile_map.dart @@ -41,7 +41,7 @@ class Selector extends SpriteComponent { } } -class IsometricTileMapGame extends BaseGame with MouseMovementDetector { +class IsometricTileMapGame extends FlameGame with MouseMovementDetector { late IsometricTileMapComponent base; late Selector selector; @@ -49,6 +49,7 @@ class IsometricTileMapGame extends BaseGame with MouseMovementDetector { @override Future onLoad() async { + await super.onLoad(); final tilesetImage = await images.load('tile_maps/tiles$suffix.png'); final tileset = SpriteSheet( image: tilesetImage, diff --git a/examples/lib/stories/utils/nine_tile_box.dart b/examples/lib/stories/utils/nine_tile_box.dart index 9ad0f1277..4e83aa864 100644 --- a/examples/lib/stories/utils/nine_tile_box.dart +++ b/examples/lib/stories/utils/nine_tile_box.dart @@ -2,23 +2,22 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; -class NineTileBoxGame extends Game { +class NineTileBoxGame extends FlameGame { late NineTileBox nineTileBox; @override Future onLoad() async { + await super.onLoad(); final sprite = Sprite(await images.load('nine-box.png')); nineTileBox = NineTileBox(sprite, tileSize: 8, destTileSize: 24); } @override void render(Canvas canvas) { + super.render(canvas); const length = 300.0; final boxSize = Vector2.all(length); final position = (size - boxSize) / 2; nineTileBox.draw(canvas, position, boxSize); } - - @override - void update(double t) {} } diff --git a/examples/lib/stories/utils/particles.dart b/examples/lib/stories/utils/particles.dart index 004189407..3c3abfbce 100644 --- a/examples/lib/stories/utils/particles.dart +++ b/examples/lib/stories/utils/particles.dart @@ -9,7 +9,7 @@ import 'package:flame/sprite.dart'; import 'package:flame/timer.dart' as flame_timer; import 'package:flutter/material.dart' hide Image; -class ParticlesGame extends BaseGame with FPSCounter { +class ParticlesGame extends FlameGame with FPSCounter { /// Defines dimensions of the sample /// grid to be displayed on the screen, /// 5x5 in this particular case @@ -19,6 +19,7 @@ class ParticlesGame extends BaseGame with FPSCounter { /// Miscellaneous values used /// by examples below final Random rnd = Random(); + Timer? spawnTimer; final StepTween steppedTween = StepTween(begin: 0, end: 5); final trafficLight = TrafficLightComponent(); final TextPaint fpsTextPaint = TextPaint( @@ -35,11 +36,24 @@ class ParticlesGame extends BaseGame with FPSCounter { @override Future onLoad() async { + await super.onLoad(); await images.load('zap.png'); await images.load('boom.png'); + } + @override + void onMount() { + spawnParticles(); // Spawn new particles every second - Timer.periodic(sceneDuration, (_) => spawnParticles()); + spawnTimer = Timer.periodic(sceneDuration, (_) { + spawnParticles(); + }); + } + + @override + void onRemove() { + super.onRemove(); + spawnTimer?.cancel(); } /// Showcases various different uses of [Particle] @@ -81,12 +95,14 @@ class ParticlesGame extends BaseGame with FPSCounter { add( // Bind all the particles to a [Component] update - // lifecycle from the [BaseGame]. - TranslatedParticle( - lifespan: 1, - offset: cellCenter, - child: particle, - ).asComponent(), + // lifecycle from the [FlameGame]. + ParticleComponent( + TranslatedParticle( + lifespan: 1, + offset: cellCenter, + child: particle, + ), + ), ); } while (particles.isNotEmpty); } @@ -512,7 +528,7 @@ class ParticlesGame extends BaseGame with FPSCounter { } } -Future loadGame() async { +Future loadGame() async { WidgetsFlutterBinding.ensureInitialized(); return ParticlesGame(); @@ -538,7 +554,8 @@ class TrafficLightComponent extends Component { Colors.red, ]; - TrafficLightComponent() { + @override + void onMount() { colorChangeTimer.start(); } diff --git a/examples/lib/stories/utils/timer.dart b/examples/lib/stories/utils/timer.dart index 45fc14b4b..c348a69ef 100644 --- a/examples/lib/stories/utils/timer.dart +++ b/examples/lib/stories/utils/timer.dart @@ -3,7 +3,7 @@ import 'package:flame/input.dart'; import 'package:flame/timer.dart'; import 'package:flutter/material.dart'; -class TimerGame extends Game with TapDetector { +class TimerGame extends FlameGame with TapDetector { final TextPaint textConfig = TextPaint( config: const TextPaintConfig( color: Color(0xFFFFFFFF), @@ -16,6 +16,7 @@ class TimerGame extends Game with TapDetector { @override Future onLoad() async { + await super.onLoad(); countdown = Timer(2); interval = Timer( 1, @@ -32,12 +33,14 @@ class TimerGame extends Game with TapDetector { @override void update(double dt) { + super.update(dt); countdown.update(dt); interval.update(dt); } @override void render(Canvas canvas) { + super.render(canvas); textConfig.render( canvas, 'Countdown: ${countdown.current}', diff --git a/examples/lib/stories/utils/timer_component.dart b/examples/lib/stories/utils/timer_component.dart index 2dc9e19d5..3ea424d54 100644 --- a/examples/lib/stories/utils/timer_component.dart +++ b/examples/lib/stories/utils/timer_component.dart @@ -22,7 +22,7 @@ class RenderedTimeComponent extends TimerComponent { } } -class TimerComponentGame extends BaseGame with TapDetector, DoubleTapDetector { +class TimerComponentGame extends FlameGame with TapDetector, DoubleTapDetector { @override void onTapDown(_) { add(RenderedTimeComponent(Timer(1)..start())); diff --git a/examples/lib/stories/widgets/overlay.dart b/examples/lib/stories/widgets/overlay.dart index 69ba41a99..558623975 100644 --- a/examples/lib/stories/widgets/overlay.dart +++ b/examples/lib/stories/widgets/overlay.dart @@ -57,22 +57,14 @@ class _OverlayExampleWidgetState extends State { void newGame() { setState(() { _myGame = ExampleGame(); - print('New game created'); }); } } -class ExampleGame extends Game with TapDetector { - @override - void update(double dt) {} - - @override - Future onLoad() async { - print('game loaded'); - } - +class ExampleGame extends FlameGame with TapDetector { @override void render(Canvas canvas) { + super.render(canvas); canvas.drawRect( const Rect.fromLTWH(100, 100, 100, 100), Paint()..color = BasicPalette.white.color, diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index 15369e2eb..fab214a1f 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flame: path: ../packages/flame - dashbook: 0.1.2 + dashbook: 0.1.4 flutter: sdk: flutter diff --git a/packages/flame/.min_coverage b/packages/flame/.min_coverage index 621537af0..a47f0931c 100644 --- a/packages/flame/.min_coverage +++ b/packages/flame/.min_coverage @@ -1 +1 @@ -57.0 +59.7 diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index 5b40f5fc3..ef9ebb8d7 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -51,6 +51,11 @@ - `TextBoxComponent` can have customizable `pixelRatio` - Add `ContainsAtLeastMockCanvas` to facilitate testing with `MockCanvas` - Support for `drawImage` for `MockCanvas` + - `Game` is now a `Component` + - `ComponentEffect` is now a `Component` + - `HasGameRef` can now operate independently from `Game` + - `initialDelay` and `peakDelay` for effects to handle time before and after an effect + - `component.onMount` now runs every time a component gets a new parent ## [1.0.0-releasecandidate.13] - Fix camera not ending up in the correct position on long jumps diff --git a/packages/flame/example/lib/main.dart b/packages/flame/example/lib/main.dart index 4bb2f4a93..c558fab93 100644 --- a/packages/flame/example/lib/main.dart +++ b/packages/flame/example/lib/main.dart @@ -41,18 +41,19 @@ class Square extends PositionComponent { } @override - void onMount() { - super.onMount(); + Future onLoad() async { + super.onLoad(); size.setValues(squareSize, squareSize); anchor = Anchor.center; } } -class MyGame extends BaseGame with DoubleTapDetector, TapDetector { +class MyGame extends FlameGame with DoubleTapDetector, TapDetector { bool running = true; @override Future onLoad() async { + await super.onLoad(); add( Square() ..x = 100 @@ -62,24 +63,18 @@ class MyGame extends BaseGame with DoubleTapDetector, TapDetector { @override void onTapUp(TapUpInfo info) { - final touchArea = RectExtension.fromVector2Center( - center: info.eventPosition.game, - width: 20, - height: 20, - ); + final touchPoint = info.eventPosition.game; - final handled = components.any((c) { - if (c is PositionComponent && c.toRect().overlaps(touchArea)) { - components.remove(c); + final handled = children.any((c) { + if (c is PositionComponent && c.containsPoint(touchPoint)) { + remove(c); return true; } return false; }); if (!handled) { - add(Square() - ..x = touchArea.left - ..y = touchArea.top); + add(Square()..position = touchPoint); } } diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index e0c93b0ed..d95dd4bd0 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -1,5 +1,4 @@ export 'src/anchor.dart'; -export 'src/components/base_component.dart'; export 'src/components/component.dart'; export 'src/components/component_set.dart'; export 'src/components/input/joystick_component.dart'; diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 5db554e66..77511a0a4 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -1,6 +1,5 @@ export 'src/effects/combined_effect.dart'; export 'src/effects/effects.dart'; -export 'src/effects/effects_handler.dart'; export 'src/effects/move_effect.dart'; export 'src/effects/rotate_effect.dart'; export 'src/effects/scale_effect.dart'; diff --git a/packages/flame/lib/game.dart b/packages/flame/lib/game.dart index a5dfa9ecb..7c9e9244c 100644 --- a/packages/flame/lib/game.dart +++ b/packages/flame/lib/game.dart @@ -1,10 +1,11 @@ export 'src/extensions/vector2.dart'; -export 'src/fps_counter.dart'; -export 'src/game/base_game.dart'; export 'src/game/camera/camera.dart'; export 'src/game/camera/viewport.dart'; -export 'src/game/game.dart'; +export 'src/game/flame_game.dart'; export 'src/game/game_widget/game_widget.dart'; +export 'src/game/mixins/fps_counter.dart'; +export 'src/game/mixins/game.dart'; +export 'src/game/mixins/loadable.dart'; export 'src/game/projector.dart'; export 'src/game/transform2d.dart'; export 'src/text.dart'; diff --git a/packages/flame/lib/src/components/base_component.dart b/packages/flame/lib/src/components/base_component.dart deleted file mode 100644 index b927e81ad..000000000 --- a/packages/flame/lib/src/components/base_component.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'dart:ui'; - -import 'package:meta/meta.dart'; - -import '../../game.dart'; -import '../../input.dart'; -import '../effects/effects.dart'; -import '../effects/effects_handler.dart'; -import '../extensions/vector2.dart'; -import '../text.dart'; -import 'component.dart'; -import 'component_set.dart'; -import 'mixins/has_game_ref.dart'; - -/// This can be extended to represent a basic Component for your game. -/// -/// The difference between this and [Component] is that the [BaseComponent] can -/// have children, handle effects and can be used to see whether a position on -/// the screen is on your component, which is useful for handling gestures. -abstract class BaseComponent extends Component { - final EffectsHandler _effectsHandler = EffectsHandler(); - - late final ComponentSet children = createComponentSet(); - - /// If the component has a parent it will be set here - BaseComponent? _parent; - - @override - BaseComponent? get parent => _parent; - - /// This is set by the BaseGame to tell this component to render additional - /// debug information, like borders, coordinates, etc. - /// This is very helpful while debugging. Set your BaseGame debugMode to true. - /// You can also manually override this for certain components in order to - /// identify issues. - bool debugMode = false; - - /// How many decimal digits to print when displaying coordinates in the - /// debug mode. Setting this to null will suppress all coordinates from - /// the output. - int? get debugCoordinatesPrecision => 0; - - Color debugColor = const Color(0xFFFF00FF); - - Paint get debugPaint => Paint() - ..color = debugColor - ..strokeWidth = 1 - ..style = PaintingStyle.stroke; - - TextPaint get debugTextPaint => TextPaint( - config: TextPaintConfig( - color: debugColor, - fontSize: 12, - ), - ); - - BaseComponent({int? priority}) : super(priority: priority); - - /// This method is called periodically by the game engine to request that your - /// component updates itself. - /// - /// The time [dt] in seconds (with microseconds precision provided by Flutter) - /// since the last update cycle. - /// This time can vary according to hardware capacity, so make sure to update - /// your state considering this. - /// All components on [BaseGame] are always updated by the same amount. The - /// time each one takes to update adds up to the next update cycle. - @mustCallSuper - @override - void update(double dt) { - children.updateComponentList(); - _effectsHandler.update(dt); - children.forEach((c) => c.update(dt)); - } - - @mustCallSuper - @override - void render(Canvas canvas) { - preRender(canvas); - } - - @mustCallSuper - @override - void renderTree(Canvas canvas) { - render(canvas); - postRender(canvas); - children.forEach((c) { - canvas.save(); - c.renderTree(canvas); - canvas.restore(); - }); - - // Any debug rendering should be rendered on top of everything - if (debugMode) { - renderDebugMode(canvas); - } - } - - /// A render cycle callback that runs before the component and its children - /// has been rendered. - @protected - void preRender(Canvas canvas) {} - - /// A render cycle callback that runs after the component has been - /// rendered, but before any children has been rendered. - void postRender(Canvas canvas) {} - - void renderDebugMode(Canvas canvas) {} - - @mustCallSuper - @override - void onGameResize(Vector2 gameSize) { - super.onGameResize(gameSize); - children.forEach((child) => child.onGameResize(gameSize)); - } - - @mustCallSuper - @override - void onMount() { - super.onMount(); - children.forEach((child) => child.onMount()); - } - - @mustCallSuper - @override - void onRemove() { - super.onRemove(); - children.forEach((child) => child.onRemove()); - } - - /// Called to check whether the point is to be counted as within the component - /// It needs to be overridden to have any effect, like it is in - /// PositionComponent. - bool containsPoint(Vector2 point) => false; - - /// Add an effect to the component - void addEffect(ComponentEffect effect) { - _effectsHandler.add(effect, this); - } - - /// Mark an effect for removal on the component - void removeEffect(ComponentEffect effect) { - _effectsHandler.removeEffect(effect); - } - - /// Remove all effects - void clearEffects() { - _effectsHandler.clearEffects(); - } - - /// Get a list of non removed effects - List get effects => _effectsHandler.effects; - - void prepare(Component child, {Game? gameRef}) { - if (this is HasGameRef) { - final c = this as HasGameRef; - gameRef ??= c.hasGameRef ? c.gameRef : null; - } else if (gameRef == null) { - assert( - !isMounted, - 'Parent was already added to Game and has no HasGameRef; in this case, gameRef is mandatory.', - ); - } - if (gameRef is BaseGame) { - gameRef.prepare(child); - } - - if (child is BaseComponent) { - child._parent = this; - child.debugMode = debugMode; - } - } - - /// Uses the game passed in, or uses the game from [HasGameRef] otherwise, - /// to prepare the child component before it is added to the list of children. - /// Note that this component needs to be added to the game first if - /// [this.gameRef] should be used to prepare the child. - /// For children that don't need preparation from the game instance can - /// disregard both the options given above. - Future addChild(Component child, {BaseGame? gameRef}) { - return children.addChild(child, gameRef: gameRef); - } - - /// Adds mutiple children. - /// - /// See [addChild] for details (or `children.addChildren()`). - Future addChildren(List cs, {BaseGame? gameRef}) { - return children.addChildren(cs, gameRef: gameRef); - } - - /// Removes a component from the component list, calling onRemove for it and - /// its children. - void removeChild(Component c) { - children.remove(c); - } - - /// Removes all the children in the list and calls onRemove for all of them - /// and their children. - void removeChildren(Iterable cs) { - children.removeAll(cs); - } - - /// Whether the children list contains the given component. - /// - /// This method uses reference equality. - bool containsChild(Component c) => children.contains(c); - - /// Call this if any of this component's children priorities have changed - /// at runtime. - /// - /// This will call `rebalanceAll` on the [children] ordered set. - void reorderChildren() => children.rebalanceAll(); - - /// This method first calls the passed handler on the leaves in the tree, - /// the children without any children of their own. - /// Then it continues through all other children. The propagation continues - /// until the handler returns false, which means "do not continue", or when - /// the handler has been called with all children - /// - /// This method is important to be used by the engine to propagate actions - /// like rendering, taps, etc, but you can call it yourself if you need to - /// apply an action to the whole component chain. - /// It will only consider components of type T in the hierarchy, - /// so use T = Component to target everything. - bool propagateToChildren( - bool Function(T) handler, - ) { - var shouldContinue = true; - for (final child in children) { - if (child is BaseComponent) { - shouldContinue = child.propagateToChildren(handler); - } - if (shouldContinue && child is T) { - shouldContinue = handler(child); - } - if (!shouldContinue) { - break; - } - } - return shouldContinue; - } - - @protected - Vector2 eventPosition(PositionInfo info) { - return isHud ? info.eventPosition.widget : info.eventPosition.game; - } - - ComponentSet createComponentSet() { - final components = ComponentSet.createDefault(prepare); - if (this is HasGameRef) { - components.register(); - } - return components; - } -} diff --git a/packages/flame/lib/src/components/component.dart b/packages/flame/lib/src/components/component.dart index 8366b4271..f82aafcb8 100644 --- a/packages/flame/lib/src/components/component.dart +++ b/packages/flame/lib/src/components/component.dart @@ -3,100 +3,330 @@ import 'dart:ui'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; +import '../../components.dart'; +import '../../game.dart'; +import '../../input.dart'; import '../extensions/vector2.dart'; +import '../game/mixins/loadable.dart'; +import 'cache/value_cache.dart'; /// This represents a Component for your game. /// -/// Components can be bullets flying on the screen, a spaceship or your player's fighter. -/// Anything that either renders or updates can be added to the list on BaseGame. It will deal with calling those methods for you. -/// Components also have other methods that can help you out if you want to override them. -abstract class Component { - /// Whether this component is HUD object or not. +/// Components can be for example bullets flying on the screen, a spaceship, a +/// timer or an enemy. Anything that either needs to be rendered and/or updated +/// is a good idea to have as a [Component], since [update] and [render] will be +/// called automatically once the component is added to the component tree in +/// your game (with `game.add`). +class Component with Loadable { + /// Whether this component is a HUD (Heads-up display) object or not. /// - /// HUD objects ignore the BaseGame.camera when rendered (so their position coordinates are considered relative to the device screen). + /// HUD objects ignore the FlameGame.camera when rendered (so their position + /// coordinates are considered relative to the device screen). bool isHud = false; - bool _isMounted = false; + /// Whether this component has been prepared and is ready to be added to the + /// game loop. + bool isPrepared = false; - /// Whether this component is currently mounted on a game or not - bool get isMounted => _isMounted; + /// Whether this component is done loading through [onLoad]. + bool isLoaded = false; - /// If the component has a parent it will be set here + /// Whether this component is currently added to a component tree. + bool isMounted = false; + + /// If the component has a parent it will be set here. Component? _parent; + /// Get the current parent of the component, if there is one, otherwise null. Component? get parent => _parent; - /// Render priority of this component. This allows you to control the order in which your components are rendered. + /// If the component should be added to another parent once it has been + /// removed from its current parent. + Component? nextParent; + + late final ComponentSet children = createComponentSet(); + + /// Render priority of this component. This allows you to control the order in + /// which your components are rendered. /// - /// Components are always updated and rendered in the order defined by what this number is when the component is added to the game. - /// The smaller the priority, the sooner your component will be updated/rendered. + /// Components are always updated and rendered in the order defined by what + /// this number is when the component is added to the game. + /// The smaller the priority, the sooner your component will be + /// updated/rendered. /// It can be any integer (negative, zero, or positive). - /// If two components share the same priority, they will probably be drawn in the order they were added. + /// If two components share the same priority, they will probably be drawn in + /// the order they were added. int get priority => _priority; int _priority; /// Whether this component should be removed or not. /// - /// It will be checked once per component per tick, and if it is true, BaseGame will remove it. + /// It will be checked once per component per tick, and if it is true, + /// FlameGame will remove it. bool shouldRemove = false; + /// Returns whether this [Component] is in debug mode or not. + /// When a child is added to the [Component] it gets the same [debugMode] as + /// its parent has when it is prepared. + /// + /// Returns `false` by default. Override it, or set it to true, to use debug + /// mode. + /// You can use this value to enable debug behaviors for your game and many + /// components will + /// show extra information on the screen when debug mode is activated. + bool debugMode = false; + + /// How many decimal digits to print when displaying coordinates in the + /// debug mode. Setting this to null will suppress all coordinates from + /// the output. + int? get debugCoordinatesPrecision => 0; + + /// The color that the debug output should be rendered with. + Color debugColor = const Color(0xFFFF00FF); + + final ValueCache _debugPaintCache = ValueCache(); + final ValueCache _debugTextPaintCache = ValueCache(); + + /// The [debugColor] represented as a [Paint] object. + Paint get debugPaint { + if (!_debugPaintCache.isCacheValid([debugColor])) { + final paint = Paint() + ..color = debugColor + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + _debugPaintCache.updateCache(paint, [debugColor]); + } + return _debugPaintCache.value!; + } + + /// Returns a [TextPaint] object with the [debugColor] set as color for the + /// text. + TextPaint get debugTextPaint { + if (!_debugTextPaintCache.isCacheValid([debugColor])) { + final textPaint = TextPaint( + config: TextPaintConfig( + color: debugColor, + fontSize: 12, + ), + ); + _debugTextPaintCache.updateCache(textPaint, [debugColor]); + } + return _debugTextPaintCache.value!; + } + Component({int? priority}) : _priority = priority ?? 0; - /// This method is called periodically by the game engine to request that your component updates itself. + /// This method is called periodically by the game engine to request that your + /// component updates itself. /// - /// The time [dt] in seconds (with microseconds precision provided by Flutter) since the last update cycle. - /// This time can vary according to hardware capacity, so make sure to update your state considering this. - /// All components on BaseGame are always updated by the same amount. The time each one takes to update adds up to the next update cycle. - void update(double dt) {} + /// The time [dt] in seconds (with microseconds precision provided by Flutter) + /// since the last update cycle. + /// This time can vary according to hardware capacity, so make sure to update + /// your state considering this. + /// All components in the tree are always updated by the same amount. The time + /// each one takes to update adds up to the next update cycle. + @mustCallSuper + void update(double dt) { + children.updateComponentList(); + children.forEach((c) => c.update(dt)); + } - /// Renders this component on the provided Canvas [c]. - void render(Canvas c) {} + void render(Canvas canvas) { + preRender(canvas); + } - /// This is used to render this component and potential children on subclasses - /// of [Component] on the provided Canvas [c]. - void renderTree(Canvas c) => render(c); + @mustCallSuper + void renderTree(Canvas canvas) { + render(canvas); + postRender(canvas); + children.forEach((c) { + canvas.save(); + c.renderTree(canvas); + canvas.restore(); + }); + + // Any debug rendering should be rendered on top of everything + if (debugMode) { + renderDebugMode(canvas); + } + } + + /// A render cycle callback that runs before the component and its children + /// has been rendered. + @protected + void preRender(Canvas canvas) {} + + /// A render cycle callback that runs after the component has been + /// rendered, but before any children has been rendered. + void postRender(Canvas canvas) {} + + void renderDebugMode(Canvas canvas) {} + + @protected + Vector2 eventPosition(PositionInfo info) { + return isHud ? info.eventPosition.widget : info.eventPosition.game; + } + + /// Remove the component from its parent in the next tick. + void removeFromParent() => shouldRemove = true; + + /// Changes the current parent for another parent and prepares the tree under + /// the new root. + void changeParent(Component component) { + parent?.remove(this); + nextParent = component; + } /// It receives the new game size. - /// Executed right after the component is attached to a game and right before [onMount] is called - void onGameResize(Vector2 gameSize) {} - - /// Remove the component from the game it is added to in the next tick - void remove() => shouldRemove = true; - - /// Called when the component has been added and prepared by the game instance. - /// - /// This can be used to make initializations on your component as, when this method is called, - /// things like [onGameResize] are already set and usable. + /// Executed right after the component is attached to a game and right before + /// [onLoad] is called. + @override @mustCallSuper - void onMount() { - _isMounted = true; + void onGameResize(Vector2 gameSize) { + super.onGameResize(gameSize); + children.forEach((child) => child.onGameResize(gameSize)); } /// Called right before the component is removed from the game + @override @mustCallSuper void onRemove() { - _isMounted = false; + children.forEach((child) { + child.onRemove(); + }); + isPrepared = false; + isMounted = false; + _parent = null; + nextParent?.add(this); + nextParent = null; } - /// Called before the component is added to the BaseGame by the add method. - /// Whenever this returns something, BaseGame will wait for the [Future] to be resolved before adding the component on the list. - /// If `null` is returned, the component is added right away. + /// Prepares and registers a component to be added on the next game tick /// - /// Has a default implementation which just returns null. + /// This method is an async operation since it await the [onLoad] method of + /// the component. Nevertheless, this method only need to be waited to finish + /// if by some reason, your logic needs to be sure that the component has + /// finished loading, otherwise, this method can be called without waiting + /// for it to finish as the FlameGame already handle the loading of the + /// component. /// - /// This can be overwritten this to add custom logic to the component loading. + /// *Note:* Do not add components on the game constructor. This method can + /// only be called after the game already has its layout set, this can be + /// verified by the [Game.hasLayout] property, to add components upon game + /// initialization, the [onLoad] method can be used instead. + Future add(Component component) { + return children.addChild(component); + } + + /// Adds multiple children. /// - /// Example: - /// ```dart - /// @override - /// Future onLoad() async { - /// myImage = await gameRef.load('my_image.png'); - /// } - /// ``` - Future? onLoad() => null; + /// See [add] for details. + Future addAll(List components) { + return children.addChildren(components); + } + + /// The children are added again to the component set so that [prepare], + /// [onLoad] and [onMount] runs again. Used when a parent is changed + /// further up the tree. + Future reAddChildren() async { + await Future.wait(children.map(add)); + await Future.wait(children.addLater.map(add)); + } + + /// Removes a component from the component tree, calling [onRemove] for it and + /// its children. + void remove(Component c) { + children.remove(c); + } + + /// Removes all the children in the list and calls [onRemove] for all of them + /// and their children. + void removeAll(Iterable cs) { + children.removeAll(cs); + } + + /// Whether the children list contains the given component. + /// + /// This method uses reference equality. + bool contains(Component c) => children.contains(c); + + /// Call this if any of this component's children priorities have changed + /// at runtime. + /// + /// This will call [ComponentSet.rebalanceAll] on the [children] ordered set. + void reorderChildren() => children.rebalanceAll(); + + /// This method first calls the passed handler on the leaves in the tree, + /// the children without any children of their own. + /// Then it continues through all other children. The propagation continues + /// until the handler returns false, which means "do not continue", or when + /// the handler has been called with all children. + /// + /// This method is important to be used by the engine to propagate actions + /// like rendering, taps, etc, but you can call it yourself if you need to + /// apply an action to the whole component chain. + /// It will only consider components of type T in the hierarchy, + /// so use T = Component to target everything. + bool propagateToChildren( + bool Function(T) handler, + ) { + var shouldContinue = true; + for (final child in children) { + if (child is Component) { + shouldContinue = child.propagateToChildren(handler); + } + if (shouldContinue && child is T) { + shouldContinue = handler(child); + } else if (shouldContinue && child is FlameGame) { + shouldContinue = child.propagateToChildren(handler); + } + if (!shouldContinue) { + break; + } + } + return shouldContinue; + } + + /// Finds the closest parent further up the hierarchy that satisfies type=T, + /// or null if none is found. + T? findParent() { + return (parent is T ? parent : parent?.findParent()) as T?; + } + + /// Called to check whether the point is to be counted as within the component + /// It needs to be overridden to have any effect, like it is in + /// PositionComponent. + bool containsPoint(Vector2 point) => false; /// Usually this is not something that the user would want to call since the /// component list isn't re-ordered when it is called. - /// See BaseGame.changePriority instead. + /// See FlameGame.changePriority instead. void changePriorityWithoutResorting(int priority) => _priority = priority; + + /// Prepares the [Component] to be added to a [parent], and if there is an + /// ancestor that is a [FlameGame] that game will do necessary preparations for + /// this component. + /// If there are no parents that are a [Game] false will be returned and this + /// will run again once an ancestor or the component itself is added to a + /// [Game]. + @mustCallSuper + void prepare(Component parent) { + _parent = parent; + final parentGame = findParent(); + if (parentGame == null) { + isPrepared = false; + } else { + if (parentGame is FlameGame) { + parentGame.prepareComponent(this); + } + + debugMode |= parent.debugMode; + isPrepared = true; + } + } + + @mustCallSuper + ComponentSet createComponentSet() { + return ComponentSet.createDefault(this); + } } diff --git a/packages/flame/lib/src/components/component_set.dart b/packages/flame/lib/src/components/component_set.dart index d6f55243c..f9cfd6f4e 100644 --- a/packages/flame/lib/src/components/component_set.dart +++ b/packages/flame/lib/src/components/component_set.dart @@ -1,11 +1,13 @@ +import 'dart:collection'; + import 'package:ordered_set/comparing.dart'; import 'package:ordered_set/queryable_ordered_set.dart'; import '../../components.dart'; -import '../game/base_game.dart'; +import '../../game.dart'; /// This is a simple wrapper over [QueryableOrderedSet] to be used by -/// [BaseGame] and [BaseComponent]. +/// [Component]. /// /// Instead of immediately modifying the component list, all insertion /// and removal operations are queued to be performed on the next tick. @@ -13,14 +15,14 @@ import '../game/base_game.dart'; /// This will avoid any concurrent modification exceptions while the game /// iterates through the component list. /// -/// This wrapper also guaranteed that prepare, onLoad, onMount and all the -/// lifecycle methods are called properly. +/// This wrapper also guaranteed that [Component.prepare], [Loadable.onLoad] +/// and all the lifecycle methods are called properly. class ComponentSet extends QueryableOrderedSet { /// Components to be added on the next update. /// /// The component list is only changed at the start of each update to avoid /// concurrency issues. - final List _addLater = []; + final Set _addLater = {}; /// Components to be removed on the next update. /// @@ -30,17 +32,18 @@ class ComponentSet extends QueryableOrderedSet { /// This is the "prepare" function that will be called *before* the /// component is added to the component list by the add/addAll methods. - final void Function(Component child, {BaseGame? gameRef}) prepare; + /// It is also called when the component changes parent. + final Component parent; ComponentSet( int Function(Component e1, Component e2)? compare, - this.prepare, + this.parent, ) : super(compare); /// Prepares and registers one component to be added on the next game tick. /// /// This is the interface compliant version; if you want to provide an - /// explicit gameRef or await for the onLoad, use [addChild]. + /// explicit gameRef or await for the [Loadable.onLoad], use [addChild]. /// /// Note: the component is only added on the next tick. This method always /// returns true. @@ -54,10 +57,10 @@ class ComponentSet extends QueryableOrderedSet { /// tick. /// /// This is the interface compliant version; if you want to provide an - /// explicit gameRef or await for the onLoad, use [addChild]. + /// explicit gameRef or await for the [Loadable.onLoad], use [addChild]. /// /// Note: the components are only added on the next tick. This method always - /// returns the total lenght of the provided list. + /// returns the total length of the provided list. @override int addAll(Iterable components) { addChildren(components); @@ -67,30 +70,45 @@ class ComponentSet extends QueryableOrderedSet { /// Prepares and registers one component to be added on the next game tick. /// /// This allows you to provide a specific gameRef if this component is being - /// added from within another component that is already on a BaseGame. + /// added from within another component that is already on a FlameGame. /// You can await for the onLoad function, if present. /// This method can be considered sync for all intents and purposes if no /// onLoad is provided by the component. - Future addChild(Component c, {BaseGame? gameRef}) async { - prepare(c, gameRef: gameRef); - - final loadFuture = c.onLoad(); - if (loadFuture != null) { - await loadFuture; + Future addChild(Component component) async { + component.prepare(parent); + if (!component.isPrepared) { + // Since the components won't be added until a proper game is added + // further up in the tree we can add them to the _addLater list and + // then re-add them once there is a proper root. + _addLater.add(component); + return; + } + // [Component.onLoad] (if it is defined) should only run the first time that + // a component is added to a parent. + if (!component.isLoaded) { + final onLoad = component.onLoadCache; + if (onLoad != null) { + await onLoad; + } + component.isLoaded = true; } - _addLater.add(c); + // Should run every time the component gets a new parent, including its + // first parent. + component.onMount(); + if (component.children.isNotEmpty) { + await component.reAddChildren(); + } + + _addLater.add(component); } /// Prepares and registers a list of component to be added on the next game /// tick. /// /// See [addChild] for more details. - Future addChildren( - Iterable components, { - BaseGame? gameRef, - }) async { - final ps = components.map((c) => addChild(c, gameRef: gameRef)); + Future addChildren(Iterable components) async { + final ps = components.map(addChild); await Future.wait(ps); } @@ -120,6 +138,21 @@ class ComponentSet extends QueryableOrderedSet { return toList().reversed; } + /// Whether the component set is empty and that there are no components marked + /// to be added later. + @override + bool get isEmpty => super.isEmpty && _addLater.isEmpty; + + /// Whether the component set contains components or that there are components + /// marked to be added later. + @override + bool get isNotEmpty => !isEmpty; + + /// All the children that has been queued to be added to the component set. + UnmodifiableListView get addLater { + return UnmodifiableListView(_addLater); + } + /// Call this on your update method. /// /// This method effectuates any pending operations of insertion or removal, @@ -130,17 +163,15 @@ class ComponentSet extends QueryableOrderedSet { _removeLater.forEach((c) { c.onRemove(); super.remove(c); + c.shouldRemove = false; }); _removeLater.clear(); - if (_addLater.isNotEmpty) { - final addNow = _addLater.toList(growable: false); - _addLater.clear(); - addNow.forEach((c) { - super.add(c); - c.onMount(); - }); - } + _addLater.forEach((c) { + super.add(c); + c.isMounted = true; + }); + _addLater.clear(); } @override @@ -161,13 +192,14 @@ class ComponentSet extends QueryableOrderedSet { /// Creates a [ComponentSet] with a default value for the compare function, /// using the Component's priority for sorting. /// - /// You must still provide your [prepare] function depending on the context. + /// You must provide the parent so that it can be handed to the children that + /// will be added. static ComponentSet createDefault( - void Function(Component child, {BaseGame? gameRef}) prepare, + Component parent, ) { return ComponentSet( Comparing.on((c) => c.priority), - prepare, + parent, ); } } diff --git a/packages/flame/lib/src/components/input/hud_button_component.dart b/packages/flame/lib/src/components/input/hud_button_component.dart index ae994b636..8980138f5 100644 --- a/packages/flame/lib/src/components/input/hud_button_component.dart +++ b/packages/flame/lib/src/components/input/hud_button_component.dart @@ -33,7 +33,7 @@ class HudButtonComponent extends HudMarginComponent with Tappable { @override Future onLoad() async { await super.onLoad(); - addChild(button); + add(button); } @override @@ -41,7 +41,7 @@ class HudButtonComponent extends HudMarginComponent with Tappable { bool onTapDown(TapDownInfo info) { if (buttonDown != null) { children.remove(button); - addChild(buttonDown!); + add(buttonDown!); } onPressed?.call(); return false; @@ -59,7 +59,7 @@ class HudButtonComponent extends HudMarginComponent with Tappable { bool onTapCancel() { if (buttonDown != null) { children.remove(buttonDown!); - addChild(button); + add(button); } return false; } diff --git a/packages/flame/lib/src/components/input/hud_margin_component.dart b/packages/flame/lib/src/components/input/hud_margin_component.dart index f43df85dd..4260afcba 100644 --- a/packages/flame/lib/src/components/input/hud_margin_component.dart +++ b/packages/flame/lib/src/components/input/hud_margin_component.dart @@ -5,7 +5,7 @@ import '../../../components.dart'; import '../../../extensions.dart'; import '../../../game.dart'; -class HudMarginComponent extends PositionComponent +class HudMarginComponent extends PositionComponent with HasGameRef { @override bool isHud = true; diff --git a/packages/flame/lib/src/components/input/joystick_component.dart b/packages/flame/lib/src/components/input/joystick_component.dart index 8909126c1..f79647048 100644 --- a/packages/flame/lib/src/components/input/joystick_component.dart +++ b/packages/flame/lib/src/components/input/joystick_component.dart @@ -72,9 +72,9 @@ class JoystickComponent extends HudMarginComponent with Draggable { knob.position.add(size / 2); _baseKnobPosition = knob.position.clone(); if (background != null) { - addChild(background!); + add(background!); } - addChild(knob); + add(knob); } @override diff --git a/packages/flame/lib/src/components/mixins/collidable.dart b/packages/flame/lib/src/components/mixins/collidable.dart index 88299e91e..70dfd4d4f 100644 --- a/packages/flame/lib/src/components/mixins/collidable.dart +++ b/packages/flame/lib/src/components/mixins/collidable.dart @@ -21,7 +21,7 @@ mixin Collidable on Hitbox { void onCollisionEnd(Collidable other) {} } -class ScreenCollidable extends PositionComponent +class ScreenCollidable extends PositionComponent with Hitbox, Collidable, HasGameRef { @override CollidableType collidableType = CollidableType.passive; diff --git a/packages/flame/lib/src/components/mixins/draggable.dart b/packages/flame/lib/src/components/mixins/draggable.dart index f864150db..d64b41853 100644 --- a/packages/flame/lib/src/components/mixins/draggable.dart +++ b/packages/flame/lib/src/components/mixins/draggable.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import '../../game/base_game.dart'; +import '../../../components.dart'; +import '../../game/flame_game.dart'; import '../../gestures/events.dart'; -import '../base_component.dart'; -mixin Draggable on BaseComponent { +mixin Draggable on Component { bool _isDragged = false; bool get isDragged => _isDragged; @@ -62,7 +62,7 @@ mixin Draggable on BaseComponent { } } -mixin HasDraggableComponents on BaseGame { +mixin HasDraggableComponents on FlameGame { @mustCallSuper void onDragStart(int pointerId, DragStartInfo info) { _onGenericEventReceived((c) => c.handleDragStart(pointerId, info)); @@ -84,11 +84,8 @@ mixin HasDraggableComponents on BaseGame { } void _onGenericEventReceived(bool Function(Draggable) handler) { - for (final c in components.reversed()) { - var shouldContinue = true; - if (c is BaseComponent) { - shouldContinue = c.propagateToChildren(handler); - } + for (final c in children.reversed()) { + var shouldContinue = c.propagateToChildren(handler); if (c is Draggable && shouldContinue) { shouldContinue = handler(c); } diff --git a/packages/flame/lib/src/components/mixins/has_collidables.dart b/packages/flame/lib/src/components/mixins/has_collidables.dart index 5458b2dea..80f1d20d6 100644 --- a/packages/flame/lib/src/components/mixins/has_collidables.dart +++ b/packages/flame/lib/src/components/mixins/has_collidables.dart @@ -2,8 +2,20 @@ import '../../../game.dart'; import '../../components/mixins/collidable.dart'; import '../../geometry/collision_detection.dart'; -mixin HasCollidables on BaseGame { +mixin HasCollidables on FlameGame { + @override + Future? onLoad() { + children.register(); + return super.onLoad(); + } + + @override + void update(double dt) { + super.update(dt); + handleCollidables(); + } + void handleCollidables() { - collisionDetection(components.query()); + collisionDetection(children.query()); } } diff --git a/packages/flame/lib/src/components/mixins/has_game_ref.dart b/packages/flame/lib/src/components/mixins/has_game_ref.dart index da19d9930..3e5635310 100644 --- a/packages/flame/lib/src/components/mixins/has_game_ref.dart +++ b/packages/flame/lib/src/components/mixins/has_game_ref.dart @@ -1,27 +1,31 @@ import '../../../components.dart'; import '../../../game.dart'; -mixin HasGameRef on Component { +mixin HasGameRef on Component { T? _gameRef; T get gameRef { - final ref = _gameRef; - if (ref == null) { - throw 'Accessing gameRef before the component was added to the game!'; + if (_gameRef == null) { + var c = parent; + while (c != null) { + if (c is HasGameRef) { + _gameRef = c.gameRef; + return _gameRef!; + } else if (c is T) { + _gameRef = c; + return c; + } else { + c = c.parent; + } + } + throw StateError('Cannot find reference $T in the component tree'); } - return ref; + return _gameRef!; } - bool get hasGameRef => _gameRef != null; - - set gameRef(T gameRef) { - _gameRef = gameRef; - if (this is BaseComponent) { - // TODO(luan) this is wrong, should be done using propagateToChildren - (this as BaseComponent) - .children - .query() - .forEach((e) => e.gameRef = gameRef); - } + @override + void onRemove() { + super.onRemove(); + _gameRef = null; } } diff --git a/packages/flame/lib/src/components/mixins/has_paint.dart b/packages/flame/lib/src/components/mixins/has_paint.dart index b1c547195..61a9f61cc 100644 --- a/packages/flame/lib/src/components/mixins/has_paint.dart +++ b/packages/flame/lib/src/components/mixins/has_paint.dart @@ -8,7 +8,7 @@ import '../../palette.dart'; /// Component will always have a main Paint that can be accessed /// by the [paint] attribute and other paints can be manipulated/accessed /// using [getPaint], [setPaint] and [deletePaint] by a paintId of generic type [T], that can be omited if the component only have one paint. -mixin HasPaint on BaseComponent { +mixin HasPaint on Component { final Map _paints = {}; Paint paint = BasicPalette.white.paint(); diff --git a/packages/flame/lib/src/components/mixins/hoverable.dart b/packages/flame/lib/src/components/mixins/hoverable.dart index bde335bd8..6b64efd11 100644 --- a/packages/flame/lib/src/components/mixins/hoverable.dart +++ b/packages/flame/lib/src/components/mixins/hoverable.dart @@ -1,11 +1,11 @@ import 'package:meta/meta.dart'; +import '../../../components.dart'; import '../../../game.dart'; -import '../../game/base_game.dart'; +import '../../game/flame_game.dart'; import '../../gestures/events.dart'; -import '../base_component.dart'; -mixin Hoverable on BaseComponent { +mixin Hoverable on Component { bool _isHovered = false; bool get isHovered => _isHovered; void onHoverEnter(PointerHoverInfo info) {} @@ -27,7 +27,7 @@ mixin Hoverable on BaseComponent { } } -mixin HasHoverableComponents on BaseGame { +mixin HasHoverableComponents on FlameGame { @mustCallSuper void onMouseMove(PointerHoverInfo info) { bool _mouseMoveHandler(Hoverable c) { @@ -35,12 +35,13 @@ mixin HasHoverableComponents on BaseGame { return true; // always continue } - for (final c in components.reversed()) { - if (c is BaseComponent) { - c.propagateToChildren(_mouseMoveHandler); + for (final c in children.reversed()) { + var shouldContinue = c.propagateToChildren(_mouseMoveHandler); + if (c is Hoverable && shouldContinue) { + shouldContinue = _mouseMoveHandler(c); } - if (c is Hoverable) { - _mouseMoveHandler(c); + if (!shouldContinue) { + break; } } } diff --git a/packages/flame/lib/src/components/mixins/tappable.dart b/packages/flame/lib/src/components/mixins/tappable.dart index 4da00f3e9..fcc165bb4 100644 --- a/packages/flame/lib/src/components/mixins/tappable.dart +++ b/packages/flame/lib/src/components/mixins/tappable.dart @@ -1,11 +1,11 @@ import 'package:meta/meta.dart'; +import '../../../components.dart'; import '../../../game.dart'; -import '../../game/base_game.dart'; +import '../../game/flame_game.dart'; import '../../gestures/events.dart'; -import '../base_component.dart'; -mixin Tappable on BaseComponent { +mixin Tappable on Component { bool onTapCancel() { return true; } @@ -47,13 +47,10 @@ mixin Tappable on BaseComponent { } } -mixin HasTappableComponents on BaseGame { +mixin HasTappableComponents on FlameGame { void _handleTapEvent(bool Function(Tappable child) tapEventHandler) { - for (final c in components.reversed()) { - var shouldContinue = true; - if (c is BaseComponent) { - shouldContinue = c.propagateToChildren(tapEventHandler); - } + for (final c in children.reversed()) { + var shouldContinue = c.propagateToChildren(tapEventHandler); if (c is Tappable && shouldContinue) { shouldContinue = tapEventHandler(c); } diff --git a/packages/flame/lib/src/components/parallax_component.dart b/packages/flame/lib/src/components/parallax_component.dart index e53ece441..f1332515d 100644 --- a/packages/flame/lib/src/components/parallax_component.dart +++ b/packages/flame/lib/src/components/parallax_component.dart @@ -8,11 +8,10 @@ import '../../components.dart'; import '../../game.dart'; import '../assets/images.dart'; import '../extensions/vector2.dart'; -import '../game/game.dart'; import '../parallax.dart'; import 'position_component.dart'; -extension ParallaxComponentExtension on Game { +extension ParallaxComponentExtension on FlameGame { Future loadParallaxComponent( List dataList, { Vector2? size, @@ -41,7 +40,7 @@ extension ParallaxComponentExtension on Game { /// A full parallax, several layers of images drawn out on the screen and each /// layer moves with different velocities to give an effect of depth. -class ParallaxComponent extends PositionComponent +class ParallaxComponent extends PositionComponent with HasGameRef { bool isFullscreen = true; Parallax? _parallax; diff --git a/packages/flame/lib/src/components/particle_component.dart b/packages/flame/lib/src/components/particle_component.dart index 4bdd4665d..6bd995d13 100644 --- a/packages/flame/lib/src/components/particle_component.dart +++ b/packages/flame/lib/src/components/particle_component.dart @@ -4,17 +4,15 @@ import '../particles/particle.dart'; import 'component.dart'; /// Base container for [Particle] instances to be attach -/// to a [Component] tree. Could be added either to BaseGame -/// or an implementation of BaseComponent. +/// to a [Component] tree. Could be added either to FlameGame +/// or an implementation of [Component]. /// Proxies [Component] lifecycle hooks to nested [Particle]. class ParticleComponent extends Component { Particle particle; - ParticleComponent({ - required this.particle, - }); + ParticleComponent(this.particle); - /// This [ParticleComponent] will be removed by the BaseGame. + /// This [ParticleComponent] will be removed by the FlameGame. @override bool get shouldRemove => particle.shouldRemove; @@ -27,6 +25,7 @@ class ParticleComponent extends Component { /// [Particle] within this [Component]. @override void render(Canvas canvas) { + super.render(canvas); particle.render(canvas); } diff --git a/packages/flame/lib/src/components/position_component.dart b/packages/flame/lib/src/components/position_component.dart index 5089943e9..20afd80ea 100644 --- a/packages/flame/lib/src/components/position_component.dart +++ b/packages/flame/lib/src/components/position_component.dart @@ -1,12 +1,12 @@ import 'dart:math' as math; import 'dart:ui' hide Offset; + import '../anchor.dart'; import '../extensions/offset.dart'; import '../extensions/rect.dart'; import '../extensions/vector2.dart'; import '../game/notifying_vector2.dart'; import '../game/transform2d.dart'; -import 'base_component.dart'; import 'component.dart'; /// A [Component] implementation that represents an object that can be @@ -58,7 +58,7 @@ import 'component.dart'; /// the approximate bounding rectangle of the rendered picture. If you /// do not specify the size of a PositionComponent, then it will be /// equal to zero and the component won't be able to respond to taps. -class PositionComponent extends BaseComponent { +class PositionComponent extends Component { PositionComponent({ Vector2? position, Vector2? size, diff --git a/packages/flame/lib/src/components/sprite_animation_component.dart b/packages/flame/lib/src/components/sprite_animation_component.dart index 44ed195ef..e263bb2ae 100644 --- a/packages/flame/lib/src/components/sprite_animation_component.dart +++ b/packages/flame/lib/src/components/sprite_animation_component.dart @@ -10,8 +10,15 @@ import 'position_component.dart'; export '../sprite_animation.dart'; class SpriteAnimationComponent extends PositionComponent with HasPaint { + /// The animation used by the component. SpriteAnimation? animation; + + /// If the component should be removed once the animation has finished. + /// Needs the animation to have `loop = false` to ever remove the component, + /// since it will never finish otherwise. bool removeOnFinish = false; + + /// Whether the animation is paused or playing. bool playing; /// Creates a component with an empty animation which can be set later @@ -31,7 +38,7 @@ class SpriteAnimationComponent extends PositionComponent with HasPaint { /// Creates a SpriteAnimationComponent from a [size], an [image] and [data]. Check [SpriteAnimationData] for more info on the available options. /// - /// Optionally [removeOnFinish] can be set to true to have this component be auto removed from the BaseGame when the animation is finished. + /// Optionally [removeOnFinish] can be set to true to have this component be auto removed from the FlameGame when the animation is finished. SpriteAnimationComponent.fromFrameData( Image image, SpriteAnimationData data, { diff --git a/packages/flame/lib/src/components/sprite_animation_group_component.dart b/packages/flame/lib/src/components/sprite_animation_group_component.dart index 0abb88620..42eb810da 100644 --- a/packages/flame/lib/src/components/sprite_animation_group_component.dart +++ b/packages/flame/lib/src/components/sprite_animation_group_component.dart @@ -42,7 +42,7 @@ class SpriteAnimationGroupComponent extends PositionComponent with HasPaint { /// Check [SpriteAnimationData] for more info on the available options. /// /// Optionally [removeOnFinish] can be mapped to true to have this component be auto - /// removed from the BaseGame when the animation is finished. + /// removed from the FlameGame when the animation is finished. SpriteAnimationGroupComponent.fromFrameData( Image image, Map data, { diff --git a/packages/flame/lib/src/components/sprite_batch_component.dart b/packages/flame/lib/src/components/sprite_batch_component.dart index 377a99efc..ffdddb19e 100644 --- a/packages/flame/lib/src/components/sprite_batch_component.dart +++ b/packages/flame/lib/src/components/sprite_batch_component.dart @@ -21,6 +21,7 @@ class SpriteBatchComponent extends Component { @override void render(Canvas canvas) { + super.render(canvas); spriteBatch?.render( canvas, blendMode: blendMode, diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index 43226b884..92d89566f 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -107,6 +107,7 @@ class TextBoxComponent extends PositionComponent { @override Future onLoad() async { + await super.onLoad(); await redraw(); } diff --git a/packages/flame/lib/src/device.dart b/packages/flame/lib/src/device.dart index 4f1f0dbf1..628e2be8c 100644 --- a/packages/flame/lib/src/device.dart +++ b/packages/flame/lib/src/device.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/packages/flame/lib/src/effects/color_effect.dart b/packages/flame/lib/src/effects/color_effect.dart index 4368e72e6..35934890f 100644 --- a/packages/flame/lib/src/effects/color_effect.dart +++ b/packages/flame/lib/src/effects/color_effect.dart @@ -11,6 +11,7 @@ class ColorEffect extends ComponentEffect { final String? paintId; late Paint _original; + late Paint _peak; late ColorTween _tween; @@ -18,21 +19,28 @@ class ColorEffect extends ComponentEffect { required this.color, required this.duration, this.paintId, - Curve? curve, bool isInfinite = false, bool isAlternating = false, + double? initialDelay, + double? peakDelay, + Curve? curve, + bool? removeOnFinish, }) : super( isInfinite, isAlternating, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, curve: curve, ); @override - void initialize(HasPaint component) { - super.initialize(component); - peakTime = duration; + Future onLoad() async { + super.onLoad(); + setPeakTimeFromDuration(duration); - _original = component.getPaint(paintId); + _original = affectedParent.getPaint(paintId); + _peak = Paint()..color = color; _tween = ColorTween( begin: _original.color, @@ -41,21 +49,27 @@ class ColorEffect extends ComponentEffect { } @override - void setComponentToEndState() { - component?.tint(color); + void setComponentToOriginalState() { + affectedParent.paint = _original; } @override - void setComponentToOriginalState() { - component?.paint = _original; + void setComponentToPeakState() { + affectedParent.tint(_peak.color); } @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); + if (hasCompleted()) { + return; + } final color = _tween.lerp(curveProgress); if (color != null) { - component?.tint(color); + affectedParent.tint(color); } } } diff --git a/packages/flame/lib/src/effects/combined_effect.dart b/packages/flame/lib/src/effects/combined_effect.dart index 4351f4889..866cb81c5 100644 --- a/packages/flame/lib/src/effects/combined_effect.dart +++ b/packages/flame/lib/src/effects/combined_effect.dart @@ -1,109 +1,70 @@ import 'dart:math'; import 'dart:ui'; -import '../components/position_component.dart'; +import '../../components.dart'; import 'effects.dart'; -class CombinedEffect extends PositionComponentEffect { - final List effects; - final double offset; +/// The [CombinedEffect] is just a container for multiple effects, all settings +/// are handled on each individual effect. [CombinedEffect] is not an effect +/// itself. +class CombinedEffect extends ComponentEffect with EffectsHelper { + /// A [CombinedEffect] is infinite if any of its children has [isInfinite] set + /// to true. + @override + bool get isInfinite => effects.any((effect) => effect.isInfinite); + + /// A [CombinedEffect] can't alternate, but it can have alternating children. + @override + final bool isAlternating = false; + + @override + final double initialDelay = 0.0; + + @override + final double peakDelay = 0.0; CombinedEffect({ - required this.effects, - this.offset = 0.0, - bool isInfinite = false, - bool isAlternating = false, + List effects = const [], + bool? removeOnFinish, VoidCallback? onComplete, }) : super( - isInfinite, - isAlternating, - modifiesPosition: effects.any((e) => e.modifiesPosition), - modifiesAngle: effects.any((e) => e.modifiesAngle), - modifiesSize: effects.any((e) => e.modifiesSize), + false, + false, + removeOnFinish: removeOnFinish, onComplete: onComplete, ) { - assert( - effects.every((effect) => effect.component == null), - 'Each effect can only be added once', - ); - final types = effects.map((e) => e.runtimeType); - assert( - types.toSet().length == types.length, - "All effect types have to be different so that they don't clash", - ); + effects.forEach(add); } @override - void initialize(PositionComponent component) { - super.initialize(component); - effects.forEach((effect) { - effect.initialize(component); - // Only change these if the effect modifies these - endPosition = effect.originalPosition != effect.endPosition - ? effect.endPosition - : endPosition; - endAngle = - effect.originalAngle != effect.endAngle ? effect.endAngle : endAngle; - endSize = - effect.originalSize != effect.endSize ? effect.endSize : endSize; - peakTime = max( - peakTime, - effect.iterationTime + offset * effects.indexOf(effect), + Future add(Component component) async { + await super.add(component); + if (component is ComponentEffect && component.isPrepared) { + final effect = component; + final effects = children.query(); + final types = effects.map((e) => e.runtimeType); + assert( + !types.contains(effect.runtimeType), + "All effect types have to be different so that they don't clash", ); - }); - if (isAlternating) { - endPosition = originalPosition; - endAngle = originalAngle; - endSize = originalSize; + peakTime = max(peakTime, effect.iterationTime); } } @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); - effects.forEach((effect) => _updateEffect(effect, dt)); - if (effects.every((effect) => effect.hasCompleted())) { - if (isAlternating && curveDirection.isNegative) { - effects.forEach((effect) => effect.isAlternating = true); - } - } } + // no-op, since the CombinedEffect can't alternate. + @override + void setComponentToPeakState() {} + @override - void reset() { - super.reset(); - effects.forEach((effect) => effect.reset()); - if (component != null) { - initialize(component!); - } - } - - @override - void dispose() { - super.dispose(); - effects.forEach((effect) => effect.dispose()); - } - - void _updateEffect(PositionComponentEffect effect, double dt) { - final isReverse = curveDirection.isNegative; - final initialOffset = effects.indexOf(effect) * offset; - final effectOffset = - isReverse ? peakTime - effect.peakTime - initialOffset : initialOffset; - final passedOffset = isReverse ? peakTime - currentTime : currentTime; - if (!effect.hasCompleted() && effectOffset < passedOffset) { - final time = - effectOffset < passedOffset - dt ? dt : passedOffset - effectOffset; - effect.update(time); - } - if (isMax()) { - _maybeReverse(effect); - } - } - - void _maybeReverse(PositionComponentEffect effect) { - if (isAlternating && !effect.isAlternating && effect.isMax()) { - // Make the effect go in reverse - effect.isAlternating = true; - } + void setComponentToOriginalState() { + effects.forEach((effect) => effect.setComponentToOriginalState()); } } diff --git a/packages/flame/lib/src/effects/effects.dart b/packages/flame/lib/src/effects/effects.dart index 9d44c96d6..df8b0359a 100644 --- a/packages/flame/lib/src/effects/effects.dart +++ b/packages/flame/lib/src/effects/effects.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../components/base_component.dart'; +import '../../components.dart'; import '../components/position_component.dart'; import '../extensions/vector2.dart'; @@ -11,51 +11,134 @@ export './rotate_effect.dart'; export './sequence_effect.dart'; export './size_effect.dart'; -abstract class ComponentEffect { - T? component; +abstract class ComponentEffect extends Component { + T _affectedParent(Component? component) { + if (component is T) { + return component; + } else { + return _affectedParent(component!.parent); + } + } + + /// If the effect has a parent further up in the tree that will be affected by + /// this effect, that parent will be set here. + late T affectedParent; + + /// The callback that is called when the effect is completed. Function()? onComplete; - bool _isDisposed = false; - bool get isDisposed => _isDisposed; - bool _isPaused = false; + + /// Whether the effect is paused or not. bool get isPaused => _isPaused; + + /// Resume the effect from a paused state. void resume() => _isPaused = false; + + /// Pause the effect. void pause() => _isPaused = true; - /// If the animation should first follow the initial curve and then follow the - /// curve backwards - bool isInfinite; + /// If the effect should first follow the initial curve and then follow the + /// curve backwards. bool isAlternating; + + /// Whether the effect should continue to loop forever. + bool isInfinite; + + /// Whether the effect should be removed from its parent once it has + /// completed. + bool removeOnFinish; + + /// If the effect is relative to the current state of the component or not. final bool isRelative; + final bool _initialIsInfinite; final bool _initialIsAlternating; - double? percentage; + + /// The percentage of the effect that has passed including [initialDelay] and + /// [peakDelay]. + double percentage = 0.0; + + /// The outcome the curve function, only updates after [initialDelay] and before + /// [peakDelay]. double curveProgress = 0.0; + + /// Whether the effect has started or not. + bool hasStarted = false; + + /// How much time it takes for the effect to peak, which means right before it + /// starts any potential reversal or reset. Including both offsets. double peakTime = 0.0; + + /// The time passed since the start of the effect, it will start to decrease + /// after it has reached [peakTime] if [isAlternating] is true, and reset to + /// zero if it is not. double currentTime = 0.0; + + /// When an effect reaches the end, and the beginning if it is alternating, + /// it will overshoot 0.0 and [peakTime], this time is added to the next time + /// step. double driftTime = 0.0; + + /// Whether the effect is going forward or is reversing. + /// + /// Reversing in this context means that after the effect has peaked and if it + /// has [isAlternating] set to true, it will do the effect backwards back to + /// its original state. int curveDirection = 1; + + /// Which curve that the effect is following. Curve curve; + /// The time (in seconds) that should pass before the effect starts each + /// iteration. + double initialDelay; + + /// The time (in seconds) that should pass before the effect ends each + /// peak. + double peakDelay; + + /// The total time offset spent waiting in one iteration, so from the start to + /// the end of the effect and then back again if it [isAlternating]. + double get totalOffset => initialDelay + peakDelay * (isAlternating ? 2 : 1); + /// If this is set to true the effect will not be set to its original state /// once it is done. bool skipEffectReset = false; + /// The total time of one iteration of the effect, including offsets. double get iterationTime => peakTime * (isAlternating ? 2 : 1); ComponentEffect( this._initialIsInfinite, this._initialIsAlternating, { this.isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, Curve? curve, this.onComplete, }) : isInfinite = _initialIsInfinite, isAlternating = _initialIsAlternating, + initialDelay = initialDelay ?? 0.0, + peakDelay = peakDelay ?? 0.0, + removeOnFinish = removeOnFinish ?? true, curve = curve ?? Curves.linear; + @override + Future onLoad() async { + super.onLoad(); + affectedParent = _affectedParent(parent); + parent?.children.register(); + } + + @override @mustCallSuper void update(double dt) { + if (isPaused) { + return; + } + super.update(dt); if (isAlternating) { curveDirection = isMax() ? -1 : (isMin() ? 1 : curveDirection); } @@ -64,43 +147,57 @@ abstract class ComponentEffect { reset(); } } - if (!hasCompleted()) { - currentTime += (dt + driftTime) * curveDirection; - percentage = (currentTime / peakTime).clamp(0.0, 1.0).toDouble(); - curveProgress = curve.transform(percentage!); - _updateDriftTime(); - currentTime = currentTime.clamp(0.0, peakTime).toDouble(); + + currentTime += (dt + driftTime) * curveDirection; + percentage = (currentTime / peakTime).clamp(0.0, 1.0).toDouble(); + if (currentTime >= initialDelay && currentTime <= peakTime - peakDelay) { + final effectPercentage = + ((currentTime - initialDelay) / (peakTime - initialDelay - peakDelay)) + .clamp(0.0, 1.0); + curveProgress = curve.transform(effectPercentage); } - } + _updateDriftTime(); + currentTime = currentTime.clamp(0.0, peakTime).toDouble(); - @mustCallSuper - void initialize(T component) { - this.component = component; + if (hasCompleted()) { + setComponentToEndState(); + onComplete?.call(); + if (removeOnFinish) { + removeFromParent(); + } + } + hasStarted = true; } - void dispose() => _isDisposed = true; - /// Whether the effect has completed or not. bool hasCompleted() { return (!isInfinite && !isAlternating && isMax()) || - (!isInfinite && isAlternating && isMin()) || - isDisposed; + (!isInfinite && isAlternating && isMin() && hasStarted) || + shouldRemove; } - bool isMax() => percentage == null ? false : percentage == 1.0; - bool isMin() => percentage == null ? false : percentage == 0.0; - bool isRootEffect() => component?.effects.contains(this) == true; + /// Whether the effect has reached its end, before potentially reversing + bool isMax() => percentage == 1.0; + + /// Whether the effect is at its beginning + bool isMin() => percentage == 0.0; + + bool isRootEffect() { + return parent is! ComponentEffect; + } /// Resets the effect and the component which the effect was added to. + @mustCallSuper void reset() { resetEffect(); setComponentToOriginalState(); } /// Resets the effect to its original state so that it can be re-run. + @mustCallSuper void resetEffect() { - _isDisposed = false; - percentage = null; + shouldRemove = false; + percentage = 0.0; currentTime = 0.0; curveDirection = 1; isInfinite = _initialIsInfinite; @@ -123,60 +220,103 @@ abstract class ComponentEffect { /// Called when the effect is removed from the component. /// Calls the [onComplete] callback if it is defined and sets the effect back /// to its original state so that it can be re-added. + @override void onRemove() { - onComplete?.call(); + super.onRemove(); if (!skipEffectReset) { resetEffect(); } } + /// Sets the affected component to the state that it should be in before the + /// effect is started. void setComponentToOriginalState(); - void setComponentToEndState(); + + /// Sets the affected component to the state that it should be in when the + /// effect is peaking (before it potentially starts reversing). + void setComponentToPeakState(); + + /// Sets the affected component to the state that it should be in after the + /// effect is done. + void setComponentToEndState() { + isAlternating ? setComponentToOriginalState() : setComponentToPeakState(); + } + + void setPeakTimeFromDuration(double duration) { + peakTime = duration / (isAlternating ? 2 : 1) + initialDelay + peakDelay; + } } abstract class PositionComponentEffect extends ComponentEffect { + /// The duration of the effect + double? duration; + + /// The speed of the effect + double? speed; + /// Used to be able to determine the start state of the component Vector2? originalPosition; double? originalAngle; Vector2? originalSize; Vector2? originalScale; - /// Used to be able to determine the end state of a sequence of effects - Vector2? endPosition; - double? endAngle; - Vector2? endSize; - Vector2? endScale; + /// Used to be able to determine what state that the component should be in + /// when the effect is peaking. + Vector2? peakPosition; + double? peakAngle; + Vector2? peakSize; + Vector2? peakScale; + + /// Used to be able to determine what state that the component should be in + /// when the effect is done. + Vector2? get endPosition => isAlternating ? originalPosition : peakPosition; + double? get endAngle => isAlternating ? originalAngle : peakAngle; + Vector2? get endSize => isAlternating ? originalSize : peakSize; + Vector2? get endScale => isAlternating ? originalScale : peakScale; /// Whether the state of a certain field was modified by the effect - final bool modifiesPosition; - final bool modifiesAngle; - final bool modifiesSize; - final bool modifiesScale; + bool modifiesPosition; + bool modifiesAngle; + bool modifiesSize; + bool modifiesScale; PositionComponentEffect( bool initialIsInfinite, bool initialIsAlternating, { + this.duration, + this.speed, bool isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, Curve? curve, this.modifiesPosition = false, this.modifiesAngle = false, this.modifiesSize = false, this.modifiesScale = false, VoidCallback? onComplete, - }) : super( + }) : assert( + (duration != null) ^ (speed != null), + 'Either speed or duration necessary', + ), + super( initialIsInfinite, initialIsAlternating, isRelative: isRelative, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, curve: curve, onComplete: onComplete, ); @mustCallSuper @override - void initialize(PositionComponent component) { - super.initialize(component); - this.component = component; + Future onLoad() async { + super.onLoad(); + final component = affectedParent; + originalPosition = component.position.clone(); originalAngle = component.angle; originalSize = component.size.clone(); @@ -185,10 +325,10 @@ abstract class PositionComponentEffect /// If these aren't modified by the extending effect it is assumed that the /// effect didn't bring the component to another state than the one it /// started in - endPosition = component.position.clone(); - endAngle = component.angle; - endSize = component.size.clone(); - endScale = component.scale.clone(); + peakPosition = component.position.clone(); + peakAngle = component.angle; + peakSize = component.size.clone(); + peakScale = component.scale.clone(); } /// Only change the parts of the component that is affected by the @@ -200,35 +340,33 @@ abstract class PositionComponentEffect Vector2? size, Vector2? scale, ) { - if (isRootEffect()) { - if (modifiesPosition) { - assert( - position != null, - '`position` must not be `null` for an effect which modifies `position`', - ); - component?.position.setFrom(position!); - } - if (modifiesAngle) { - assert( - angle != null, - '`angle` must not be `null` for an effect which modifies `angle`', - ); - component?.angle = angle!; - } - if (modifiesSize) { - assert( - size != null, - '`size` must not be `null` for an effect which modifies `size`', - ); - component?.size.setFrom(size!); - } - if (modifiesScale) { - assert( - scale != null, - '`scale` must not be `null` for an effect which modifies `scale`', - ); - component?.scale.setFrom(scale!); - } + if (modifiesPosition) { + assert( + position != null, + '`position` must not be `null` for an effect which modifies `position`', + ); + affectedParent.position.setFrom(position!); + } + if (modifiesAngle) { + assert( + angle != null, + '`angle` must not be `null` for an effect which modifies `angle`', + ); + affectedParent.angle = angle!; + } + if (modifiesSize) { + assert( + size != null, + '`size` must not be `null` for an effect which modifies `size`', + ); + affectedParent.size.setFrom(size!); + } + if (modifiesScale) { + assert( + scale != null, + '`scale` must not be `null` for an effect which modifies `scale`', + ); + affectedParent.scale.setFrom(scale!); } } @@ -243,40 +381,19 @@ abstract class PositionComponentEffect } @override - void setComponentToEndState() { - _setComponentState(endPosition, endAngle, endSize, endScale); + void setComponentToPeakState() { + _setComponentState(peakPosition, peakAngle, peakSize, peakScale); } } -abstract class SimplePositionComponentEffect extends PositionComponentEffect { - double? duration; - double? speed; +mixin EffectsHelper on Component { + void clearEffects() { + children.removeAll(effects); + } - SimplePositionComponentEffect( - bool initialIsInfinite, - bool initialIsAlternating, { - this.duration, - this.speed, - Curve? curve, - bool isRelative = false, - bool modifiesPosition = false, - bool modifiesAngle = false, - bool modifiesSize = false, - bool modifiesScale = false, - VoidCallback? onComplete, - }) : assert( - (duration != null) ^ (speed != null), - 'Either speed or duration necessary', - ), - super( - initialIsInfinite, - initialIsAlternating, - isRelative: isRelative, - curve: curve, - modifiesPosition: modifiesPosition, - modifiesAngle: modifiesAngle, - modifiesSize: modifiesSize, - modifiesScale: modifiesScale, - onComplete: onComplete, - ); + List get effects { + return children.isRegistered() + ? children.query() + : []; + } } diff --git a/packages/flame/lib/src/effects/effects_handler.dart b/packages/flame/lib/src/effects/effects_handler.dart deleted file mode 100644 index 3a62f8575..000000000 --- a/packages/flame/lib/src/effects/effects_handler.dart +++ /dev/null @@ -1,56 +0,0 @@ -import '../components/base_component.dart'; -import 'effects.dart'; - -export './move_effect.dart'; -export './rotate_effect.dart'; -export './sequence_effect.dart'; -export './size_effect.dart'; - -class EffectsHandler { - /// The effects that should run on the component - final List _effects = []; - - /// The effects that should be added on the next update iteration - final List _addLater = []; - - void update(double dt) { - _effects.addAll(_addLater); - _addLater.clear(); - _effects.removeWhere((e) { - final hasCompleted = e.hasCompleted(); - if (hasCompleted) { - e.onRemove(); - } - return hasCompleted; - }); - _effects.where((e) => !e.isPaused).forEach((e) { - e.update(dt); - if (e.hasCompleted()) { - e.setComponentToEndState(); - } - }); - } - - /// Add an effect to the handler - void add(ComponentEffect effect, BaseComponent component) { - _addLater.add(effect..initialize(component)); - } - - /// Mark an effect for removal - void removeEffect(ComponentEffect effect) { - effect.dispose(); - } - - /// Remove all effects - void clearEffects() { - _addLater.forEach(removeEffect); - _effects.forEach(removeEffect); - } - - /// Get a list of non removed effects - List get effects { - return List.from(_effects) - ..addAll(_addLater) - ..where((e) => !e.hasCompleted()); - } -} diff --git a/packages/flame/lib/src/effects/move_effect.dart b/packages/flame/lib/src/effects/move_effect.dart index 55eb49cf9..215f2f9f4 100644 --- a/packages/flame/lib/src/effects/move_effect.dart +++ b/packages/flame/lib/src/effects/move_effect.dart @@ -20,11 +20,10 @@ class Vector2Percentage { ); } -class MoveEffect extends SimplePositionComponentEffect { +class MoveEffect extends PositionComponentEffect { List path; Vector2Percentage? _currentSubPath; List? _percentagePath; - late Vector2 _startPosition; /// Duration or speed needs to be defined MoveEffect({ @@ -35,12 +34,11 @@ class MoveEffect extends SimplePositionComponentEffect { bool isInfinite = false, bool isAlternating = false, bool isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, VoidCallback? onComplete, - }) : assert( - (duration != null) ^ (speed != null), - 'Either speed or duration necessary', - ), - super( + }) : super( isInfinite, isAlternating, duration: duration, @@ -48,19 +46,21 @@ class MoveEffect extends SimplePositionComponentEffect { curve: curve, isRelative: isRelative, modifiesPosition: true, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, onComplete: onComplete, ); @override - void initialize(PositionComponent component) { - super.initialize(component); + Future onLoad() async { + super.onLoad(); List _movePath; - _startPosition = component.position.clone(); // With relative here we mean that any vector in the list is relative // to the previous vector in the list, except the first one which is // relative to the start position of the component. if (isRelative) { - var lastPosition = _startPosition; + var lastPosition = originalPosition!; _movePath = []; for (final v in path) { final nextPosition = v + lastPosition; @@ -70,17 +70,17 @@ class MoveEffect extends SimplePositionComponentEffect { } else { _movePath = path; } - endPosition = isAlternating ? _startPosition : _movePath.last; + peakPosition = _movePath.last; var pathLength = 0.0; - var lastPosition = _startPosition; + var lastPosition = originalPosition!; for (final v in _movePath) { pathLength += v.distanceTo(lastPosition); lastPosition = v; } _percentagePath = []; - lastPosition = _startPosition; + lastPosition = originalPosition!; for (final v in _movePath) { final lengthToPrevious = lastPosition.distanceTo(v); final lastEndAt = @@ -101,14 +101,13 @@ class MoveEffect extends SimplePositionComponentEffect { // `duration` is not null when speed is null duration ??= totalPathLength / speed!; - - // `speed` is always not null here already - peakTime = isAlternating ? duration! / 2 : duration!; + duration = duration! + totalOffset; + setPeakTimeFromDuration(duration!); } @override - void reset() { - super.reset(); + void resetEffect() { + super.resetEffect(); if (_percentagePath?.isNotEmpty ?? false) { _currentSubPath = _percentagePath!.first; } @@ -116,7 +115,13 @@ class MoveEffect extends SimplePositionComponentEffect { @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); + if (hasCompleted()) { + return; + } _currentSubPath ??= _percentagePath!.first; if (!curveDirection.isNegative && _currentSubPath!.endAt < curveProgress || curveDirection.isNegative && _currentSubPath!.startAt > curveProgress) { @@ -126,7 +131,7 @@ class MoveEffect extends SimplePositionComponentEffect { final lastEndAt = _currentSubPath!.startAt; final localPercentage = (curveProgress - lastEndAt) / (_currentSubPath!.endAt - lastEndAt); - component?.position.setFrom(_currentSubPath!.previous + + affectedParent.position.setFrom(_currentSubPath!.previous + ((_currentSubPath!.v - _currentSubPath!.previous) * localPercentage)); } } diff --git a/packages/flame/lib/src/effects/opacity_effect.dart b/packages/flame/lib/src/effects/opacity_effect.dart index 3f1c9973f..0a4b963b4 100644 --- a/packages/flame/lib/src/effects/opacity_effect.dart +++ b/packages/flame/lib/src/effects/opacity_effect.dart @@ -11,7 +11,7 @@ class OpacityEffect extends ComponentEffect { final String? paintId; late Color _original; - late Color _final; + late Color _peak; late double _difference; @@ -19,13 +19,19 @@ class OpacityEffect extends ComponentEffect { required this.opacity, required this.duration, this.paintId, - Curve? curve, bool isInfinite = false, bool isAlternating = false, + double? initialDelay, + double? peakDelay, + Curve? curve, + bool? removeOnFinish, }) : super( isInfinite, isAlternating, + initialDelay: initialDelay, + peakDelay: peakDelay, curve: curve, + removeOnFinish: removeOnFinish, ); OpacityEffect.fadeOut({ @@ -55,27 +61,28 @@ class OpacityEffect extends ComponentEffect { ); @override - void initialize(HasPaint component) { - super.initialize(component); + Future onLoad() async { + super.onLoad(); peakTime = duration; - _original = component.getPaint(paintId).color; - _final = _original.withOpacity(opacity); + _original = affectedParent.getPaint(paintId).color; + _peak = _original.withOpacity(opacity); _difference = _original.opacity - opacity; + setPeakTimeFromDuration(duration); } @override - void setComponentToEndState() { - component?.setColor( - _final, + void setComponentToPeakState() { + affectedParent.setColor( + _peak, paintId: paintId, ); } @override void setComponentToOriginalState() { - component?.setColor( + affectedParent.setColor( _original, paintId: paintId, ); @@ -83,8 +90,14 @@ class OpacityEffect extends ComponentEffect { @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); - component?.setOpacity( + if (hasCompleted()) { + return; + } + affectedParent.setOpacity( _original.opacity - _difference * curveProgress, paintId: paintId, ); diff --git a/packages/flame/lib/src/effects/rotate_effect.dart b/packages/flame/lib/src/effects/rotate_effect.dart index be1244caf..7dd46f6d3 100644 --- a/packages/flame/lib/src/effects/rotate_effect.dart +++ b/packages/flame/lib/src/effects/rotate_effect.dart @@ -2,12 +2,10 @@ import 'dart:ui'; import 'package:flutter/animation.dart'; -import '../../components.dart'; import 'effects.dart'; -class RotateEffect extends SimplePositionComponentEffect { +class RotateEffect extends PositionComponentEffect { double angle; - late double _startAngle; late double _delta; /// Duration or speed needs to be defined @@ -19,38 +17,44 @@ class RotateEffect extends SimplePositionComponentEffect { bool isInfinite = false, bool isAlternating = false, bool isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, VoidCallback? onComplete, - }) : assert( - (duration != null) ^ (speed != null), - 'Either speed or duration necessary', - ), - super( + }) : super( isInfinite, isAlternating, duration: duration, speed: speed, curve: curve, isRelative: isRelative, + removeOnFinish: removeOnFinish, + initialDelay: initialDelay, + peakDelay: peakDelay, modifiesAngle: true, onComplete: onComplete, ); @override - void initialize(PositionComponent component) { - super.initialize(component); - _startAngle = component.angle; - _delta = isRelative ? angle : angle - _startAngle; - if (!isAlternating) { - endAngle = _startAngle + _delta; - } + Future onLoad() async { + super.onLoad(); + final startAngle = originalAngle!; + _delta = isRelative ? angle : angle - startAngle; + peakAngle = startAngle + _delta; speed ??= _delta.abs() / duration!; duration ??= _delta.abs() / speed!; - peakTime = isAlternating ? duration! / 2 : duration!; + setPeakTimeFromDuration(duration!); } @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); - component?.angle = _startAngle + _delta * curveProgress; + if (hasCompleted()) { + return; + } + affectedParent.angle = originalAngle! + _delta * curveProgress; } } diff --git a/packages/flame/lib/src/effects/scale_effect.dart b/packages/flame/lib/src/effects/scale_effect.dart index 595753f2f..2def0d09d 100644 --- a/packages/flame/lib/src/effects/scale_effect.dart +++ b/packages/flame/lib/src/effects/scale_effect.dart @@ -6,9 +6,8 @@ import '../../components.dart'; import '../extensions/vector2.dart'; import 'effects.dart'; -class ScaleEffect extends SimplePositionComponentEffect { +class ScaleEffect extends PositionComponentEffect { Vector2 scale; - late Vector2 _startScale; late Vector2 _delta; /// Duration or speed needs to be defined @@ -20,12 +19,11 @@ class ScaleEffect extends SimplePositionComponentEffect { bool isInfinite = false, bool isAlternating = false, bool isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, VoidCallback? onComplete, - }) : assert( - duration != null || speed != null, - 'Either speed or duration necessary', - ), - super( + }) : super( isInfinite, isAlternating, duration: duration, @@ -33,25 +31,32 @@ class ScaleEffect extends SimplePositionComponentEffect { curve: curve, isRelative: isRelative, modifiesScale: true, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, onComplete: onComplete, ); @override - void initialize(PositionComponent component) { - super.initialize(component); - _startScale = component.scale.clone(); - _delta = isRelative ? scale : scale - _startScale; - if (!isAlternating) { - endScale = _startScale + _delta; - } + Future onLoad() async { + super.onLoad(); + final startScale = originalScale!; + _delta = isRelative ? scale : scale - startScale; + peakScale = startScale + _delta; speed ??= _delta.length / duration!; duration ??= _delta.length / speed!; - peakTime = isAlternating ? duration! / 2 : duration!; + setPeakTimeFromDuration(duration!); } @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); - component?.scale.setFrom(_startScale + _delta * curveProgress); + if (hasCompleted()) { + return; + } + affectedParent.scale.setFrom(originalScale! + _delta * curveProgress); } } diff --git a/packages/flame/lib/src/effects/sequence_effect.dart b/packages/flame/lib/src/effects/sequence_effect.dart index 5b58aaaa4..eb7b3574c 100644 --- a/packages/flame/lib/src/effects/sequence_effect.dart +++ b/packages/flame/lib/src/effects/sequence_effect.dart @@ -1,12 +1,10 @@ import 'dart:ui'; -import '../components/position_component.dart'; import 'effects.dart'; class SequenceEffect extends PositionComponentEffect { - final List effects; - late PositionComponentEffect currentEffect; - late bool _currentWasAlternating; + final List effects; + late ComponentEffect currentEffect; static const int _initialIndex = 0; static const double _initialDriftModifier = 0.0; @@ -15,99 +13,123 @@ class SequenceEffect extends PositionComponentEffect { double _driftModifier = _initialDriftModifier; SequenceEffect({ - required this.effects, + this.effects = const [], bool isInfinite = false, bool isAlternating = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, VoidCallback? onComplete, }) : super( isInfinite, isAlternating, - modifiesPosition: effects.any((e) => e.modifiesPosition), - modifiesAngle: effects.any((e) => e.modifiesAngle), - modifiesSize: effects.any((e) => e.modifiesSize), + duration: 0.0, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, onComplete: onComplete, ) { assert( - effects.every((effect) => effect.component == null), + effects.every((effect) => effect.parent == null), 'Each effect can only be added once', ); assert( effects.every((effect) => !effect.isInfinite), 'No effects added to the sequence can be infinite', ); + final types = effects.map((e) => e.runtimeType); + assert( + types.toSet().length == types.length, + "All effect types have to be different so that they don't clash", + ); } @override - void initialize(PositionComponent component) { - super.initialize(component); + Future onLoad() async { + await super.onLoad(); _currentIndex = _initialIndex; _driftModifier = _initialDriftModifier; + peakTime = initialDelay + peakDelay; - effects.forEach((effect) { - effect.reset(); - component.position.setFrom(endPosition!); - component.angle = endAngle!; - component.size.setFrom(endSize!); - effect.initialize(component); - endPosition = effect.endPosition; - endAngle = effect.endAngle; - endSize = effect.endSize; - }); - // Add all the effects iteration time since they can alternate within the - // sequence effect - peakTime = effects.fold( - 0, - (time, effect) => time + effect.iterationTime, - ); - if (isAlternating) { - endPosition = originalPosition; - endAngle = originalAngle; - endSize = originalSize; + for (final effect in effects) { + effect.removeOnFinish = false; + if (effect is PositionComponentEffect) { + // We set the affected parent to the current peak position on the + // sequence effect so that it can continue from where the last effect + // ended. + affectedParent.position.setFrom(peakPosition!); + affectedParent.angle = peakAngle!; + affectedParent.size.setFrom(peakSize!); + affectedParent.scale.setFrom(peakScale!); + await add(effect); + peakPosition!.setFrom(effect.endPosition!); + peakAngle = effect.endAngle; + peakSize!.setFrom(effect.endSize!); + peakScale!.setFrom(effect.endScale!); + // Since only the parent effect will reset the affected component we + // need to check what properties the child effects affect. + modifiesPosition |= effect.modifiesPosition; + modifiesAngle |= effect.modifiesAngle; + modifiesSize |= effect.modifiesSize; + modifiesScale |= effect.modifiesScale; + } + effect.pause(); + // Add the full effects iteration time since they can alternate within the + // sequence effect + peakTime += effect.iterationTime; } - component.position.setFrom(originalPosition!); - component.angle = originalAngle!; - component.size.setFrom(originalSize!); - currentEffect = effects.first; - _currentWasAlternating = currentEffect.isAlternating; + + // Set the parent to the state that it should have before the effects are + // executed + affectedParent.position.setFrom(originalPosition!); + affectedParent.angle = originalAngle!; + affectedParent.size.setFrom(originalSize!); + affectedParent.scale.setFrom(originalScale!); + + currentEffect = effects[0]; + currentEffect.resume(); } @override void update(double dt) { + if (isPaused || hasCompleted()) { + return; + } super.update(dt); - // If the last effect's time to completion overshot its total time, add that - // time to the first time step of the next effect. - currentEffect.update(dt + _driftModifier); _driftModifier = 0.0; if (currentEffect.hasCompleted()) { - currentEffect.setComponentToEndState(); _driftModifier = currentEffect.driftTime; + // Reset the effect if it was alternating so that it can repeat when the + // SequenceEffect alternates. + if (currentEffect.isAlternating && isAlternating) { + currentEffect.resetEffect(); + } + // Pause the current effect so that the next effect can continue. + currentEffect.pause(); + _currentIndex++; + // Whether the effects should start to go in reverse in this time step. + final shouldReverse = + isAlternating && (curveDirection.isNegative || isMax()); final orderedEffects = - curveDirection.isNegative ? effects.reversed.toList() : effects; - // Make sure the current effect has the `isAlternating` value it - // initially started with - currentEffect.isAlternating = _currentWasAlternating; - // Get the next effect that should be executed + shouldReverse ? effects.reversed.toList() : effects; + // Get the next effect that should be executed. currentEffect = orderedEffects[_currentIndex % effects.length]; - // Keep track of what value of `isAlternating` the effect had from the - // start - _currentWasAlternating = currentEffect.isAlternating; - if (isAlternating && - !currentEffect.isAlternating && - curveDirection.isNegative) { - // Make the effect go in reverse + // If the last effect's time to completion overshot its total time, add that + // time to the first time step of the next effect. + currentEffect.driftTime = _driftModifier; + if (shouldReverse && !currentEffect.isAlternating) { + // Make the current upcoming effect go in reverse. currentEffect.isAlternating = true; } + currentEffect.resume(); } } @override void reset() { super.reset(); - effects.forEach((e) => e.reset()); - if (component != null) { - initialize(component!); - } + effects.forEach((e) => e.resetEffect()); } } diff --git a/packages/flame/lib/src/effects/size_effect.dart b/packages/flame/lib/src/effects/size_effect.dart index b1ac89e95..6334d3e87 100644 --- a/packages/flame/lib/src/effects/size_effect.dart +++ b/packages/flame/lib/src/effects/size_effect.dart @@ -6,9 +6,8 @@ import '../../components.dart'; import '../extensions/vector2.dart'; import 'effects.dart'; -class SizeEffect extends SimplePositionComponentEffect { +class SizeEffect extends PositionComponentEffect { Vector2 size; - late Vector2 _startSize; late Vector2 _delta; /// Duration or speed needs to be defined @@ -20,12 +19,11 @@ class SizeEffect extends SimplePositionComponentEffect { bool isInfinite = false, bool isAlternating = false, bool isRelative = false, + double? initialDelay, + double? peakDelay, + bool? removeOnFinish, VoidCallback? onComplete, - }) : assert( - duration != null || speed != null, - 'Either speed or duration necessary', - ), - super( + }) : super( isInfinite, isAlternating, duration: duration, @@ -33,25 +31,32 @@ class SizeEffect extends SimplePositionComponentEffect { curve: curve, isRelative: isRelative, modifiesSize: true, + initialDelay: initialDelay, + peakDelay: peakDelay, + removeOnFinish: removeOnFinish, onComplete: onComplete, ); @override - void initialize(PositionComponent component) { - super.initialize(component); - _startSize = component.size.clone(); - _delta = isRelative ? size : size - _startSize; - if (!isAlternating) { - endSize = _startSize + _delta; - } + Future onLoad() async { + super.onLoad(); + final startSize = originalSize!; + _delta = isRelative ? size : size - startSize; + peakSize = startSize + _delta; speed ??= _delta.length / duration!; duration ??= _delta.length / speed!; - peakTime = isAlternating ? duration! / 2 : duration!; + setPeakTimeFromDuration(duration!); } @override void update(double dt) { + if (isPaused) { + return; + } super.update(dt); - component?.size.setFrom(_startSize + _delta * curveProgress); + if (hasCompleted()) { + return; + } + affectedParent.size.setFrom(originalSize! + _delta * curveProgress); } } diff --git a/packages/flame/lib/src/game/base_game.dart b/packages/flame/lib/src/game/base_game.dart deleted file mode 100644 index 04420bc7e..000000000 --- a/packages/flame/lib/src/game/base_game.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'dart:ui'; - -import 'package:meta/meta.dart'; -import 'package:ordered_set/queryable_ordered_set.dart'; - -import '../../components.dart'; -import '../../extensions.dart'; -import '../components/component.dart'; -import '../components/mixins/collidable.dart'; -import '../components/mixins/draggable.dart'; -import '../components/mixins/has_collidables.dart'; -import '../components/mixins/has_game_ref.dart'; -import '../components/mixins/hoverable.dart'; -import '../components/mixins/tappable.dart'; -import 'camera/camera.dart'; -import 'camera/camera_wrapper.dart'; -import 'game.dart'; - -/// This is a more complete and opinionated implementation of Game. -/// -/// BaseGame should be extended to add your game logic. -/// [update], [render] and [onResize] methods have default implementations. -/// This is the recommended structure to use for most games. -/// It is based on the Component system. -class BaseGame extends Game { - BaseGame() { - components = createComponentSet(); - _cameraWrapper = CameraWrapper(Camera(), components); - } - - /// The list of components to be updated and rendered by the base game. - late final ComponentSet components; - - /// The camera translates the coordinate space after the viewport is applied. - Camera get camera => _cameraWrapper.camera; - - // When the Game becomes a Component (#906), this could be added directly - // into the component tree. - late final CameraWrapper _cameraWrapper; - - /// This is overwritten to consider the viewport transformation. - /// - /// Which means that this is the logical size of the game screen area as - /// exposed to the canvas after viewport transformations and camera zooming. - /// - /// This does not match the Flutter widget size; for that see [canvasSize]. - @override - Vector2 get size => camera.gameSize; - - /// This is the original Flutter widget size, without any transformation. - Vector2 get canvasSize => camera.canvasSize; - - /// This method setps up the OrderedSet instance used by this game, before - /// any lifecycle methods happen. - /// - /// You can return a specific sub-class of OrderedSet, like - /// [QueryableOrderedSet] for example, that we use for Collidables. - ComponentSet createComponentSet() { - final components = ComponentSet.createDefault( - (c, {BaseGame? gameRef}) => prepare(c), - ); - if (this is HasCollidables) { - components.register(); - } - return components; - } - - /// This method is called for every component added. - /// It does preparation on a component before any update or render method is called on it. - /// - /// You can use this to setup your mixins, pre-calculate stuff on every component, or anything you desire. - /// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding. - @mustCallSuper - void prepare(Component c) { - assert( - hasLayout, - '"prepare/add" called before the game is ready. Did you try to access it on the Game constructor? Use the "onLoad" method instead.', - ); - - if (c is Collidable) { - assert( - this is HasCollidables, - 'You can only use the Hitbox/Collidable feature with games that has the HasCollidables mixin', - ); - } - if (c is Tappable) { - assert( - this is HasTappableComponents, - 'Tappable Components can only be added to a BaseGame with HasTappableComponents', - ); - } - if (c is Draggable) { - assert( - this is HasDraggableComponents, - 'Draggable Components can only be added to a BaseGame with HasDraggableComponents', - ); - } - if (c is Hoverable) { - assert( - this is HasHoverableComponents, - 'Hoverable Components can only be added to a BaseGame with HasHoverableComponents', - ); - } - - if (debugMode && c is BaseComponent) { - c.debugMode = true; - } - - if (c is HasGameRef) { - c.gameRef = this; - } - - // first time resize - c.onGameResize(size); - } - - /// Prepares and registers a component to be added on the next game tick - /// - /// This methods is an async operation since it await the `onLoad` method of - /// the component. Nevertheless, this method only need to be waited to finish - /// if by some reason, your logic needs to be sure that the component has - /// finished loading, otherwise, this method can be called without waiting - /// for it to finish as the BaseGame already handle the loading of the - /// component. - /// - /// *Note:* Do not add components on the game constructor. This method can - /// only be called after the game already has its layout set, this can be - /// verified by the [hasLayout] property, to add components upon a game - /// initialization, the [onLoad] method can be used instead. - Future add(Component c) { - return components.addChild(c); - } - - /// Adds a list of components, calling addChild for each one. - /// - /// The returned Future completes once all are loaded and added. - /// Component loading is done in parallel. - Future addAll(Iterable cs) { - return components.addChildren(cs); - } - - /// Removes a component from the component list, calling onRemove for it and - /// its children. - void remove(Component c) { - components.remove(c); - } - - /// Removes all the components in the list and calls onRemove for all of them - /// and their children. - void removeAll(Iterable cs) { - components.removeAll(cs); - } - - /// This implementation of render renders each component, making sure the - /// canvas is reset for each one. - /// - /// You can override it further to add more custom behavior. - /// Beware of however you are rendering components if not using this; you - /// must be careful to save and restore the canvas to avoid components - /// messing up with each other. - @override - @mustCallSuper - void render(Canvas canvas) { - _cameraWrapper.render(canvas); - } - - /// This implementation of update updates every component in the list. - /// - /// It also actually adds the components added via [add] since the previous tick, - /// and remove those that are marked for destruction via the [Component.shouldRemove] method. - /// You can override it further to add more custom behavior. - @override - @mustCallSuper - void update(double dt) { - components.updateComponentList(); - - if (this is HasCollidables) { - (this as HasCollidables).handleCollidables(); - } - - components.forEach((c) => c.update(dt)); - _cameraWrapper.update(dt); - } - - /// This implementation of resize passes the resize call along to every - /// component in the list, enabling each one to make their decisions as how to handle the resize. - /// - /// It also updates the [size] field of the class to be used by later added components and other methods. - /// You can override it further to add more custom behavior, but you should seriously consider calling the super implementation as well. - /// This implementation also uses the current [camera] in order to transform the coordinate system appropriately. - @override - @mustCallSuper - void onResize(Vector2 canvasSize) { - super.onResize(canvasSize); - camera.handleResize(canvasSize); - components.forEach((c) => c.onGameResize(size)); - } - - /// Returns whether this [Game] is in debug mode or not. - /// - /// Returns `false` by default. Override it, or set it to true, to use debug mode. - /// You can use this value to enable debug behaviors for your game and many components will - /// show extra information on the screen when debug mode is activated - bool debugMode = false; - - /// Changes the priority of [component] and reorders the games component list. - /// - /// Returns true if changing the component's priority modified one of the - /// components that existed directly on the game and false if it - /// either was a child of another component, if it didn't exist at all or if - /// it was a component added directly on the game but its priority didn't - /// change. - bool changePriority( - Component component, - int priority, { - bool reorderRoot = true, - }) { - if (component.priority == priority) { - return false; - } - component.changePriorityWithoutResorting(priority); - if (reorderRoot) { - if (component.parent != null && component.parent is BaseComponent) { - (component.parent! as BaseComponent).reorderChildren(); - } else if (components.contains(component)) { - components.rebalanceAll(); - } - } - return true; - } - - /// Since changing priorities is quite an expensive operation you should use - /// this method if you want to change multiple priorities at once so that the - /// tree doesn't have to be reordered multiple times. - void changePriorities(Map priorities) { - var hasRootComponents = false; - final parents = {}; - priorities.forEach((component, priority) { - final wasUpdated = changePriority( - component, - priority, - reorderRoot: false, - ); - if (wasUpdated) { - if (component.parent != null && component.parent is BaseComponent) { - parents.add(component.parent! as BaseComponent); - } else { - hasRootComponents |= components.contains(component); - } - } - }); - if (hasRootComponents) { - components.rebalanceAll(); - } - parents.forEach((parent) => parent.reorderChildren()); - } - - /// Returns the current time in seconds with microseconds precision. - /// - /// This is compatible with the `dt` value used in the [update] method. - double currentTime() { - return DateTime.now().microsecondsSinceEpoch.toDouble() / - Duration.microsecondsPerSecond; - } - - @override - Vector2 projectVector(Vector2 vector) { - return camera.combinedProjector.projectVector(vector); - } - - @override - Vector2 unprojectVector(Vector2 vector) { - return camera.combinedProjector.unprojectVector(vector); - } - - @override - Vector2 scaleVector(Vector2 vector) { - return camera.combinedProjector.scaleVector(vector); - } - - @override - Vector2 unscaleVector(Vector2 vector) { - return camera.combinedProjector.unscaleVector(vector); - } -} diff --git a/packages/flame/lib/src/game/camera/camera.dart b/packages/flame/lib/src/game/camera/camera.dart index c5b1ad521..a94a58fbf 100644 --- a/packages/flame/lib/src/game/camera/camera.dart +++ b/packages/flame/lib/src/game/camera/camera.dart @@ -36,7 +36,7 @@ import '../projector.dart'; /// The shake adds a random immediate delta to each tick to simulate the shake /// effect. /// -/// Note: in the context of the BaseGame, the camera effectively translates +/// Note: in the context of the FlameGame, the camera effectively translates /// the position where components are rendered with relation to the Viewport. /// Components marked as `isHud = true` are always rendered in screen /// coordinates, bypassing the camera altogether. @@ -142,7 +142,7 @@ class Camera extends Projector { /// viewport applies a (normally) fixed zoom to adapt multiple screens into /// one aspect ratio. The zoom might be different per dimension depending /// on the Viewport implementation. Also, if used with the default - /// BaseGame implementation, it will apply to all components. + /// FlameGame implementation, it will apply to all components. /// The zoom from the camera is only for components that respect camera, /// and is applied after the viewport is set. It exists to be used if there /// is any kind of user configurable camera on your game. @@ -153,7 +153,7 @@ class Camera extends Projector { /// Use this method to transform the canvas using the current rules provided /// by this camera object. /// - /// If you are using BaseGame, this will be done for you for all non-HUD + /// If you are using FlameGame, this will be done for you for all non-HUD /// components. /// When using this method you are responsible for saving/restoring canvas /// state to avoid leakage. diff --git a/packages/flame/lib/src/game/camera/camera_wrapper.dart b/packages/flame/lib/src/game/camera/camera_wrapper.dart index 10f965531..b8cf954c2 100644 --- a/packages/flame/lib/src/game/camera/camera_wrapper.dart +++ b/packages/flame/lib/src/game/camera/camera_wrapper.dart @@ -1,13 +1,14 @@ import 'dart:ui'; + import '../../../components.dart'; import 'camera.dart'; -/// This class encapsulates BaseGame's rendering functionality. It will be +/// This class encapsulates FlameGame's rendering functionality. It will be /// converted into a proper Component in a future release, but until then -/// using it in any code other than the BaseGame class is unsafe and +/// using it in any code other than the FlameGame class is unsafe and /// not recommended. class CameraWrapper { - // TODO(st-pasha): extend from BaseComponent + // TODO(st-pasha): extend from Component CameraWrapper(this.camera, this.world); final Camera camera; diff --git a/packages/flame/lib/src/game/flame_game.dart b/packages/flame/lib/src/game/flame_game.dart new file mode 100644 index 000000000..45d6fadf3 --- /dev/null +++ b/packages/flame/lib/src/game/flame_game.dart @@ -0,0 +1,229 @@ +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +import '../../components.dart'; +import '../../extensions.dart'; +import '../components/component.dart'; +import '../components/mixins/collidable.dart'; +import '../components/mixins/draggable.dart'; +import '../components/mixins/has_collidables.dart'; +import '../components/mixins/hoverable.dart'; +import '../components/mixins/tappable.dart'; +import 'camera/camera.dart'; +import 'camera/camera_wrapper.dart'; +import 'mixins/game.dart'; + +/// This is a more complete and opinionated implementation of [Game]. +/// +/// [FlameGame] can be extended to add your game logic, or you can keep the +/// logic in child [Component]s. +/// +/// This is the recommended base class to use for most games made with Flame. +/// It is based on the Flame Component System (also known as FCS). +class FlameGame extends Component with Game { + FlameGame() { + _cameraWrapper = CameraWrapper(Camera(), children); + } + + /// The camera translates the coordinate space after the viewport is applied. + Camera get camera => _cameraWrapper.camera; + + // When the Game becomes a Component (#906), this could be added directly + // into the component tree. + late final CameraWrapper _cameraWrapper; + + /// This is overwritten to consider the viewport transformation. + /// + /// Which means that this is the logical size of the game screen area as + /// exposed to the canvas after viewport transformations and camera zooming. + /// + /// This does not match the Flutter widget size; for that see [canvasSize]. + @override + Vector2 get size => camera.gameSize; + + /// This is the original Flutter widget size, without any transformation. + Vector2 get canvasSize => camera.canvasSize; + + /// This method is called for every component before it is added to the + /// component tree. + /// It does preparation on a component before any update or render method is + /// called on it. + /// + /// You can use this to set up your mixins or pre-calculate things for + /// example. + /// By default, this calls the first [onGameResize] for every component, so + /// don't forget to call `super.prepareComponent` when overriding. + @mustCallSuper + void prepareComponent(Component c) { + assert( + hasLayout, + '"prepare/add" called before the game is ready. ' + 'Did you try to access it on the Game constructor? ' + 'Use the "onLoad" ot "onParentMethod" method instead.', + ); + + if (c is Collidable) { + assert( + this is HasCollidables, + 'You can only use the Hitbox/Collidable feature with games that has ' + 'the HasCollidables mixin', + ); + } + if (c is Tappable) { + assert( + this is HasTappableComponents, + 'Tappable Components can only be added to a FlameGame with ' + 'HasTappableComponents', + ); + } + if (c is Draggable) { + assert( + this is HasDraggableComponents, + 'Draggable Components can only be added to a FlameGame with ' + 'HasDraggableComponents', + ); + } + if (c is Hoverable) { + assert( + this is HasHoverableComponents, + 'Hoverable Components can only be added to a FlameGame with ' + 'HasHoverableComponents', + ); + } + + // First time resize + c.onGameResize(size); + } + + /// This implementation of render renders each component, making sure the + /// canvas is reset for each one. + /// + /// You can override it further to add more custom behavior. + /// Beware of that if you are rendering components without using this method; + /// you must be careful to save and restore the canvas to avoid components + /// interfering with each others rendering. + @override + @mustCallSuper + void render(Canvas canvas) { + _cameraWrapper.render(canvas); + } + + /// This updates every component in the tree. + /// + /// It also adds the components added via [add] since the previous tick, and + /// removes those that are marked for removal via the [remove] and + /// [Component.removeFromParent] methods. + /// You can override it to add more custom behavior. + @override + @mustCallSuper + void update(double dt) { + super.update(dt); + _cameraWrapper.update(dt); + } + + /// This passes the new size along to every component in the tree via their + /// [Component.onGameResize] method, enabling each one to make their decision + /// of how to handle the resize event. + /// + /// It also updates the [size] field of the class to be used by later added + /// components and other methods. + /// You can override it further to add more custom behavior, but you should + /// seriously consider calling the super implementation as well. + /// This implementation also uses the current [camera] in order to transform + /// the coordinate system appropriately. + @override + @mustCallSuper + void onGameResize(Vector2 canvasSize) { + camera.handleResize(canvasSize); + super.onGameResize(canvasSize); + } + + /// Changes the priority of [component] and reorders the games component list. + /// + /// Returns true if changing the component's priority modified one of the + /// components that existed directly on the game and false if it + /// either was a child of another component, if it didn't exist at all or if + /// it was a component added directly on the game but its priority didn't + /// change. + bool changePriority( + Component component, + int priority, { + bool reorderRoot = true, + }) { + if (component.priority == priority) { + return false; + } + component.changePriorityWithoutResorting(priority); + if (reorderRoot) { + final parent = component.parent; + if (parent != null) { + parent.reorderChildren(); + } else if (contains(component)) { + children.rebalanceAll(); + } + } + return true; + } + + /// Since changing priorities is quite an expensive operation you should use + /// this method if you want to change multiple priorities at once so that the + /// tree doesn't have to be reordered multiple times. + void changePriorities(Map priorities) { + var hasRootComponents = false; + final parents = {}; + priorities.forEach((component, priority) { + final wasUpdated = changePriority( + component, + priority, + reorderRoot: false, + ); + if (wasUpdated) { + final parent = component.parent; + if (parent != null) { + parents.add(parent); + } else { + hasRootComponents |= contains(component); + } + } + }); + if (hasRootComponents) { + children.rebalanceAll(); + } + parents.forEach((parent) => parent.reorderChildren()); + } + + /// Whether a point is within the boundaries of the visible part of the game. + @override + bool containsPoint(Vector2 p) { + return p.x > 0 && p.y > 0 && p.x < size.x && p.y < size.y; + } + + /// Returns the current time in seconds with microseconds precision. + /// + /// This is compatible with the `dt` value used in the [update] method. + double currentTime() { + return DateTime.now().microsecondsSinceEpoch.toDouble() / + Duration.microsecondsPerSecond; + } + + @override + Vector2 projectVector(Vector2 vector) { + return camera.combinedProjector.projectVector(vector); + } + + @override + Vector2 unprojectVector(Vector2 vector) { + return camera.combinedProjector.unprojectVector(vector); + } + + @override + Vector2 scaleVector(Vector2 vector) { + return camera.combinedProjector.scaleVector(vector); + } + + @override + Vector2 unscaleVector(Vector2 vector) { + return camera.combinedProjector.unscaleVector(vector); + } +} diff --git a/packages/flame/lib/src/game/game_render_box.dart b/packages/flame/lib/src/game/game_render_box.dart index ff46d7d18..52551390e 100644 --- a/packages/flame/lib/src/game/game_render_box.dart +++ b/packages/flame/lib/src/game/game_render_box.dart @@ -6,8 +6,8 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart' hide WidgetBuilder; import '../extensions/size.dart'; -import 'game.dart'; import 'game_loop.dart'; +import 'mixins/game.dart'; // ignore: prefer_mixin class GameRenderBox extends RenderBox with WidgetsBindingObserver { @@ -25,7 +25,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { @override void performResize() { super.performResize(); - game.onResize(constraints.biggest.toVector2()); + game.onGameResize(constraints.biggest.toVector2()); } @override @@ -48,6 +48,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { @override void detach() { super.detach(); + game.onRemove(); game.detach(); gameLoop?.dispose(); gameLoop = null; diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index f8f9457cb..91357e432 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -5,8 +5,8 @@ import 'package:flutter/widgets.dart'; import '../../../extensions.dart'; import '../../../input.dart'; import '../../extensions/size.dart'; -import '../game.dart'; import '../game_render_box.dart'; +import '../mixins/game.dart'; import 'gestures.dart'; typedef GameLoadingWidgetBuilder = Widget Function( @@ -23,8 +23,8 @@ typedef OverlayWidgetBuilder = Widget Function( T game, ); -/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the flutter tree -/// +/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the +/// Flutter tree. class GameWidget extends StatefulWidget { /// The game instance in which this widget will render final T game; @@ -32,8 +32,9 @@ class GameWidget extends StatefulWidget { /// The text direction to be used in text elements in a game. final TextDirection? textDirection; - /// Builder to provide a widget tree to be built whilst the [Future] provided - /// via [Game.onLoad] is not resolved. By default this is an empty Container(). + /// Builder to provide a widget tree to be built while the Game's [Future] + /// provided via `Game.onLoad` and `Game.onMount` is not resolved. + /// By default this is an empty Container(). final GameLoadingWidgetBuilder? loadingBuilder; /// If set, errors during the onLoad method will not be thrown @@ -42,7 +43,7 @@ class GameWidget extends StatefulWidget { final GameErrorWidgetBuilder? errorBuilder; /// Builder to provide a widget tree to be built between the game elements and - /// the background color provided via [Game.backgroundColor] + /// the background color provided via [Game.backgroundColor]. final WidgetBuilder? backgroundBuilder; /// A map to show widgets overlay. @@ -52,8 +53,9 @@ class GameWidget extends StatefulWidget { /// - [Game.overlays] final Map>? overlayBuilderMap; - /// A List of the initially active overlays, this is used only on the first build of the widget. - /// To control the overlays that are active use [Game.overlays] + /// A List of the initially active overlays, this is used only on the first + /// build of the widget. + /// To control the overlays that are active use [Game.overlays]. /// /// See also: /// - [new GameWidget] @@ -85,7 +87,8 @@ class GameWidget extends StatefulWidget { /// ... /// ``` /// - /// It is also possible to render layers of widgets over the game surface with widget subtrees. + /// It is also possible to render layers of widgets over the game surface with + /// widget subtrees. /// /// To do that a [overlayBuilderMap] should be provided. The visibility of /// these overlays are controlled by [Game.overlays] property @@ -135,10 +138,11 @@ class _GameWidgetState extends State> { MouseCursor? _mouseCursor; - Future? _gameLoaderFuture; - - Future get _gameLoaderFutureCache => - _gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad()); + Future get _loaderFuture { + final onLoad = widget.game.onLoadCache; + final onMount = widget.game.onMount; + return (onLoad ?? Future.value()).then((_) => onMount()); + } late FocusNode _focusNode; @@ -190,9 +194,6 @@ class _GameWidgetState extends State> { // Reset mouse cursor _initMouseCursor(); addMouseCursorListener(); - - // Reset the loader future - _gameLoaderFuture = null; } } @@ -217,7 +218,8 @@ class _GameWidgetState extends State> { }); } - // widget overlay stuff + //#region Widget overlay methods + void addOverlaysListener() { widget.game.overlays.addListener(onChangeActiveOverlays); initialActiveOverlays = widget.game.overlays.value; @@ -243,6 +245,8 @@ class _GameWidgetState extends State> { }); } + //#endregion + KeyEventResult _handleKeyEvent(FocusNode focusNode, RawKeyEvent event) { final game = widget.game; if (game is KeyboardEvents) { @@ -304,9 +308,9 @@ class _GameWidgetState extends State> { color: widget.game.backgroundColor(), child: LayoutBuilder( builder: (_, BoxConstraints constraints) { - widget.game.onResize(constraints.biggest.toVector2()); + widget.game.onGameResize(constraints.biggest.toVector2()); return FutureBuilder( - future: _gameLoaderFutureCache, + future: _loaderFuture, builder: (_, snapshot) { if (snapshot.hasError) { final errorBuilder = widget.errorBuilder; diff --git a/packages/flame/lib/src/game/game_widget/gestures.dart b/packages/flame/lib/src/game/game_widget/gestures.dart index a35c0f811..483d3088b 100644 --- a/packages/flame/lib/src/game/game_widget/gestures.dart +++ b/packages/flame/lib/src/game/game_widget/gestures.dart @@ -7,7 +7,7 @@ import '../../components/mixins/draggable.dart'; import '../../extensions/offset.dart'; import '../../gestures/detectors.dart'; import '../../gestures/events.dart'; -import '../game.dart'; +import '../mixins/game.dart'; bool hasBasicGestureDetectors(Game game) => game is TapDetector || diff --git a/packages/flame/lib/src/fps_counter.dart b/packages/flame/lib/src/game/mixins/fps_counter.dart similarity index 95% rename from packages/flame/lib/src/fps_counter.dart rename to packages/flame/lib/src/game/mixins/fps_counter.dart index 5d2f1a65b..916af6f6d 100644 --- a/packages/flame/lib/src/fps_counter.dart +++ b/packages/flame/lib/src/game/mixins/fps_counter.dart @@ -1,6 +1,6 @@ import 'package:flutter/scheduler.dart'; -import 'game/game.dart'; +import '../../../game.dart'; const _maxFrames = 60; const frameInterval = diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/mixins/game.dart similarity index 76% rename from packages/flame/lib/src/game/game.dart rename to packages/flame/lib/src/game/mixins/game.dart index 45eeda897..f54ea8089 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/mixins/game.dart @@ -6,24 +6,36 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -import '../assets/assets_cache.dart'; -import '../assets/images.dart'; -import '../extensions/offset.dart'; -import '../extensions/vector2.dart'; -import '../sprite.dart'; -import '../sprite_animation.dart'; -import 'game_render_box.dart'; -import 'projector.dart'; +import '../../../components.dart'; +import '../../assets/assets_cache.dart'; +import '../../assets/images.dart'; +import '../../extensions/offset.dart'; +import '../../extensions/vector2.dart'; +import '../../sprite.dart'; +import '../../sprite_animation.dart'; +import '../game_render_box.dart'; +import '../projector.dart'; +import 'loadable.dart'; -/// Represents a generic game. +/// This gives access to a low-level game API, to not build everything from a +/// low level `FlameGame` should be used. /// -/// Subclass this to implement the [update] and [render] methods. -/// Flame will deal with calling these methods properly when the game's widget is rendered. -abstract class Game extends Projector { +/// Add this mixin to your game class and implement the [update] and [render] +/// methods to use it in a `GameWidget`. +/// Flame will deal with calling these methods properly when the game's widget +/// is rendered. +mixin Game on Loadable implements Projector { final images = Images(); final assets = AssetsCache(); - /// Just a reference back to the render box that is kept up to date by the engine. + /// This should update the state of the game. + void update(double dt); + + /// This should render the game. + void render(Canvas canvas); + + /// Just a reference back to the render box that is kept up to date by the + /// engine. GameRenderBox? _gameRenderBox; /// Currently attached build context. Can be null if not attached. @@ -32,40 +44,39 @@ abstract class Game extends Projector { /// Whether the game widget was attached to the Flutter tree. bool get isAttached => buildContext != null; - /// Current size of the game as provided by the framework; it will be null if layout has not been computed yet. + /// Current size of the game as provided by the framework; it will be null if + /// layout has not been computed yet. /// /// Use [size] and [hasLayout] for safe access. Vector2? _size; - /// Current game viewport size, updated every resize via the [onResize] method hook + /// Current game viewport size, updated every resize via the [onGameResize] + /// method hook. Vector2 get size { assertHasLayout(); return _size!; } - /// Indicates if the this game instance had its layout layed into the GameWidget - /// Only this is true, the game is ready to have its size used or in the case - /// of a BaseGame, to receive components. + /// Indicates if this game instance is connected to a GameWidget that is live + /// in the flutter widget tree. + /// Once this is true, the game is ready to have its size used or in the case + /// of a FlameGame, to receive components. bool get hasLayout => _size != null; /// Returns the game background color. /// By default it will return a black color. - /// It cannot be changed at runtime, because the game widget does not get rebuild when this value changes. + /// It cannot be changed at runtime, because the game widget does not get + /// rebuild when this value changes. Color backgroundColor() => const Color(0xFF000000); - /// Implement this method to update the game state, given the time [dt] that has passed since the last update. - /// - /// Keep the updates as short as possible. [dt] is in seconds, with microseconds precision. - void update(double dt); - - /// Implement this method to render the current game state in the [canvas]. - void render(Canvas canvas); - - /// This is the resize hook; every time the game widget is resized, this hook is called. + /// This is the resize hook; every time the game widget is resized, this hook + /// is called. /// /// The default implementation just sets the new size on the size field + @override @mustCallSuper - void onResize(Vector2 size) { + void onGameResize(Vector2 size) { + super.onGameResize(size); _size = (_size ?? Vector2.zero())..setFrom(size); } @@ -73,11 +84,13 @@ abstract class Game extends Projector { void assertHasLayout() { assert( hasLayout, - '"size" is not ready yet. Did you try to access it on the Game constructor? Use the "onLoad" method instead.', + '"size" is not ready yet. Did you try to access it on the Game ' + 'constructor? Use the "onLoad" method instead.', ); } - /// This is the lifecycle state change hook; every time the game is resumed, paused or suspended, this is called. + /// This is the lifecycle state change hook; every time the game is resumed, + /// paused or suspended, this is called. /// /// The default implementation does nothing; override to use the hook. /// Check [AppLifecycleState] for details about the events received. @@ -86,9 +99,9 @@ abstract class Game extends Projector { /// Use for calculating the FPS. void onTimingsCallback(List timings) {} - /// Marks game as not attached tto any widget tree. + /// Marks game as attached to any Flutter widget tree. /// - /// Should be called manually. + /// Should not be called manually. void attach(PipelineOwner owner, GameRenderBox gameRenderBox) { if (isAttached) { throw UnsupportedError(''' @@ -97,26 +110,14 @@ abstract class Game extends Projector { '''); } _gameRenderBox = gameRenderBox; - onAttach(); } - // Called when the Game widget is attached - @mustCallSuper - void onAttach() {} - - /// Marks game as not attached tto any widget tree. + /// Marks game as no longer attached to any Flutter widget tree. /// /// Should not be called manually. void detach() { _gameRenderBox = null; _size = null; - onDetach(); - } - - // Called when the Game widget is detached - @mustCallSuper - void onDetach() { - images.clearCache(); } /// Converts a global coordinate (i.e. w.r.t. the app itself) to a local @@ -153,7 +154,8 @@ abstract class Game extends Projector { @override Vector2 scaleVector(Vector2 vector) => vector; - /// Utility method to load and cache the image for a sprite based on its options + /// Utility method to load and cache the image for a sprite based on its + /// options. Future loadSprite( String path, { Vector2? srcSize, @@ -167,7 +169,8 @@ abstract class Game extends Projector { ); } - /// Utility method to load and cache the image for a sprite animation based on its options + /// Utility method to load and cache the image for a sprite animation based on + /// its options. Future loadSpriteAnimation( String path, SpriteAnimationData data, @@ -179,25 +182,24 @@ abstract class Game extends Projector { ); } - /// Flag to tell the game loop if it should start running upon creation + /// Flag to tell the game loop if it should start running upon creation. bool runOnCreation = true; - /// Pauses the engine game loop execution + /// Pauses the engine game loop execution. void pauseEngine() => pauseEngineFn?.call(); - /// Resumes the engine game loop execution + /// Resumes the engine game loop execution. void resumeEngine() => resumeEngineFn?.call(); VoidCallback? pauseEngineFn; VoidCallback? resumeEngineFn; - /// Use this method to load the assets need for the game instance to run - Future onLoad() async {} - /// A property that stores an [ActiveOverlaysNotifier] /// - /// This is useful to render widgets above a game, like a pause menu for example. - /// Overlays visible or hidden via [overlays].add or [overlays].remove, respectively. + /// This is useful to render widgets above a game, like a pause menu for + /// example. + /// Overlays visible or hidden via [overlays].add or [overlays].remove, + /// respectively. /// /// Ex: /// ``` @@ -217,7 +219,8 @@ abstract class Game extends Projector { final mouseCursor = ValueNotifier(null); } -/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. +/// A [ChangeNotifier] used to control the visibility of overlays on a [Game] +/// instance. /// /// To learn more, see: /// - [Game.overlays] diff --git a/packages/flame/lib/src/game/mixins/keyboard.dart b/packages/flame/lib/src/game/mixins/keyboard.dart index f1c068f4f..1187786b9 100644 --- a/packages/flame/lib/src/game/mixins/keyboard.dart +++ b/packages/flame/lib/src/game/mixins/keyboard.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + import '../../../components.dart'; import '../../../game.dart'; +import 'game.dart'; -/// A [BaseComponent] mixin to add keyboard handling capability to components. +/// A [Component] mixin to add keyboard handling capability to components. /// Must be used in components that can only be added to games that are mixed /// with [HasKeyboardHandlerComponents]. -mixin KeyboardHandler on BaseComponent { +mixin KeyboardHandler on Component { bool onKeyEvent( RawKeyEvent event, Set keysPressed, @@ -15,22 +17,20 @@ mixin KeyboardHandler on BaseComponent { } } -/// A [BaseGame] mixin that implements [KeyboardEvents] with keyboard event +/// A [FlameGame] mixin that implements [KeyboardEvents] with keyboard event /// propagation to components that are mixed with [KeyboardHandler]. /// /// Attention: should not be used alongside [KeyboardEvents] in a game subclass. /// Using this mixin remove the necessity of [KeyboardEvents]. -mixin HasKeyboardHandlerComponents on BaseGame implements KeyboardEvents { +mixin HasKeyboardHandlerComponents on FlameGame implements KeyboardEvents { bool _handleKeyboardEvent( bool Function(KeyboardHandler child) keyboardEventHandler, ) { var shouldContinue = true; - for (final c in components.toList().reversed) { - if (c is BaseComponent) { - shouldContinue = c.propagateToChildren( - keyboardEventHandler, - ); - } + for (final c in children.toList().reversed) { + shouldContinue = c.propagateToChildren( + keyboardEventHandler, + ); if (c is KeyboardHandler && shouldContinue) { shouldContinue = keyboardEventHandler(c); } diff --git a/packages/flame/lib/src/game/mixins/loadable.dart b/packages/flame/lib/src/game/mixins/loadable.dart new file mode 100644 index 000000000..cf9047ab6 --- /dev/null +++ b/packages/flame/lib/src/game/mixins/loadable.dart @@ -0,0 +1,66 @@ +import 'package:meta/meta.dart'; + +import '../../../game.dart'; + +/// From an end-user perspective this mixin is usually used together with [Game] +/// to create a game class which is more low-level than the [FlameGame]. +/// +/// What it provides in practice is a cache for [onLoad], so that a +/// component/class can be certain that [onLoad] only runs once which then gives +/// the possibility to do late initializations in [onLoad]. +/// +/// It also provides empty implementations of [onMount] and [onRemove] which are +/// called when the implementing class/component is added or removed from a +/// parent/widget, in that respective order. +mixin Loadable { + /// This receives the new bounding size from its parent, which could be for + /// example a [GameWidget] or a `Component`. + void onGameResize(Vector2 size) {} + + /// Whenever [onLoad] returns something, the parent will wait for the [Future] + /// to be resolved before adding it. + /// If `null` is returned, the class is added right away. + /// + /// The default implementation just returns null. + /// + /// This can be overwritten to add custom logic to the component loading. + /// + /// Example: + /// ```dart + /// @override + /// Future onLoad() async { + /// myImage = await gameRef.load('my_image.png'); + /// } + /// ``` + Future? onLoad() => null; + + Future? _onLoadCache; + + /// Since [onLoad] only should run once throughout a the lifetime of the + /// implementing class, it is cached so that it can be reused when the parent + /// component/game/widget changes. + @internal + Future? get onLoadCache => _onLoadCache ?? (_onLoadCache = onLoad()); + + /// Called after the component has successfully run [onLoad] and before the + /// component is added to its new parent. + /// + /// Whenever [onMount] returns something, the parent will wait for the + /// [Future] to be resolved before adding it. + /// If `null` is returned, the class is added right away. + /// + /// This can be overwritten to add custom logic to the component's mounting. + /// + /// Example: + /// ```dart + /// @override + /// void onMount() { + /// position = parent!.size / 2; + /// } + /// ``` + void onMount() {} + + /// Called when the class is removed from its parent. + /// The parent could be for example a [GameWidget] or a `Component`. + void onRemove() {} +} diff --git a/packages/flame/lib/src/gestures/detectors.dart b/packages/flame/lib/src/gestures/detectors.dart index 746c81f5f..e6fe51369 100644 --- a/packages/flame/lib/src/gestures/detectors.dart +++ b/packages/flame/lib/src/gestures/detectors.dart @@ -1,4 +1,4 @@ -import '../game/game.dart'; +import '../game/mixins/game.dart'; import 'events.dart'; mixin MultiTouchTapDetector on Game { diff --git a/packages/flame/lib/src/gestures/events.dart b/packages/flame/lib/src/gestures/events.dart index 7c0b49a8a..1c7ffecda 100644 --- a/packages/flame/lib/src/gestures/events.dart +++ b/packages/flame/lib/src/gestures/events.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import '../../extensions.dart'; -import '../game/game.dart'; +import '../game/mixins/game.dart'; /// [EventPosition] converts position based events to three different coordinate systems (global, local and game). /// diff --git a/packages/flame/lib/src/parallax.dart b/packages/flame/lib/src/parallax.dart index 75af034a6..b6be5feb4 100644 --- a/packages/flame/lib/src/parallax.dart +++ b/packages/flame/lib/src/parallax.dart @@ -3,13 +3,13 @@ import 'dart:ui'; import 'package:flutter/painting.dart'; +import '../game.dart'; import 'assets/images.dart'; import 'extensions/canvas.dart'; import 'extensions/image.dart'; import 'extensions/rect.dart'; import 'extensions/vector2.dart'; import 'flame.dart'; -import 'game/game.dart'; import 'sprite_animation.dart'; extension ParallaxExtension on Game { diff --git a/packages/flame/lib/src/particles/particle.dart b/packages/flame/lib/src/particles/particle.dart index aafe67585..d8fec3ef9 100644 --- a/packages/flame/lib/src/particles/particle.dart +++ b/packages/flame/lib/src/particles/particle.dart @@ -4,8 +4,6 @@ import 'dart:ui'; import 'package:flutter/animation.dart'; import '../../extensions.dart'; -import '../components/component.dart'; -import '../components/particle_component.dart'; import '../timer.dart'; import 'accelerated_particle.dart'; import 'composed_particle.dart'; @@ -174,9 +172,4 @@ abstract class Particle { Particle scaled(double scale) { return ScaledParticle(scale: scale, child: this, lifespan: _lifespan); } - - /// Wraps this particle with a [ParticleComponent]. - /// - /// Should be used with the FCS. - Component asComponent() => ParticleComponent(particle: this); } diff --git a/packages/flame/lib/src/sprite_animation.dart b/packages/flame/lib/src/sprite_animation.dart index 855256741..4403295c8 100644 --- a/packages/flame/lib/src/sprite_animation.dart +++ b/packages/flame/lib/src/sprite_animation.dart @@ -96,7 +96,8 @@ class SpriteAnimationFrame { typedef OnCompleteSpriteAnimation = void Function(); -/// Represents a sprite animation, that is, a list of sprites that change with time. +/// Represents a sprite animation, that is, a list of sprites that change with +/// time. class SpriteAnimation { /// The frames that compose this animation. List frames = []; @@ -104,7 +105,8 @@ class SpriteAnimation { /// Index of the current frame that should be displayed. int currentIndex = 0; - /// Current clock time (total time) of this animation, in seconds, since last frame. + /// Current clock time (total time) of this animation, in seconds, since last + /// frame. /// /// It's ticked by the update method. It's reset every frame change. double clock = 0.0; @@ -112,7 +114,8 @@ class SpriteAnimation { /// Total elapsed time of this animation, in seconds, since start or a reset. double elapsed = 0.0; - /// Whether the animation loops after the last sprite of the list, going back to the first, or keeps returning the last when done. + /// Whether the animation loops after the last sprite of the list, going back + /// to the first, or keeps returning the last when done. bool loop = true; /// Registered method to be triggered when the animation complete. diff --git a/packages/flame/lib/src/sprite_batch.dart b/packages/flame/lib/src/sprite_batch.dart index 3d512eab3..070e678bd 100644 --- a/packages/flame/lib/src/sprite_batch.dart +++ b/packages/flame/lib/src/sprite_batch.dart @@ -3,11 +3,11 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; +import '../game.dart'; import 'assets/images.dart'; import 'extensions/image.dart'; import 'extensions/vector2.dart'; import 'flame.dart'; -import 'game/game.dart'; extension SpriteBatchExtension on Game { /// Utility method to load and cache the image for a [SpriteBatch] based on its options diff --git a/packages/flame/lib/src/timer.dart b/packages/flame/lib/src/timer.dart index f3611d6bb..b9d8c2e52 100644 --- a/packages/flame/lib/src/timer.dart +++ b/packages/flame/lib/src/timer.dart @@ -66,7 +66,7 @@ class Timer { } /// Simple component which wraps a [Timer] instance allowing it to be easily -/// used inside a BaseGame game. +/// used inside a FlameGame game. class TimerComponent extends Component { Timer timer; diff --git a/packages/flame/pubspec.yaml b/packages/flame/pubspec.yaml index d66fcd73d..e1e9a25a4 100644 --- a/packages/flame/pubspec.yaml +++ b/packages/flame/pubspec.yaml @@ -7,13 +7,13 @@ dependencies: flutter: sdk: flutter meta: ^1.3.0 - ordered_set: ^3.1.0 + ordered_set: ^3.2.0 vector_math: '>=2.1.0 <3.0.0' dev_dependencies: flutter_test: sdk: flutter - test: ^1.16.0 + test: ^1.17.10 dart_code_metrics: ^4.1.0 dartdoc: ^0.42.0 mocktail: ^0.1.4 diff --git a/packages/flame/test/components/collidable_type_test.dart b/packages/flame/test/components/collidable_type_test.dart index fbac9c8d5..8606ee387 100644 --- a/packages/flame/test/components/collidable_type_test.dart +++ b/packages/flame/test/components/collidable_type_test.dart @@ -5,9 +5,9 @@ import 'package:flame/geometry.dart'; import 'package:test/test.dart'; import 'package:vector_math/vector_math_64.dart'; -class TestGame extends BaseGame with HasCollidables { +class TestGame extends FlameGame with HasCollidables { TestGame() { - onResize(Vector2.all(200)); + onGameResize(Vector2.all(200)); } } @@ -46,18 +46,19 @@ class TestBlock extends PositionComponent with Hitbox, Collidable { } void main() { - TestGame gameWithCollidables(List collidables) { + Future gameWithCollidables(List collidables) async { final game = TestGame(); - game.addAll(collidables); + await game.onLoad(); + await game.addAll(collidables); game.update(0); - expect(game.components.isNotEmpty, collidables.isNotEmpty); + expect(game.children.isNotEmpty, collidables.isNotEmpty); return game; } group( 'Varying CollisionType tests', () { - test('Actives do collide', () { + test('Actives do collide', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -68,14 +69,14 @@ void main() { Vector2.all(10), CollidableType.active, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collidedWith(blockB), true); expect(blockB.collidedWith(blockA), true); expect(blockA.collisions.length, 1); expect(blockB.collisions.length, 1); }); - test('Sensors do not collide', () { + test('Sensors do not collide', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -86,12 +87,12 @@ void main() { Vector2.all(10), CollidableType.passive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.isEmpty, true); expect(blockB.collisions.isEmpty, true); }); - test('Inactives do not collide', () { + test('Inactives do not collide', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -102,12 +103,12 @@ void main() { Vector2.all(10), CollidableType.inactive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.isEmpty, true); expect(blockB.collisions.isEmpty, true); }); - test('Active collides with static', () { + test('Active collides with static', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -118,14 +119,14 @@ void main() { Vector2.all(10), CollidableType.passive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collidedWith(blockB), true); expect(blockB.collidedWith(blockA), true); expect(blockA.collisions.length, 1); expect(blockB.collisions.length, 1); }); - test('Sensor collides with active', () { + test('Sensor collides with active', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -136,14 +137,14 @@ void main() { Vector2.all(10), CollidableType.active, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collidedWith(blockB), true); expect(blockB.collidedWith(blockA), true); expect(blockA.collisions.length, 1); expect(blockB.collisions.length, 1); }); - test('Sensor does not collide with inactive', () { + test('Sensor does not collide with inactive', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -154,12 +155,12 @@ void main() { Vector2.all(10), CollidableType.inactive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.length, 0); expect(blockB.collisions.length, 0); }); - test('Inactive does not collide with static', () { + test('Inactive does not collide with static', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -170,12 +171,12 @@ void main() { Vector2.all(10), CollidableType.passive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.length, 0); expect(blockB.collisions.length, 0); }); - test('Active does not collide with inactive', () { + test('Active does not collide with inactive', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -186,12 +187,12 @@ void main() { Vector2.all(10), CollidableType.inactive, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.length, 0); expect(blockB.collisions.length, 0); }); - test('Inactive does not collide with active', () { + test('Inactive does not collide with active', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -202,12 +203,12 @@ void main() { Vector2.all(10), CollidableType.active, ); - gameWithCollidables([blockA, blockB]); + await gameWithCollidables([blockA, blockB]); expect(blockA.collisions.length, 0); expect(blockB.collisions.length, 0); }); - test('Correct collisions with many involved collidables', () { + test('Correct collisions with many involved collidables', () async { final actives = List.generate( 100, (_) => TestBlock( @@ -232,7 +233,7 @@ void main() { CollidableType.inactive, ), ); - gameWithCollidables((actives + statics + inactives)..shuffle()); + await gameWithCollidables((actives + statics + inactives)..shuffle()); expect( actives.fold( true, @@ -256,7 +257,8 @@ void main() { true, ); }); - test('Detects collision after scale', () { + + test('Detects collision after scale', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -267,7 +269,7 @@ void main() { Vector2.all(10), CollidableType.active, ); - final game = gameWithCollidables([blockA, blockB]); + final game = await gameWithCollidables([blockA, blockB]); expect(blockA.collidedWith(blockB), false); expect(blockB.collidedWith(blockA), false); expect(blockA.collisions.length, 0); @@ -279,13 +281,14 @@ void main() { expect(blockA.collisions.length, 1); expect(blockB.collisions.length, 1); }); - test('TestPoint detects point after scale', () { + + test('TestPoint detects point after scale', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), CollidableType.active, ); - final game = gameWithCollidables([blockA]); + final game = await gameWithCollidables([blockA]); expect(blockA.containsPoint(Vector2.all(11)), false); blockA.scale = Vector2.all(2.0); game.update(0); diff --git a/packages/flame/test/components/collision_callback_test.dart b/packages/flame/test/components/collision_callback_test.dart index d5d87156b..8a3f222d0 100644 --- a/packages/flame/test/components/collision_callback_test.dart +++ b/packages/flame/test/components/collision_callback_test.dart @@ -5,9 +5,9 @@ import 'package:flame/geometry.dart'; import 'package:test/test.dart'; import 'package:vector_math/vector_math_64.dart'; -class TestGame extends BaseGame with HasCollidables { +class TestGame extends FlameGame with HasCollidables { TestGame() { - onResize(Vector2.all(200)); + onGameResize(Vector2.all(200)); } } @@ -58,18 +58,19 @@ class TestBlock extends PositionComponent with Hitbox, Collidable { } void main() { - TestGame gameWithCollidables(List collidables) { + Future gameWithCollidables(List collidables) async { final game = TestGame(); - game.addAll(collidables); + await game.onLoad(); + await game.addAll(collidables); game.update(0); - expect(game.components.isNotEmpty, collidables.isNotEmpty); + expect(game.children.isNotEmpty, collidables.isNotEmpty); return game; } group( 'Collision callbacks are called properly', () { - test('collidable callbacks are called', () { + test('collidable callbacks are called', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -78,7 +79,7 @@ void main() { Vector2.all(1), Vector2.all(10), ); - final game = gameWithCollidables([blockA, blockB]); + final game = await gameWithCollidables([blockA, blockB]); expect(blockA.hasCollisionWith(blockB), true); expect(blockB.hasCollisionWith(blockA), true); expect(blockA.collisions.length, 1); @@ -96,7 +97,7 @@ void main() { expect(blockB.endCounter, 1); }); - test('hitbox callbacks are called', () { + test('hitbox callbacks are called', () async { final blockA = TestBlock( Vector2.zero(), Vector2.all(10), @@ -107,7 +108,7 @@ void main() { ); final hitboxA = blockA.hitbox; final hitboxB = blockB.hitbox; - final game = gameWithCollidables([blockA, blockB]); + final game = await gameWithCollidables([blockA, blockB]); expect(hitboxA.hasCollisionWith(hitboxB), true); expect(hitboxB.hasCollisionWith(hitboxA), true); expect(hitboxA.collisions.length, 1); diff --git a/packages/flame/test/components/component_lifecycle_test.dart b/packages/flame/test/components/component_lifecycle_test.dart new file mode 100644 index 000000000..85cbcbabb --- /dev/null +++ b/packages/flame/test/components/component_lifecycle_test.dart @@ -0,0 +1,115 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MyComponent extends Component { + final List events; + + MyComponent(this.events); + + @override + void prepare(Component parent) { + super.prepare(parent); + events.add('prepared: $isPrepared'); + } + + @override + Future onLoad() async { + await super.onLoad(); + events.add('onLoad'); + } + + @override + void onMount() { + events.add('onMount'); + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + events.add('onGameResize'); + } +} + +void main() { + group('Component - Lifecycle', () { + test('Lifecycle in correct order', () async { + final events = []; + final game = FlameGame(); + game.onGameResize(Vector2.zero()); + await game.add(MyComponent(events)); + + expect( + events, + ['onGameResize', 'prepared: true', 'onLoad', 'onMount'], + ); + }); + + test('Parent prepares the component', () async { + final parentEvents = []; + final childEvents = []; + final game = FlameGame(); + game.onGameResize(Vector2.zero()); + final parent = MyComponent(parentEvents); + await parent.add(MyComponent(childEvents)); + await game.add(parent); + + // The parent tries to prepare the component before it is added to the + // game and fails since it doesn't have a game root and therefore re-adds + // the child when it has a proper root. + expect( + parentEvents, + ['onGameResize', 'prepared: true', 'onLoad', 'onMount'], + ); + expect( + childEvents, + [ + 'prepared: false', + 'onGameResize', + 'prepared: true', + 'onLoad', + 'onMount', + ], + ); + }); + + test('Correct lifecycle on parent change', () async { + final parentEvents = []; + final childEvents = []; + final game = FlameGame(); + game.onGameResize(Vector2.zero()); + final parent = MyComponent(parentEvents); + final child = MyComponent(childEvents); + await parent.add(child); + await game.add(parent); + game.update(0); + child.changeParent(game); + game.update(0); + + expect( + parentEvents, + [ + 'onGameResize', + 'prepared: true', + 'onLoad', + 'onMount', + ], + ); + // onLoad should only be called the first time that the component is + // loaded. + expect( + childEvents, + [ + 'prepared: false', + 'onGameResize', + 'prepared: true', + 'onLoad', + 'onMount', + 'onGameResize', + 'prepared: true', + 'onMount', + ], + ); + }); + }); +} diff --git a/packages/flame/test/components/component_test.dart b/packages/flame/test/components/component_test.dart index 820a3814e..e25713289 100644 --- a/packages/flame/test/components/component_test.dart +++ b/packages/flame/test/components/component_test.dart @@ -4,7 +4,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:test/test.dart'; -class RemoveComponent extends BaseComponent { +class RemoveComponent extends Component { int removeCounter = 0; @override @@ -86,12 +86,12 @@ void main() { test('test remove and shouldRemove', () { final c1 = SpriteComponent(); expect(c1.shouldRemove, equals(false)); - c1.remove(); + c1.removeFromParent(); expect(c1.shouldRemove, equals(true)); final c2 = SpriteAnimationComponent(); expect(c2.shouldRemove, equals(false)); - c2.remove(); + c2.removeFromParent(); expect(c2.shouldRemove, equals(true)); c2.shouldRemove = false; @@ -99,12 +99,12 @@ void main() { }); test('remove and re-add should not double trigger onRemove', () { - final game = BaseGame()..onResize(Vector2.zero()); + final game = FlameGame()..onGameResize(Vector2.zero()); final component = RemoveComponent(); game.add(component); game.update(0); - component.remove(); + component.removeFromParent(); game.update(0); expect(component.removeCounter, 1); component.shouldRemove = false; @@ -112,7 +112,7 @@ void main() { game.add(component); game.update(0); expect(component.removeCounter, 0); - expect(game.components.length, 1); + expect(game.children.length, 1); }); }); } diff --git a/packages/flame/test/components/composed_component_test.dart b/packages/flame/test/components/composed_component_test.dart index f8b81c78c..b2811953c 100644 --- a/packages/flame/test/components/composed_component_test.dart +++ b/packages/flame/test/components/composed_component_test.dart @@ -5,7 +5,11 @@ import 'package:flame/game.dart'; import 'package:flame/test.dart'; import 'package:test/test.dart'; -class MyGame extends BaseGame with HasTappableComponents {} +class MyGame extends FlameGame with HasTappableComponents { + MyGame() : super() { + onGameResize(Vector2.zero()); + } +} class MyTap extends PositionComponent with Tappable { late Vector2 gameSize; @@ -42,42 +46,56 @@ class MyTap extends PositionComponent with Tappable { class MyAsyncChild extends MyTap { @override - Future onLoad() => Future.value(); + Future onLoad() async { + await super.onLoad(); + return Future.value(); + } } class MyComposed extends PositionComponent with HasGameRef, Tappable {} -class MySimpleComposed extends BaseComponent with HasGameRef, Tappable {} +class MySimpleComposed extends Component with HasGameRef, Tappable {} // composed w/o HasGameRef -class PlainComposed extends BaseComponent {} +class PlainComposed extends Component {} Vector2 size = Vector2.all(300); void main() { group('composable component test', () { - test('adds the child to the component', () { + test('child is not added until the component is prepared', () async { final child = MyTap(); final wrapper = MyComposed(); - wrapper.addChild(child); + await wrapper.add(child); + + expect(child.isPrepared, false); + expect(child.isLoaded, false); + expect(wrapper.contains(child), false); + + final game = MyGame(); + await game.add(wrapper); wrapper.update(0); // children are only added on the next tick - expect(wrapper.containsChild(child), true); + expect(child.isPrepared, true); + expect(child.isLoaded, true); + expect(wrapper.contains(child), true); }); - test('removes the child from the component', () { + test('removes the child from the component', () async { final child = MyTap(); final wrapper = MyComposed(); + final game = MyGame(); + await game.add(wrapper); - wrapper.addChild(child); - expect(wrapper.containsChild(child), false); + await wrapper.add(child); + expect(wrapper.contains(child), false); wrapper.update(0); // children are only added on the next tick - expect(wrapper.containsChild(child), true); + expect(wrapper.contains(child), true); - wrapper.children.remove(child); - expect(wrapper.containsChild(child), true); + wrapper.remove(child); + expect(wrapper.contains(child), true); wrapper.update(0); // children are only removed on the next tick - expect(wrapper.containsChild(child), false); + expect(wrapper.contains(child), false); }); test( @@ -85,13 +103,15 @@ void main() { () async { final child = MyAsyncChild(); final wrapper = MyComposed(); + final game = MyGame(); + await game.add(wrapper); - final future = wrapper.addChild(child); - expect(wrapper.containsChild(child), false); + final future = wrapper.add(child); + expect(wrapper.contains(child), false); await future; - expect(wrapper.containsChild(child), false); + expect(wrapper.contains(child), false); wrapper.update(0); - expect(wrapper.containsChild(child), true); + expect(wrapper.contains(child), true); }, ); @@ -100,10 +120,10 @@ void main() { final child = MyTap(); final wrapper = MyComposed(); - game.onResize(size); + game.onGameResize(size); child.size.setValues(1.0, 1.0); game.add(wrapper); - wrapper.addChild(child); + wrapper.add(child); game.update(0.0); game.onTapDown(1, createTapDownEvent(game)); @@ -111,14 +131,14 @@ void main() { expect(child.tapped, true); }); - test('add multiple children with addChildren', () { + test('add multiple children with addAll', () async { final game = MyGame(); final children = List.generate(10, (_) => MyTap()); final wrapper = MyComposed(); - wrapper.children.addChildren(children); + await wrapper.addAll(children); - game.onResize(size); - game.add(wrapper); + game.onGameResize(size); + await game.add(wrapper); game.update(0.0); expect(wrapper.children.length, children.length); }); @@ -132,9 +152,9 @@ void main() { ..position.setFrom(Vector2.all(100)) ..size.setFrom(Vector2.all(300)); - game.onResize(size); + game.onGameResize(size); game.add(wrapper); - wrapper.addChild(child); + wrapper.add(child); game.update(0.0); game.onTapDown( 1, @@ -149,14 +169,13 @@ void main() { expect(child.tapTimes, 1); }); - test('updates and renders children', () { + test('updates and renders children', () async { final game = MyGame(); - game.onResize(Vector2.all(100)); final child = MyTap(); final wrapper = MyComposed(); - wrapper.addChild(child); - game.add(wrapper); + wrapper.add(child); + await game.add(wrapper); game.update(0.0); game.render(MockCanvas()); @@ -164,52 +183,32 @@ void main() { expect(child.updated, true); }); - test('initially same debugMode as parent', () { + test('initially same debugMode as parent', () async { final game = MyGame(); - game.onResize(Vector2.all(100)); final child = MyTap(); final wrapper = MyComposed(); wrapper.debugMode = true; - wrapper.addChild(child); - game.add(wrapper); + await wrapper.add(child); + await game.add(wrapper); game.update(0.0); expect(child.debugMode, true); wrapper.debugMode = false; expect(child.debugMode, true); }); - test('initially same debugMode as parent when BaseComponent', () { + + test('initially same debugMode as parent when Component', () async { final game = MyGame(); - game.onResize(Vector2.all(100)); final child = MyTap(); final wrapper = MySimpleComposed(); wrapper.debugMode = true; - wrapper.addChild(child); - game.add(wrapper); + wrapper.add(child); + await game.add(wrapper); game.update(0.0); expect(child.debugMode, true); }); - test('fail to add child if no gameRef can be acquired', () { - final game = MyGame(); - game.onResize(Vector2.all(100)); - - final parent = PlainComposed(); - - // this is ok; when the parent is added to the game the children will - // get mounted - parent.addChild(MyTap()); - - game.add(parent); - game.update(0); - - // this is not ok, the child would never be mounted! - expect( - () => parent.addChild(MyTap()), - throwsA(isA()), - ); - }); }); } diff --git a/packages/flame/test/components/draggable_test.dart b/packages/flame/test/components/draggable_test.dart index 55228c98f..ca8a40b48 100644 --- a/packages/flame/test/components/draggable_test.dart +++ b/packages/flame/test/components/draggable_test.dart @@ -4,9 +4,9 @@ import 'package:flame/input.dart'; import 'package:flutter/gestures.dart'; import 'package:test/test.dart'; -class _GameWithDraggables extends BaseGame with HasDraggableComponents {} +class _GameWithDraggables extends FlameGame with HasDraggableComponents {} -class _GameWithoutDraggables extends BaseGame {} +class _GameWithoutDraggables extends FlameGame {} class DraggableComponent extends PositionComponent with Draggable { bool hasStartedDragging = false; @@ -22,12 +22,12 @@ void main() { group('draggables test', () { test('make sure they cannot be added to invalid games', () async { final game1 = _GameWithDraggables(); - game1.onResize(Vector2.all(100)); + game1.onGameResize(Vector2.all(100)); // should be ok await game1.add(DraggableComponent()); final game2 = _GameWithoutDraggables(); - game2.onResize(Vector2.all(100)); + game2.onGameResize(Vector2.all(100)); expect( () => game2.add(DraggableComponent()), @@ -37,7 +37,7 @@ void main() { test('can be dragged', () async { final game = _GameWithDraggables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final component = DraggableComponent() ..x = 10 ..y = 10 @@ -62,7 +62,7 @@ void main() { test('when the game has camera zoom, can be dragged', () async { final game = _GameWithDraggables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final component = DraggableComponent() ..x = 10 ..y = 10 @@ -88,7 +88,7 @@ void main() { test('when the game has a moved camera, dragging works', () async { final game = _GameWithDraggables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final component = DraggableComponent() ..x = 50 ..y = 50 @@ -116,7 +116,7 @@ void main() { test('isDragged is changed', () async { final game = _GameWithDraggables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final component = DraggableComponent() ..x = 10 ..y = 10 diff --git a/packages/flame/test/components/has_game_ref_test.dart b/packages/flame/test/components/has_game_ref_test.dart index d81066169..1ef72ebe0 100644 --- a/packages/flame/test/components/has_game_ref_test.dart +++ b/packages/flame/test/components/has_game_ref_test.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:test/test.dart'; -class MyGame extends BaseGame { +class MyGame extends FlameGame { bool calledFoo = false; void foo() { calledFoo = true; @@ -20,7 +20,7 @@ void main() { test('simple test', () { final c = MyComponent(); final game = MyGame(); - game.onResize(Vector2.all(200)); + game.onGameResize(Vector2.all(200)); game.add(c); c.foo(); expect(game.calledFoo, true); diff --git a/packages/flame/test/components/hoverable_test.dart b/packages/flame/test/components/hoverable_test.dart index 10e2e4324..e38a475c2 100644 --- a/packages/flame/test/components/hoverable_test.dart +++ b/packages/flame/test/components/hoverable_test.dart @@ -7,9 +7,9 @@ import 'package:flame/src/gestures/events.dart'; import 'package:flutter/gestures.dart' show PointerHoverEvent; import 'package:test/test.dart'; -class _GameWithHoverables extends BaseGame with HasHoverableComponents {} +class _GameWithHoverables extends FlameGame with HasHoverableComponents {} -class _GameWithoutHoverables extends BaseGame {} +class _GameWithoutHoverables extends FlameGame {} class HoverableComponent extends PositionComponent with Hoverable { int enterCount = 0; @@ -30,12 +30,12 @@ void main() { group('hoverable test', () { test('make sure they cannot be added to invalid games', () async { final game1 = _GameWithHoverables(); - game1.onResize(Vector2.all(100)); + game1.onGameResize(Vector2.all(100)); // should be ok await game1.add(HoverableComponent()); final game2 = _GameWithoutHoverables(); - game2.onResize(Vector2.all(100)); + game2.onGameResize(Vector2.all(100)); expect( () => game2.add(HoverableComponent()), @@ -44,7 +44,7 @@ void main() { }); test('single component', () async { final game = _GameWithHoverables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final c = HoverableComponent() ..position = Vector2(10, 20) @@ -88,7 +88,7 @@ void main() { }); test('camera is respected', () async { final game = _GameWithHoverables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final c = HoverableComponent() ..position = Vector2(10, 20) @@ -110,7 +110,7 @@ void main() { }); test('multiple components', () async { final game = _GameWithHoverables(); - game.onResize(Vector2.all(100)); + game.onGameResize(Vector2.all(100)); final a = HoverableComponent() ..position = Vector2(10, 0) diff --git a/packages/flame/test/components/joystick_component_test.dart b/packages/flame/test/components/joystick_component_test.dart index 5d8f16e1c..f4f498de1 100644 --- a/packages/flame/test/components/joystick_component_test.dart +++ b/packages/flame/test/components/joystick_component_test.dart @@ -5,7 +5,7 @@ import 'package:flame/input.dart'; import 'package:flutter/widgets.dart'; import 'package:test/test.dart'; -class TestGame extends BaseGame with HasDraggableComponents {} +class TestGame extends FlameGame with HasDraggableComponents {} void main() { group('JoystickDirection tests', () { @@ -15,7 +15,7 @@ void main() { size: 20, margin: const EdgeInsets.only(left: 20, bottom: 20), ); - final game = TestGame()..onResize(Vector2.all(200)); + final game = TestGame()..onGameResize(Vector2.all(200)); game.add(joystick); game.update(0); diff --git a/packages/flame/test/components/parallax_test.dart b/packages/flame/test/components/parallax_test.dart index 934ad0d36..8bbea1d7b 100644 --- a/packages/flame/test/components/parallax_test.dart +++ b/packages/flame/test/components/parallax_test.dart @@ -2,16 +2,17 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:test/test.dart'; -class ParallaxGame extends BaseGame { +class ParallaxGame extends FlameGame { late final ParallaxComponent parallaxComponent; late final Vector2? parallaxSize; ParallaxGame({this.parallaxSize}) { - onResize(Vector2.all(500)); + onGameResize(Vector2.all(500)); } @override Future onLoad() async { + await super.onLoad(); parallaxComponent = await loadParallaxComponent( [], size: parallaxSize, diff --git a/packages/flame/test/components/position_component_test.dart b/packages/flame/test/components/position_component_test.dart index 4f9946754..bb09c60f7 100644 --- a/packages/flame/test/components/position_component_test.dart +++ b/packages/flame/test/components/position_component_test.dart @@ -8,7 +8,7 @@ import 'package:flame/src/test_helpers/mock_canvas.dart'; import 'package:flame/src/test_helpers/random_test.dart'; import 'package:test/test.dart'; -class MyBaseComponent extends BaseComponent {} +class MyComponent extends Component {} class MyHitboxComponent extends PositionComponent with Hitbox {} @@ -232,7 +232,7 @@ void main() { child.size.setValues(3.0, 1.0); child.angle = 0.0; child.anchor = Anchor.topLeft; - parent.addChild(child); + parent.add(child); expect(child.absoluteTopLeftPosition, child.position + parent.position); expect( @@ -432,7 +432,7 @@ void main() { ..size = Vector2(10, 8) ..position = Vector2(50, 20) ..anchor = const Anchor(0.1, 0.2); - parent.addChild(child); + parent.add(child); for (var i = 0; i < 100; i++) { child.angle = (rnd.nextDouble() - 0.5) * 10; @@ -509,8 +509,8 @@ void main() { final parent = PositionComponent(size: Vector2.all(100)); final comp1 = PositionComponent(position: Vector2(10, 20)); final comp2 = PositionComponent(position: Vector2(40, 60)); - parent.addChild(comp1); - parent.addChild(comp2); + parent.add(comp1); + parent.add(comp2); // The distance is the same in both directions expect(comp1.distance(comp2), 50); @@ -530,14 +530,14 @@ void main() { test('deep nested', () { final c1 = PositionComponent()..position = Vector2(10, 20); - final c2 = MyBaseComponent(); + final c2 = MyComponent(); final c3 = PositionComponent()..position = Vector2(-1, -1); - final c4 = MyBaseComponent(); + final c4 = MyComponent(); final c5 = PositionComponent()..position = Vector2(5, 0); - c1.addChild(c2); - c2.addChild(c3); - c3.addChild(c4); - c4.addChild(c5); + c1.add(c2); + c2.add(c3); + c3.add(c4); + c4.add(c5); // Verify that the absolute coordinate is computed correctly even // if the component is part of a nested tree where not all of // the components are [PositionComponent]s. @@ -548,7 +548,7 @@ void main() { final parent = PositionComponent(position: Vector2(12, 19)); final child = PositionComponent(position: Vector2(11, -1), size: Vector2(4, 6)); - parent.addChild(child); + parent.add(child); expect(child.anchor, Anchor.topLeft); expect(child.topLeftPosition, Vector2(11, -1)); diff --git a/packages/flame/test/components/priority_test.dart b/packages/flame/test/components/priority_test.dart index 21cc4f764..f4fc50673 100644 --- a/packages/flame/test/components/priority_test.dart +++ b/packages/flame/test/components/priority_test.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:test/test.dart'; -class PriorityComponent extends BaseComponent { +class PriorityComponent extends Component { PriorityComponent(int priority) : super(priority: priority); } @@ -16,10 +16,10 @@ void main() { test('components with different priorities are sorted in the list', () { final priorityComponents = List.generate(10, (i) => PriorityComponent(i)); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); + final game = FlameGame()..onGameResize(Vector2.zero()); game.addAll(priorityComponents); game.update(0); - componentsSorted(game.components); + componentsSorted(game.children); }); test('changing priority should reorder component list', () { @@ -27,8 +27,8 @@ void main() { final priorityComponents = List.generate(10, (i) => PriorityComponent(i)) ..add(firstCompopnent); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); - final components = game.components; + final game = FlameGame()..onGameResize(Vector2.zero()); + final components = game.children; game.addAll(priorityComponents); game.update(0); componentsSorted(components); @@ -40,8 +40,8 @@ void main() { test('changing priorities should reorder component list', () { final priorityComponents = List.generate(10, (i) => PriorityComponent(i)); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); - final components = game.components; + final game = FlameGame()..onGameResize(Vector2.zero()); + final components = game.children; game.addAll(priorityComponents); game.update(0); componentsSorted(components); @@ -56,9 +56,9 @@ void main() { final parentComponent = PriorityComponent(0); final priorityComponents = List.generate(10, (i) => PriorityComponent(i)); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); + final game = FlameGame()..onGameResize(Vector2.zero()); game.add(parentComponent); - parentComponent.children.addChildren(priorityComponents, gameRef: game); + parentComponent.addAll(priorityComponents); final children = parentComponent.children; game.update(0); componentsSorted(children); @@ -71,9 +71,9 @@ void main() { final parentComponent = PriorityComponent(0); final priorityComponents = List.generate(10, (i) => PriorityComponent(i)); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); + final game = FlameGame()..onGameResize(Vector2.zero()); game.add(parentComponent); - parentComponent.children.addChildren(priorityComponents, gameRef: game); + parentComponent.addAll(priorityComponents); final children = parentComponent.children; game.update(0); componentsSorted(children); @@ -89,10 +89,10 @@ void main() { final parentComponent = PriorityComponent(0); final priorityComponents = List.generate(10, (i) => PriorityComponent(i)); priorityComponents.shuffle(); - final game = BaseGame()..onResize(Vector2.zero()); + final game = FlameGame()..onGameResize(Vector2.zero()); game.add(grandParentComponent); - grandParentComponent.addChild(parentComponent, gameRef: game); - parentComponent.children.addChildren(priorityComponents, gameRef: game); + grandParentComponent.add(parentComponent); + parentComponent.addAll(priorityComponents); final children = parentComponent.children; game.update(0); componentsSorted(children); diff --git a/packages/flame/test/components/resizable_test.dart b/packages/flame/test/components/resizable_test.dart index 2f3db5543..a5ccdd3d4 100644 --- a/packages/flame/test/components/resizable_test.dart +++ b/packages/flame/test/components/resizable_test.dart @@ -15,45 +15,45 @@ class MyComponent extends PositionComponent { } } -class MyGame extends BaseGame {} - Vector2 size = Vector2(1.0, 1.0); void main() { group('resizable test', () { - test('game calls resize on add', () { + test('game calls resize on add', () async { final a = MyComponent('a'); - final game = MyGame(); - game.onResize(size); + final game = FlameGame(); + game.onGameResize(size); - game.add(a); + await game.add(a); // component is just added on the next iteration game.update(0); expect(a.gameSize, size); }); - test('game calls resize after added', () { - final a = MyComponent('a'); - final game = MyGame(); - game.onResize(Vector2.all(10)); - game.add(a); + test('game calls resize after added', () async { + final a = MyComponent('a'); + final game = FlameGame(); + game.onGameResize(Vector2.all(10)); + + await game.add(a); // component is just added on the next iteration game.update(0); - game.onResize(size); + game.onGameResize(size); expect(a.gameSize, size); }); - test("game calls doesn't change component size", () { - final a = MyComponent('a'); - final game = MyGame(); - game.onResize(Vector2.all(10)); - game.add(a); + test("game calls doesn't change component size", () async { + final a = MyComponent('a'); + final game = FlameGame(); + game.onGameResize(Vector2.all(10)); + + await game.add(a); // component is just added on the next iteration game.update(0); - game.onResize(size); + game.onGameResize(size); expect(a.size, isNot(size)); }); }); diff --git a/packages/flame/test/components/sprite_animation_component_test.dart b/packages/flame/test/components/sprite_animation_component_test.dart index 1d19cfcd9..4a1ae036a 100644 --- a/packages/flame/test/components/sprite_animation_component_test.dart +++ b/packages/flame/test/components/sprite_animation_component_test.dart @@ -11,7 +11,7 @@ void main() async { group('SpriteAnimationComponent shouldRemove test', () { test('removeOnFinish is true and animation#loop is false', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -25,24 +25,24 @@ void main() async { removeOnFinish: true, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, true); // runs a cycle to remove the component game.update(0.1); - expect(game.components.length, 0); + expect(game.children.length, 0); }); test('removeOnFinish is true and animation#loop is true', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -57,24 +57,24 @@ void main() async { removeOnFinish: true, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test('removeOnFinish is false and animation#loop is false', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -89,24 +89,24 @@ void main() async { removeOnFinish: false, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test('removeOnFinish is false and animation#loop is true', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -122,24 +122,24 @@ void main() async { removeOnFinish: false, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test("component isn't removed if it is not playing", () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -154,27 +154,27 @@ void main() async { playing: false, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to potentially remove the component game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); }); group('SpriteAnimation timing of animation frames', () { test('Last animation frame is not skipped', () { // See https://github.com/flame-engine/flame/issues/895 - final game = BaseGame(); + final game = FlameGame(); // Non-looping animation, with the expected total duration of 0.500 s final animation = SpriteAnimation.spriteList( List.filled(5, Sprite(image)), @@ -186,7 +186,7 @@ void main() async { callbackInvoked++; }; final component = SpriteAnimationComponent(animation: animation); - game.onResize(size); + game.onGameResize(size); game.add(component); game.update(0.01); expect(animation.currentIndex, 0); diff --git a/packages/flame/test/components/sprite_animation_group_component_test.dart b/packages/flame/test/components/sprite_animation_group_component_test.dart index e8d04f2c2..6b6075c59 100644 --- a/packages/flame/test/components/sprite_animation_group_component_test.dart +++ b/packages/flame/test/components/sprite_animation_group_component_test.dart @@ -51,7 +51,7 @@ void main() async { }); group('SpriteAnimationGroupComponent shouldRemove test', () { test('removeOnFinish is true and there is no any state yet', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -65,26 +65,26 @@ void main() async { removeOnFinish: {AnimationState.idle: true}, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle and the component should still be there game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test( 'removeOnFinish is true and current state animation#loop is false', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -99,25 +99,25 @@ void main() async { current: AnimationState.idle, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, true); // runs a cycle to remove the component game.update(0.1); - expect(game.components.length, 0); + expect(game.children.length, 0); }, ); test('removeOnFinish is true and current animation#loop is true', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -133,24 +133,24 @@ void main() async { current: AnimationState.idle, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test('removeOnFinish is false and current animation#loop is false', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -165,24 +165,24 @@ void main() async { // when omited, removeOnFinish is false for all states ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); test('removeOnFinish is false and current animation#loop is true', () { - final game = BaseGame(); + final game = FlameGame(); final animation = SpriteAnimation.spriteList( [ Sprite(image), @@ -198,20 +198,20 @@ void main() async { current: AnimationState.idle, ); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); expect(component.shouldRemove, false); - expect(game.components.length, 1); + expect(game.children.length, 1); game.update(2); expect(component.shouldRemove, false); // runs a cycle to remove the component, but failed game.update(0.1); - expect(game.components.length, 1); + expect(game.children.length, 1); }); }); } diff --git a/packages/flame/test/components/tapables_test.dart b/packages/flame/test/components/tapables_test.dart index 436316148..763b6add2 100644 --- a/packages/flame/test/components/tapables_test.dart +++ b/packages/flame/test/components/tapables_test.dart @@ -2,9 +2,9 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:test/test.dart'; -class _GameWithTappables extends BaseGame with HasTappableComponents {} +class _GameWithTappables extends FlameGame with HasTappableComponents {} -class _GameWithoutTappables extends BaseGame {} +class _GameWithoutTappables extends FlameGame {} class TappableComponent extends PositionComponent with Tappable {} @@ -12,12 +12,12 @@ void main() { group('tappables test', () { test('make sure they cannot be added to invalid games', () async { final game1 = _GameWithTappables(); - game1.onResize(Vector2.all(100)); + game1.onGameResize(Vector2.all(100)); // should be ok await game1.add(TappableComponent()); final game2 = _GameWithoutTappables(); - game2.onResize(Vector2.all(100)); + game2.onGameResize(Vector2.all(100)); expect( () => game2.add(TappableComponent()), diff --git a/packages/flame/test/components/text_box_component_test.dart b/packages/flame/test/components/text_box_component_test.dart index 20ccdb532..db3a42b08 100644 --- a/packages/flame/test/components/text_box_component_test.dart +++ b/packages/flame/test/components/text_box_component_test.dart @@ -22,8 +22,8 @@ void main() { test('onLoad waits for cache to be done', () async { final c = TextBoxComponent('foo bar'); - final game = BaseGame(); - game.onResize(Vector2.all(100)); + final game = FlameGame(); + game.onGameResize(Vector2.all(100)); await game.add(c); game.update(0); diff --git a/packages/flame/test/effects/combined_effect_test.dart b/packages/flame/test/effects/combined_effect_test.dart index 9a44f71c4..cb460c18a 100644 --- a/packages/flame/test/effects/combined_effect_test.dart +++ b/packages/flame/test/effects/combined_effect_test.dart @@ -1,27 +1,39 @@ import 'dart:math'; import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; import 'package:flame/src/test_helpers/random_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'effect_test_utils.dart'; +class ReadyGame extends FlameGame { + ReadyGame() { + onGameResize(Vector2.zero()); + } +} + class Elements extends BaseElements { + bool onCompleteCalled = false; + Elements(Random random) : super(random); @override - TestComponent component() => TestComponent( - position: randomVector2(), - size: randomVector2(), - angle: randomAngle(), - ); + TestComponent component() { + return TestComponent( + position: randomVector2(), + size: randomVector2(), + angle: randomAngle(), + ); + } + + FlameGame game() => ReadyGame(); CombinedEffect effect({ - bool isInfinite = false, - bool isAlternating = false, bool hasAlternatingMoveEffect = false, bool hasAlternatingRotateEffect = false, bool hasAlternatingSizeEffect = false, + bool hasAlternatingScaleEffect = false, }) { final move = MoveEffect( path: path, @@ -33,15 +45,19 @@ class Elements extends BaseElements { duration: randomDuration(), isAlternating: hasAlternatingRotateEffect, )..skipEffectReset = true; - final scale = SizeEffect( + final size = SizeEffect( size: argumentSize, duration: randomDuration(), isAlternating: hasAlternatingSizeEffect, )..skipEffectReset = true; + final scale = ScaleEffect( + scale: argumentScale, + duration: randomDuration(), + isAlternating: hasAlternatingScaleEffect, + )..skipEffectReset = true; return CombinedEffect( - effects: [move, scale, rotate], - isInfinite: isInfinite, - isAlternating: isAlternating, + effects: [move, size, rotate, scale], + onComplete: () => onCompleteCalled = true, )..skipEffectReset = true; } } @@ -51,157 +67,36 @@ void main() { 'CombinedEffect can combine', (Random random, WidgetTester tester) async { final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(), - expectedPosition: e.path.last, - expectedAngle: e.argumentAngle, - expectedSize: e.argumentSize, - random: random, - ); + final component = e.component(); + final game = e.game(); + final effect = e.effect(); + await game.add(component); + await component.add(effect); + var timePassed = 0.0; + const timeStep = 1 / 60; + while (timePassed <= effect.iterationTime) { + game.update(timeStep); + timePassed += timeStep; + } + game.update(0); + expect(effect.hasCompleted(), true); + expect(e.onCompleteCalled, true); }, ); testWidgetsRandom( - 'CombinedEffect will stop sequence after it is done', + 'CombinedEffect can not contain children of same type', (Random random, WidgetTester tester) async { final e = Elements(random); - effectTest( - tester, - e.component(), - e.effect(), - expectedPosition: e.path.last, - expectedAngle: e.argumentAngle, - expectedSize: e.argumentSize, - iterations: 1.5, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can alternate', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(isAlternating: true), - expectedPosition: positionComponent.position.clone(), - expectedAngle: positionComponent.angle, - expectedSize: positionComponent.size.clone(), - iterations: 2.0, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can alternate and be infinite', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(isInfinite: true, isAlternating: true), - expectedPosition: positionComponent.position.clone(), - expectedAngle: positionComponent.angle, - expectedSize: positionComponent.size.clone(), - shouldComplete: false, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect alternation can peak', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(isAlternating: true), - expectedPosition: e.path.last, - expectedAngle: e.argumentAngle, - expectedSize: e.argumentSize, - shouldComplete: false, - iterations: 0.5, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can be infinite', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(isInfinite: true), - expectedPosition: e.path.last, - expectedAngle: e.argumentAngle, - expectedSize: e.argumentSize, - iterations: 3.0, - shouldComplete: false, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can contain alternating MoveEffect', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(hasAlternatingMoveEffect: true), - expectedPosition: positionComponent.position.clone(), - expectedAngle: e.argumentAngle, - expectedSize: e.argumentSize, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can contain alternating RotateEffect', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(hasAlternatingRotateEffect: true), - expectedPosition: e.path.last, - expectedAngle: positionComponent.angle, - expectedSize: e.argumentSize, - random: random, - ); - }, - ); - - testWidgetsRandom( - 'CombinedEffect can contain alternating SizeEffect', - (Random random, WidgetTester tester) async { - final e = Elements(random); - final positionComponent = e.component(); - effectTest( - tester, - positionComponent, - e.effect(hasAlternatingSizeEffect: true), - expectedPosition: e.path.last, - expectedAngle: e.argumentAngle, - expectedSize: positionComponent.size.clone(), - random: random, + final component = e.component(); + final game = e.game(); + final effect = e.effect(); + await game.add(component); + await component.add(effect); + game.update(0); + expect( + () async => effect.add(SizeEffect(duration: 1.0, size: Vector2.zero())), + throwsA(isA()), ); }, ); diff --git a/packages/flame/test/effects/effect_test_utils.dart b/packages/flame/test/effects/effect_test_utils.dart index a99a01a2a..8225a50ba 100644 --- a/packages/flame/test/effects/effect_test_utils.dart +++ b/packages/flame/test/effects/effect_test_utils.dart @@ -7,37 +7,44 @@ import 'package:flame/test.dart'; import 'package:flutter_test/flutter_test.dart'; class Callback { - bool isCalled = false; + int calledNumber = 0; - void call() => isCalled = true; + void call() => calledNumber++; } void effectTest( WidgetTester tester, PositionComponent component, - PositionComponentEffect effect, { + ComponentEffect effect, { bool shouldComplete = true, double iterations = 1.0, double expectedAngle = 0.0, Vector2? expectedPosition, Vector2? expectedSize, Vector2? expectedScale, + double epsilon = 0.01, required Random random, }) async { + component.children.register(); expectedPosition ??= Vector2.zero(); expectedSize ??= Vector2.all(100.0); expectedScale ??= Vector2.all(1.0); final callback = Callback(); effect.onComplete = callback.call; - final game = BaseGame(); - game.onResize(Vector2.all(200)); - game.add(component); - component.addEffect(effect); - final duration = effect.iterationTime; + final game = FlameGame(); + game.onGameResize(Vector2.all(200)); await tester.pumpWidget(GameWidget( game: game, )); - var timeLeft = iterations * duration; + await game.add(component); + await component.add(effect); + final duration = effect.iterationTime; + // Since the effects will flip over to 0 again when they reach their peak and + // they are infinite and not alternating, we don't want to go all the way to + // the peak. + final noOvershootTime = + effect.isInfinite && !effect.isAlternating ? 0.00001 : 0.0; + var timeLeft = (iterations - noOvershootTime) * duration; while (timeLeft > 0) { var stepDelta = 50.0 + random.nextInt(50); stepDelta /= 1000; @@ -45,35 +52,41 @@ void effectTest( game.update(stepDelta); timeLeft -= stepDelta; } + game.update(0); if (!shouldComplete) { expectVector2( component.position, expectedPosition, + epsilon: epsilon, reason: 'Position is not correct', ); expectDouble( component.angle, expectedAngle, + epsilon: epsilon, reason: 'Angle is not correct', ); expectVector2( component.size, expectedSize, + epsilon: epsilon, reason: 'Size is not correct', ); expectVector2( component.scale, expectedScale, + epsilon: epsilon, reason: 'Scale is not correct', ); } else { // To account for float number operations making effects not finish const epsilon = 0.001; - if (effect.percentage! < epsilon) { - game.update(effect.currentTime); - } else if (1.0 - effect.percentage! < epsilon) { - game.update(effect.peakTime - effect.currentTime); + final percentage = effect.percentage; + if (percentage < epsilon) { + game.update(effect.currentTime + epsilon); + } else if (1.0 - percentage < epsilon) { + game.update(effect.peakTime - effect.currentTime + epsilon); } expectVector2( @@ -97,15 +110,18 @@ void effectTest( reason: 'Scale is not exactly correct', ); } - expect(effect.hasCompleted(), shouldComplete, reason: 'Effect shouldFinish'); - game.update(0.0); + expect(effect.hasCompleted(), shouldComplete, reason: 'Effect should finish'); + game.update(0); // Children are removed before update logic expect( - callback.isCalled, - shouldComplete, + callback.calledNumber, + shouldComplete ? 1 : 0, reason: 'Callback was treated wrong', ); - game.update(0.0); // Since effects are removed before they are updated - expect(component.effects.isEmpty, shouldComplete); + expect( + component.children.query().every((e) => e.shouldRemove), + shouldComplete, + reason: 'Component had wrong number of children', + ); } class TestComponent extends PositionComponent { diff --git a/packages/flame/test/effects/paint_effect_test.dart b/packages/flame/test/effects/paint_effect_test.dart index 27175a4eb..9a5d49a9c 100644 --- a/packages/flame/test/effects/paint_effect_test.dart +++ b/packages/flame/test/effects/paint_effect_test.dart @@ -7,22 +7,20 @@ import 'package:flutter_test/flutter_test.dart'; class MyComponent extends PositionComponent with HasPaint {} -class MyGame extends BaseGame {} - void main() { group('Paint Effects', () { group('OpacityEffect', () { test( 'Sets the correct opacity on the paint', - () { - final comp = MyComponent(); - final game = MyGame(); + () async { + final component = MyComponent(); + final game = FlameGame(); - game.onResize(Vector2.all(100)); - game.add(comp); + game.onGameResize(Vector2.all(100)); + game.add(component); game.update(0); // Making sure the component was added - comp.addEffect( + await component.add( OpacityEffect( opacity: 0, duration: 1, @@ -31,7 +29,7 @@ void main() { game.update(0.2); - expect(comp.paint.color.opacity, 0.8); + expect(component.paint.color.opacity, 0.8); }, ); }); @@ -39,15 +37,15 @@ void main() { group('ColorEffect', () { test( 'Sets the correct color filter on the paint', - () { - final comp = MyComponent(); - final game = MyGame(); + () async { + final component = MyComponent(); + final game = FlameGame(); - game.onResize(Vector2.all(100)); - game.add(comp); + game.onGameResize(Vector2.all(100)); + game.add(component); game.update(0); // Making sure the component was added - comp.addEffect( + await component.add( ColorEffect( color: const Color(0xFF000000), duration: 1, @@ -57,7 +55,7 @@ void main() { game.update(0.5); expect( - comp.paint.colorFilter, + component.paint.colorFilter, const ColorFilter.mode(Color(0xFF7F7F7F), BlendMode.multiply), ); }, diff --git a/packages/flame/test/effects/sequence_effect_test.dart b/packages/flame/test/effects/sequence_effect_test.dart index 908541d06..79add43c8 100644 --- a/packages/flame/test/effects/sequence_effect_test.dart +++ b/packages/flame/test/effects/sequence_effect_test.dart @@ -111,6 +111,7 @@ void main() { expectedAngle: positionComponent.angle, expectedSize: positionComponent.size.clone(), shouldComplete: false, + epsilon: 3.0, random: random, ); }, diff --git a/packages/flame/test/game/base_game_test.dart b/packages/flame/test/game/base_game_test.dart index df081f0f6..c53f62de0 100644 --- a/packages/flame/test/game/base_game_test.dart +++ b/packages/flame/test/game/base_game_test.dart @@ -10,7 +10,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart' as flutter; import 'package:test/test.dart'; -class MyGame extends BaseGame with HasTappableComponents {} +class MyGame extends FlameGame with HasTappableComponents {} class MyComponent extends PositionComponent with Tappable, HasGameRef { bool tapped = false; @@ -55,7 +55,10 @@ class MyComponent extends PositionComponent with Tappable, HasGameRef { class MyAsyncComponent extends MyComponent { @override - Future onLoad() => Future.value(); + Future onLoad() async { + await super.onLoad(); + return Future.value(); + } } class PositionComponentNoNeedForRect extends PositionComponent with Tappable {} @@ -63,17 +66,17 @@ class PositionComponentNoNeedForRect extends PositionComponent with Tappable {} Vector2 size = Vector2(1.0, 1.0); void main() { - group('BaseGame test', () { + group('FlameGame test', () { test('adds the component to the component list', () { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); // runs a cycle to add the component game.update(0.1); - expect(true, game.components.contains(component)); + expect(true, game.children.contains(component)); }); test( @@ -82,12 +85,12 @@ void main() { final game = MyGame(); final component = MyAsyncComponent(); - game.onResize(size); + game.onGameResize(size); await game.add(component); // runs a cycle to add the component game.update(0.1); - expect(true, game.components.contains(component)); + expect(true, game.children.contains(component)); expect(component.gameSize, size); expect(component.gameRef, game); @@ -98,7 +101,7 @@ void main() { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); expect(component.gameSize, size); @@ -109,7 +112,7 @@ void main() { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); // The component is not added to the component list until an update has been performed game.update(0.0); @@ -122,12 +125,12 @@ void main() { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); // The component is not added to the component list until an update has been performed game.update(0.0); - expect(game.components.contains(component), true); + expect(game.children.contains(component), true); }); flutter.testWidgets( @@ -136,10 +139,10 @@ void main() { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); late GameRenderBox renderBox; - tester.pumpWidget( + await tester.pumpWidget( Builder( builder: (BuildContext context) { renderBox = GameRenderBox(context, game); @@ -163,15 +166,15 @@ void main() { final game = MyGame(); final component = MyComponent(); - game.onResize(size); + game.onGameResize(size); game.add(component); // The component is not added to the component list until an update has been performed game.update(0.0); // The component is removed both by removing it on the game instance and // by the function on the component, but the onRemove callback should // only be called once. - component.remove(); - game.components.remove(component); + component.removeFromParent(); + game.children.remove(component); // The component is not removed from the component list until an update has been performed game.update(0.0); @@ -180,44 +183,44 @@ void main() { }); test('remove depend SpriteComponent.shouldRemove', () { - final game = MyGame()..onResize(size); + final game = MyGame()..onGameResize(size); // addLater here game.add(SpriteComponent()..shouldRemove = true); game.update(0); - expect(game.components.length, equals(1)); + expect(game.children.length, equals(1)); // remove effected here game.update(0); - expect(game.components.isEmpty, equals(true)); + expect(game.children.isEmpty, equals(true)); }); test('remove depend SpriteAnimationComponent.shouldRemove', () { - final game = MyGame()..onResize(size); + final game = MyGame()..onGameResize(size); game.add(SpriteAnimationComponent()..shouldRemove = true); game.update(0); - expect(game.components.length, equals(1)); + expect(game.children.length, equals(1)); game.update(0); - expect(game.components.isEmpty, equals(true)); + expect(game.children.isEmpty, equals(true)); }); test('clear removes all components', () { final game = MyGame(); final components = List.generate(3, (index) => MyComponent()); - game.onResize(size); + game.onGameResize(size); game.addAll(components); // The components are not added to the component list until an update has been performed game.update(0.0); - expect(game.components.length, equals(3)); + expect(game.children.length, equals(3)); - game.components.clear(); + game.children.clear(); // Ensure clear does not remove components directly - expect(game.components.length, equals(3)); + expect(game.children.length, equals(3)); game.update(0.0); - expect(game.components.isEmpty, equals(true)); + expect(game.children.isEmpty, equals(true)); }); } diff --git a/packages/flame/test/game/camera_and_viewport_test.dart b/packages/flame/test/game/camera_and_viewport_test.dart index 3ac9fda97..453a45da4 100644 --- a/packages/flame/test/game/camera_and_viewport_test.dart +++ b/packages/flame/test/game/camera_and_viewport_test.dart @@ -25,16 +25,16 @@ class TestComponent extends PositionComponent { void main() { group('viewport', () { test('default viewport does not change size', () { - final game = BaseGame(); // default viewport - game.onResize(Vector2(100.0, 200.0)); + final game = FlameGame(); // default viewport + game.onGameResize(Vector2(100.0, 200.0)); expect(game.canvasSize, Vector2(100.0, 200.00)); expect(game.size, Vector2(100.0, 200.00)); }); test('fixed ratio viewport has perfect ratio', () { - final game = BaseGame() + final game = FlameGame() ..camera.viewport = FixedResolutionViewport(Vector2.all(50)); - game.onResize(Vector2.all(200.0)); + game.onGameResize(Vector2.all(200.0)); expect(game.canvasSize, Vector2.all(200.00)); expect(game.size, Vector2.all(50.00)); @@ -54,9 +54,9 @@ void main() { }); test('fixed ratio viewport maxes width', () { - final game = BaseGame() + final game = FlameGame() ..camera.viewport = FixedResolutionViewport(Vector2.all(50)); - game.onResize(Vector2(100.0, 200.0)); + game.onGameResize(Vector2(100.0, 200.0)); expect(game.canvasSize, Vector2(100.0, 200.00)); expect(game.size, Vector2.all(50.00)); @@ -77,9 +77,9 @@ void main() { }); test('fixed ratio viewport maxes height', () { - final game = BaseGame() + final game = FlameGame() ..camera.viewport = FixedResolutionViewport(Vector2(100.0, 400.0)); - game.onResize(Vector2(100.0, 200.0)); + game.onGameResize(Vector2(100.0, 200.0)); expect(game.canvasSize, Vector2(100.0, 200.00)); expect(game.size, Vector2(100.00, 400.0)); @@ -102,8 +102,8 @@ void main() { group('camera', () { test('default camera applies no translation', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); expect(game.camera.position, Vector2.zero()); final p = TestComponent(Vector2.all(10.0)); @@ -121,8 +121,8 @@ void main() { }); test('camera snap movement', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); expect(game.camera.position, Vector2.zero()); final p = TestComponent(Vector2.all(10.0)); @@ -147,8 +147,8 @@ void main() { }); test('camera smooth movement', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); game.camera.speed = 1; // 1 pixel per second game.camera.moveTo(Vector2(0.0, 10.0)); @@ -164,8 +164,8 @@ void main() { }); test('camera follow', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); final p = TestComponent(Vector2.all(10.0))..anchor = Anchor.center; game.add(p); @@ -192,8 +192,8 @@ void main() { }); test('camera follow with relative position', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); final p = TestComponent(Vector2.all(10.0))..anchor = Anchor.center; game.add(p); @@ -219,8 +219,8 @@ void main() { ); }); test('camera follow with world boundaries', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(100.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(100.0)); final p = TestComponent(Vector2.all(10.0))..anchor = Anchor.center; game.add(p); @@ -252,8 +252,8 @@ void main() { expect(game.camera.position, Vector2(900, 900)); }); test('camera follow with world boundaries smaller than the screen', () { - final game = BaseGame(); // no camera changes - game.onResize(Vector2.all(200.0)); + final game = FlameGame(); // no camera changes + game.onGameResize(Vector2.all(200.0)); final p = TestComponent(Vector2.all(10.0))..anchor = Anchor.center; game.add(p); @@ -276,8 +276,8 @@ void main() { expect(game.camera.position, Vector2(50, 50)); }); test('camera relative offset without follow', () { - final game = BaseGame(); - game.onResize(Vector2.all(200.0)); + final game = FlameGame(); + game.onGameResize(Vector2.all(200.0)); game.camera.setRelativeOffset(Anchor.center); @@ -289,8 +289,8 @@ void main() { }); test('camera zoom', () { - final game = BaseGame(); - game.onResize(Vector2.all(200.0)); + final game = FlameGame(); + game.onGameResize(Vector2.all(200.0)); game.camera.zoom = 2; final p = TestComponent(Vector2.all(100.0))..anchor = Anchor.center; @@ -309,8 +309,8 @@ void main() { }); test('camera zoom with setRelativeOffset', () { - final game = BaseGame(); - game.onResize(Vector2.all(200.0)); + final game = FlameGame(); + game.onGameResize(Vector2.all(200.0)); game.camera.zoom = 2; game.camera.setRelativeOffset(Anchor.center); @@ -332,9 +332,9 @@ void main() { }); test('camera shake should return to where it started', () { - final game = BaseGame(); + final game = FlameGame(); final camera = game.camera; - game.onResize(Vector2.all(200.0)); + game.onGameResize(Vector2.all(200.0)); expect(camera.position, Vector2.zero()); camera.shake(duration: 9000); game.update(5000); @@ -346,9 +346,9 @@ void main() { group('viewport & camera', () { test('default ratio viewport + camera with world boundaries', () { - final game = BaseGame() + final game = FlameGame() ..camera.viewport = FixedResolutionViewport(Vector2.all(100)); - game.onResize(Vector2.all(200.0)); + game.onGameResize(Vector2.all(200.0)); expect(game.canvasSize, Vector2.all(200.00)); expect(game.size, Vector2.all(100.00)); diff --git a/packages/flame/test/game/game_widget/game_widget_keyboard_test.dart b/packages/flame/test/game/game_widget/game_widget_keyboard_test.dart index 275d3b4e9..653da753f 100644 --- a/packages/flame/test/game/game_widget/game_widget_keyboard_test.dart +++ b/packages/flame/test/game/game_widget/game_widget_keyboard_test.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; @@ -10,17 +8,11 @@ import 'package:flutter_test/flutter_test.dart'; Vector2 size = Vector2(1.0, 1.0); -class _KeyboardEventsGame extends Game with KeyboardEvents { +class _KeyboardEventsGame extends FlameGame with KeyboardEvents { final List keysPressed = []; _KeyboardEventsGame(); - @override - void render(Canvas canvas) {} - - @override - void update(double dt) {} - @override KeyEventResult onKeyEvent( RawKeyEvent event, @@ -32,7 +24,7 @@ class _KeyboardEventsGame extends Game with KeyboardEvents { } } -class _KeyboardHandlerComponent extends BaseComponent with KeyboardHandler { +class _KeyboardHandlerComponent extends Component with KeyboardHandler { final List keysPressed = []; @override @@ -42,7 +34,7 @@ class _KeyboardHandlerComponent extends BaseComponent with KeyboardHandler { } } -class _HasKeyboardHandlerComponentsGame extends BaseGame +class _HasKeyboardHandlerComponentsGame extends FlameGame with HasKeyboardHandlerComponents { _HasKeyboardHandlerComponentsGame(); @@ -50,6 +42,7 @@ class _HasKeyboardHandlerComponentsGame extends BaseGame @override Future onLoad() async { + await super.onLoad(); keyboardHandler = _KeyboardHandlerComponent(); add(keyboardHandler); } @@ -133,7 +126,7 @@ void main() async { ), ); - game.onResize(size); + game.onGameResize(size); game.update(0.1); await tester.pump(); diff --git a/packages/flame/test/game/game_widget/game_widget_lifecycle_test.dart b/packages/flame/test/game/game_widget/game_widget_lifecycle_test.dart index ac9e8e54e..fb79161b2 100644 --- a/packages/flame/test/game/game_widget/game_widget_lifecycle_test.dart +++ b/packages/flame/test/game/game_widget/game_widget_lifecycle_test.dart @@ -1,32 +1,33 @@ -import 'dart:ui'; - import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -class MyGame extends Game { +class MyGame extends FlameGame { final List events; MyGame(this.events); @override - void update(double dt) {} - - @override - void render(Canvas canvas) {} - - @override - void onAttach() { - super.onAttach(); - - events.add('attach'); + void onGameResize(Vector2 size) { + super.onGameResize(size); + events.add('onGameResize'); } @override - void onDetach() { - super.onDetach(); + Future onLoad() async { + await super.onLoad(); + events.add('onLoad'); + } - events.add('detach'); + @override + Future? onMount() { + events.add('onMount'); + } + + @override + void onRemove() { + super.onRemove(); + events.add('onRemove'); } } @@ -45,9 +46,9 @@ class TitlePage extends StatelessWidget { } class GamePage extends StatefulWidget { - final List events; + final MyGame game; - const GamePage(this.events); + const GamePage(this.game); @override State createState() { @@ -61,8 +62,7 @@ class _GamePageState extends State { @override void initState() { super.initState(); - - _game = MyGame(widget.events); + _game = widget.game; } @override @@ -93,15 +93,18 @@ class _GamePageState extends State { class MyApp extends StatelessWidget { final List events; + late final MyGame game; - const MyApp(this.events); + MyApp(this.events) { + game = MyGame(events); + } @override Widget build(BuildContext context) { return MaterialApp( routes: { '/': (_) => TitlePage(), - '/game': (_) => GamePage(events), + '/game': (_) => GamePage(game), }, ); } @@ -118,10 +121,14 @@ void main() { // I am unsure why I need two bumps here, my best theory is // that we need the first one for the navigation animation // and the second one for the page to render - await tester.pump(const Duration(milliseconds: 1000)); - await tester.pump(const Duration(milliseconds: 1000)); + await tester.pump(); + await tester.pump(); - expect(events, ['attach']); + expect( + events.contains('onLoad'), + true, + reason: 'onLoad event was not fired on attach', + ); }); testWidgets('detach when navigating out of the page', (tester) async { @@ -130,8 +137,8 @@ void main() { await tester.tap(find.text('Play')); - await tester.pump(const Duration(milliseconds: 1000)); - await tester.pump(const Duration(milliseconds: 1000)); + await tester.pump(); + await tester.pump(); await tester.tap(find.text('Back')); @@ -139,7 +146,49 @@ void main() { // happens, if it was, then the pumpAndSettle would break with a timeout await tester.pumpAndSettle(); - expect(events, ['attach', 'detach']); + expect( + events.contains('onLoad'), + true, + reason: 'onLoad was not called', + ); + expect( + events.contains('onRemove'), + true, + reason: 'onRemove was not called', + ); + }); + + testWidgets('all events are executed in the correct order', (tester) async { + final events = []; + await tester.pumpWidget(MyApp(events)); + + await tester.tap(find.text('Play')); + + await tester.pump(); + await tester.pump(); + + await tester.tap(find.text('Back')); + + // This ensures that Flame is not running anymore after the navigation + // happens, if it was, then the pumpAndSettle would break with a timeout + await tester.pumpAndSettle(); + + await tester.tap(find.text('Play')); + + await tester.pump(); + await tester.pump(); + + expect( + events, + [ + 'onGameResize', + 'onLoad', + 'onMount', + 'onRemove', + 'onGameResize', + 'onMount', + ], + ); }); }); } diff --git a/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart b/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart index 6ea24f167..069f25ec8 100644 --- a/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart +++ b/packages/flame/test/game/game_widget/game_widget_mouse_cursor_test.dart @@ -1,17 +1,7 @@ -import 'dart:ui'; - import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -class TestGame extends Game { - @override - void render(Canvas canvas) {} - - @override - void update(double dt) {} -} - Finder byMouseCursor(MouseCursor cursor) { return find.byWidgetPredicate( (widget) => widget is MouseRegion && widget.cursor == cursor, @@ -24,7 +14,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: GameWidget( - game: TestGame(), + game: FlameGame(), mouseCursor: SystemMouseCursors.grab, ), ), @@ -37,7 +27,7 @@ void main() { }); testWidgets('can change the cursor', (tester) async { - final game = TestGame(); + final game = FlameGame(); await tester.pumpWidget( MaterialApp( diff --git a/packages/flame/test/game/mixins/keyboard_test.dart b/packages/flame/test/game/mixins/keyboard_test.dart index 5f59e4bad..ba6d87165 100644 --- a/packages/flame/test/game/mixins/keyboard_test.dart +++ b/packages/flame/test/game/mixins/keyboard_test.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flutter/material.dart'; @@ -7,15 +5,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class ValidGame extends Game with KeyboardEvents { - @override - void render(Canvas canvas) {} +class ValidGame extends FlameGame with KeyboardEvents {} - @override - void update(double dt) {} -} - -class InvalidGame extends BaseGame +class InvalidGame extends FlameGame with HasKeyboardHandlerComponents, KeyboardEvents {} class MockRawKeyEventData extends Mock implements RawKeyEventData { @@ -26,7 +18,7 @@ class MockRawKeyEventData extends Mock implements RawKeyEventData { } void main() { - group('Keyboarde events', () { + group('Keyboard events', () { test( 'cannot mix KeyboardEvent and HasKeyboardHandlerComponents together', () { diff --git a/packages/flame/test/game/projections_test.dart b/packages/flame/test/game/projections_test.dart index 173f5af78..f463a806e 100644 --- a/packages/flame/test/game/projections_test.dart +++ b/packages/flame/test/game/projections_test.dart @@ -7,7 +7,7 @@ import 'package:test/test.dart'; void main() { group('projections', () { test('default viewport and camera should no-op projections', () { - final game = BaseGame(); // default viewport & camera + final game = FlameGame(); // default viewport & camera _assertIdentityOfProjector(game); expect(game.projectVector(Vector2(1, 2)), Vector2(1, 2)); @@ -17,8 +17,8 @@ void main() { }); test('viewport only with scale projection (no camera)', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; // default camera - game.onResize(Vector2(200, 200)); + final game = FlameGame()..camera.viewport = viewport; // default camera + game.onGameResize(Vector2(200, 200)); expect(viewport.scale, 2); expect(viewport.resizeOffset, Vector2.zero()); // no translation _assertIdentityOfProjector(game); @@ -30,8 +30,8 @@ void main() { }); test('viewport only with translation projection (no camera)', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; // default camera - game.onResize(Vector2(200, 100)); + final game = FlameGame()..camera.viewport = viewport; // default camera + game.onGameResize(Vector2(200, 100)); expect(viewport.scale, 1); // no scale expect(viewport.resizeOffset, Vector2(50, 0)); // y is unchanged _assertIdentityOfProjector(game); @@ -53,8 +53,8 @@ void main() { }); test('viewport only with both scale and translation (no camera)', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; // default camera - game.onResize(Vector2(200, 400)); + final game = FlameGame()..camera.viewport = viewport; // default camera + game.onGameResize(Vector2(200, 400)); expect(viewport.scale, 2); expect(viewport.resizeOffset, Vector2(0, 100)); // x is unchanged _assertIdentityOfProjector(game); @@ -66,8 +66,8 @@ void main() { expect(game.unprojectVector(Vector2(0, 400)), Vector2(0, 150)); }); test('camera only with zoom (default viewport)', () { - final game = BaseGame(); // default viewport - game.onResize(Vector2.all(1)); + final game = FlameGame(); // default viewport + game.onGameResize(Vector2.all(1)); game.camera.zoom = 3; // 3x zoom _assertIdentityOfProjector(game); @@ -80,8 +80,8 @@ void main() { expect(game.unscaleVector(Vector2(3, 6)), Vector2(1, 2)); }); test('camera only with translation (default viewport)', () { - final game = BaseGame(); // default viewport - game.onResize(Vector2.all(1)); + final game = FlameGame(); // default viewport + game.onGameResize(Vector2.all(1)); // top left corner of the screen is (50, 100) game.camera.snapTo(Vector2(50, 100)); @@ -95,8 +95,8 @@ void main() { expect(game.scaleVector(Vector2(-50, 50)), Vector2(-50, 50)); }); test('camera only with both zoom and translation (default viewport)', () { - final game = BaseGame(); // default viewport - game.onResize(Vector2.all(10)); + final game = FlameGame(); // default viewport + game.onGameResize(Vector2.all(10)); // no-op because the default is already top left game.camera.setRelativeOffset(Anchor.topLeft); @@ -134,8 +134,8 @@ void main() { }); test('camera & viewport - two translations', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; // default camera - game.onResize(Vector2(200, 100)); + final game = FlameGame()..camera.viewport = viewport; // default camera + game.onGameResize(Vector2(200, 100)); game.camera.snapTo(Vector2(10, 100)); expect(viewport.scale, 1); // no scale expect(viewport.resizeOffset, Vector2(50, 0)); // y is unchanged @@ -154,8 +154,8 @@ void main() { }); test('camera zoom & viewport translation', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; - game.onResize(Vector2(200, 100)); + final game = FlameGame()..camera.viewport = viewport; + game.onGameResize(Vector2(200, 100)); game.camera.zoom = 2; game.camera.snap(); expect(viewport.scale, 1); // no scale @@ -182,8 +182,8 @@ void main() { }); test('camera translation & viewport scale+translation', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; - game.onResize(Vector2(200, 400)); + final game = FlameGame()..camera.viewport = viewport; + game.onGameResize(Vector2(200, 400)); expect(viewport.scale, 2); expect(viewport.resizeOffset, Vector2(0, 100)); // x is unchanged @@ -206,8 +206,8 @@ void main() { }); test('camera & viewport scale/zoom + translation (cancel out scaling)', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; - game.onResize(Vector2(200, 400)); + final game = FlameGame()..camera.viewport = viewport; + game.onGameResize(Vector2(200, 400)); expect(viewport.scale, 2); expect(viewport.resizeOffset, Vector2(0, 100)); // x is unchanged @@ -242,8 +242,8 @@ void main() { }); test('camera & viewport scale/zoom + translation', () { final viewport = FixedResolutionViewport(Vector2.all(100)); - final game = BaseGame()..camera.viewport = viewport; - game.onResize(Vector2(200, 400)); + final game = FlameGame()..camera.viewport = viewport; + game.onGameResize(Vector2(200, 400)); expect(viewport.scale, 2); expect(viewport.resizeOffset, Vector2(0, 100)); // x is unchanged diff --git a/packages/flame_audio/example/lib/main.dart b/packages/flame_audio/example/lib/main.dart index ac12b718c..2cb96d2d4 100644 --- a/packages/flame_audio/example/lib/main.dart +++ b/packages/flame_audio/example/lib/main.dart @@ -18,7 +18,7 @@ void main() async { /// 2. Uses a custom AudioPool for extremely efficient audio loading and pooling /// for tapping elsewhere. /// 3. Uses the Bgm utility for background music. -class AudioGame extends BaseGame with TapDetector { +class AudioGame extends FlameGame with TapDetector { static Paint black = BasicPalette.black.paint(); static Paint gray = const PaletteEntry(Color(0xFFCCCCCC)).paint(); static TextPaint text = TextPaint( @@ -29,6 +29,7 @@ class AudioGame extends BaseGame with TapDetector { @override Future onLoad() async { + await super.onLoad(); pool = await AudioPool.create('fire_2.mp3', minPlayers: 3, maxPlayers: 4); startBgmMusic(); } diff --git a/packages/flame_fire_atlas/example/lib/main.dart b/packages/flame_fire_atlas/example/lib/main.dart index 65c56af18..c778b45fb 100644 --- a/packages/flame_fire_atlas/example/lib/main.dart +++ b/packages/flame_fire_atlas/example/lib/main.dart @@ -16,11 +16,12 @@ void main() async { } } -class ExampleGame extends BaseGame with TapDetector { +class ExampleGame extends FlameGame with TapDetector { late FireAtlas _atlas; @override Future onLoad() async { + await super.onLoad(); _atlas = await loadFireAtlas('caveace.fa'); add( SpriteAnimationComponent( diff --git a/packages/flame_fire_atlas/lib/flame_fire_atlas.dart b/packages/flame_fire_atlas/lib/flame_fire_atlas.dart index 9bcd6dfc0..3766d2b89 100644 --- a/packages/flame_fire_atlas/lib/flame_fire_atlas.dart +++ b/packages/flame_fire_atlas/lib/flame_fire_atlas.dart @@ -1,16 +1,15 @@ library flame_fire_atlas; -import 'package:flame/flame.dart'; -import 'package:flame/game.dart'; -import 'package:flame/sprite.dart'; -import 'package:flame/extensions.dart'; -import 'package:flame/assets.dart'; - -import 'package:archive/archive.dart'; - import 'dart:convert'; import 'dart:ui'; +import 'package:archive/archive.dart'; +import 'package:flame/assets.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/game.dart'; +import 'package:flame/sprite.dart'; + extension FireAtlasExtensions on Game { /// Load a [FireAtlas] instances from the given [asset] Future loadFireAtlas(String asset) async { diff --git a/packages/flame_fire_atlas/test/flame_fire_atlas_test.dart b/packages/flame_fire_atlas/test/flame_fire_atlas_test.dart index 9a2c21aff..85c8c245a 100644 --- a/packages/flame_fire_atlas/test/flame_fire_atlas_test.dart +++ b/packages/flame_fire_atlas/test/flame_fire_atlas_test.dart @@ -14,7 +14,7 @@ class ImagesMock extends Mock implements Images {} class ImageMock extends Mock implements Image {} -class MockedGame extends Mock implements Game { +class MockedGame extends Mock implements FlameGame { final _imagesMock = ImagesMock(); @override Images get images => _imagesMock; diff --git a/packages/flame_forge2d/CHANGELOG.md b/packages/flame_forge2d/CHANGELOG.md index e36049078..cb0ee4a63 100644 --- a/packages/flame_forge2d/CHANGELOG.md +++ b/packages/flame_forge2d/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Next] - The rendering of `BodyComponent` is now inline with the Flame coordinate system + - Moved `BodyComponent` base from `BaseComponent` to `Component` ## [0.8.1-releasecandidate.13] - Added physics tied to widgets example diff --git a/packages/flame_forge2d/example/lib/composition_sample.dart b/packages/flame_forge2d/example/lib/composition_sample.dart index 0ee51ebd0..ffe6c80cb 100644 --- a/packages/flame_forge2d/example/lib/composition_sample.dart +++ b/packages/flame_forge2d/example/lib/composition_sample.dart @@ -17,6 +17,7 @@ component. Click the ball to see the number increment. @override Future onLoad() async { + await super.onLoad(); final boundaries = createBoundaries(this); boundaries.forEach(add); final center = screenToWorld(camera.viewport.effectiveSize / 2); @@ -41,7 +42,7 @@ class TapableBall extends Ball with Tappable { super.onLoad(); _textPaint = TextPaint(config: _textConfig); textComponent = TextComponent(counter.toString(), textRenderer: _textPaint); - addChild(textComponent); + add(textComponent); } @override diff --git a/packages/flame_forge2d/example/lib/sprite_body_sample.dart b/packages/flame_forge2d/example/lib/sprite_body_sample.dart index 425e5b038..a11ff0ce3 100644 --- a/packages/flame_forge2d/example/lib/sprite_body_sample.dart +++ b/packages/flame_forge2d/example/lib/sprite_body_sample.dart @@ -45,6 +45,7 @@ class SpriteBodySample extends Forge2DGame with TapDetector { @override Future onLoad() async { + await super.onLoad(); _pizzaImage = await images.load('pizza.png'); addAll(createBoundaries(this)); } diff --git a/packages/flame_forge2d/example/lib/tappable_sample.dart b/packages/flame_forge2d/example/lib/tappable_sample.dart index 8d38287be..49a618bd5 100644 --- a/packages/flame_forge2d/example/lib/tappable_sample.dart +++ b/packages/flame_forge2d/example/lib/tappable_sample.dart @@ -12,6 +12,7 @@ class TappableSample extends Forge2DGame with HasTappableComponents { @override Future onLoad() async { + await super.onLoad(); final boundaries = createBoundaries(this); boundaries.forEach(add); final center = screenToWorld(camera.viewport.effectiveSize / 2); diff --git a/packages/flame_forge2d/example/lib/widget_sample.dart b/packages/flame_forge2d/example/lib/widget_sample.dart index e8083f228..27a238776 100644 --- a/packages/flame_forge2d/example/lib/widget_sample.dart +++ b/packages/flame_forge2d/example/lib/widget_sample.dart @@ -24,6 +24,7 @@ class WidgetSample extends Forge2DGame with TapDetector { @override Future onLoad() async { + await super.onLoad(); final boundaries = createBoundaries(this); addAll(boundaries); } diff --git a/packages/flame_forge2d/lib/body_component.dart b/packages/flame_forge2d/lib/body_component.dart index 5e89a75d0..c75305c76 100644 --- a/packages/flame_forge2d/lib/body_component.dart +++ b/packages/flame_forge2d/lib/body_component.dart @@ -14,7 +14,7 @@ import 'sprite_body_component.dart'; /// Since a pure BodyComponent doesn't have anything drawn on top of it, /// it is a good idea to turn on [debugMode] for it so that the bodies can be /// seen -abstract class BodyComponent extends BaseComponent +abstract class BodyComponent extends Component with HasGameRef { static const defaultColor = Color.fromARGB(255, 255, 255, 255); late Body body; @@ -39,6 +39,7 @@ abstract class BodyComponent extends BaseComponent @mustCallSuper @override Future onLoad() async { + await super.onLoad(); body = createBody(); } diff --git a/packages/flame_forge2d/lib/forge2d_game.dart b/packages/flame_forge2d/lib/forge2d_game.dart index 7b7885ffe..ea987aae8 100644 --- a/packages/flame_forge2d/lib/forge2d_game.dart +++ b/packages/flame_forge2d/lib/forge2d_game.dart @@ -8,7 +8,7 @@ import 'package:forge2d/forge2d.dart' hide Timer; import 'contact_callbacks.dart'; import 'forge2d_camera.dart'; -class Forge2DGame extends BaseGame { +class Forge2DGame extends FlameGame { static final Vector2 defaultGravity = Vector2(0, -10.0); static const double defaultZoom = 10.0; diff --git a/packages/flame_forge2d/lib/position_body_component.dart b/packages/flame_forge2d/lib/position_body_component.dart index 362eda489..d232d0946 100644 --- a/packages/flame_forge2d/lib/position_body_component.dart +++ b/packages/flame_forge2d/lib/position_body_component.dart @@ -42,7 +42,7 @@ abstract class PositionBodyComponent super.onRemove(); // Since the PositionComponent was added to the game in this class it should // also be removed by this class when the BodyComponent is removed. - positionComponent.remove(); + positionComponent.removeFromParent(); } void updatePositionComponent() { diff --git a/packages/flame_oxygen/lib/src/oxygen_game.dart b/packages/flame_oxygen/lib/src/oxygen_game.dart index 53bfb9714..4bd710549 100644 --- a/packages/flame_oxygen/lib/src/oxygen_game.dart +++ b/packages/flame_oxygen/lib/src/oxygen_game.dart @@ -11,7 +11,7 @@ import 'flame_world.dart'; /// [OxygenGame] should be extended to add your own game logic. /// /// It is based on the Oxygen package. -abstract class OxygenGame extends Game { +abstract class OxygenGame with Loadable, Game { late final FlameWorld world; OxygenGame() { diff --git a/packages/flame_svg/example/lib/main.dart b/packages/flame_svg/example/lib/main.dart index 294526d64..8fd90cf83 100644 --- a/packages/flame_svg/example/lib/main.dart +++ b/packages/flame_svg/example/lib/main.dart @@ -1,13 +1,12 @@ import 'package:flame/game.dart'; import 'package:flame_svg/flame_svg.dart'; - import 'package:flutter/material.dart'; void main() { runApp(GameWidget(game: MyGame())); } -class MyGame extends BaseGame { +class MyGame extends FlameGame { late Svg svgInstance; @override @@ -22,6 +21,7 @@ class MyGame extends BaseGame { @override Future onLoad() async { + await super.onLoad(); svgInstance = await loadSvg('android.svg'); final android = SvgComponent.fromSvg( svgInstance, diff --git a/tutorials/1_basic_square/README.md b/tutorials/1_basic_square/README.md deleted file mode 100644 index 99a4f66b4..000000000 --- a/tutorials/1_basic_square/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# Basic: Rendering a simple square on the screen - -This tutorial will introduce you to: - - - `Game`: The basic class to build a game on, you will understand its structure and how to use it - to build a simple game - - `GameWidget`: The widget used to place your game on your Flutter widget tree - - Basic rendering: You will get introduced to the basics of using the `Canvas` class from `dart:ui` - which is the class that Flame uses for rendering your game. - -All the code of this tutorial code can be found [here](./code). - -By the end of this tutorial you will have built a simple game that renders a square bouncing on the -screen that will look like this: - -![Preview](./media/preview.gif) - -## Building your first game - -`Game` is the most basic class that you can use to build your game, it includes the necessary -methods for creating a basic game loop and methods for the lifecycle of a Flame game. - -For more complex games, you will probably want to use `BaseGame`, which has a lot of utilities that -will make your life easier. We will cover that on later tutorials, for this one the `Game` class -will be enough for the concepts that you will learn here. - -`GameWidget` is, like the name suggests, a Flutter widget that will run your game and place it -inside the Flutter widget tree. - -As a first step, lets just get a Game instance running. For that, we will need to create our own -class that extends Flame's Game class, implement its methods, and pass an instance of that `Game` to -a `GameWidget`. Something like this: - -```dart -import 'package:flutter/material.dart'; -import 'package:flame/game.dart'; - -void main() { - final myGame = MyGame(); - runApp( - GameWidget( - game: myGame, - ), - ); -} - -class MyGame extends Game { - @override - void update(double dt) { /* TODO */ } - - @override - void render(Canvas canvas) { /* TODO */ } -} -``` - -That is it! If you run this, you will only see an empty black screen for now, but now we have the -bare-bones needed to start implementing our game. - -Before going further, it is important to explain what those two methods mean. - -Flame's `Game` class is an implementation of a Game Loop, which the basic structure on which most -games are built on. It is called a Game Loop because it really works as an infinite loop that will -continue to iterate as long as the game is running. The loop goes through the following steps: - - 1. Take input from the player - 2. Process the logic for the next frame - 3. Render the frame. - -In this tutorial we will focus on both step two and three. - -To process the logic of the game, we use the `update` method, which always runs before the frame is -rendered. Update receives a single argument, a `double` value called `dt` (delta time), which is the -amount of seconds between the current iteration and the last one. This delta time is very important -since we use it to correctly calculate the speed of movement, animations and etc. - -To render the frame, we use the `render` method, this method receives a single argument which is an -instance of a `dart:ui` `Canvas` class. With that instance we can basically render anything we want. -It is important to not have any game logic in this method, it should only contain render -instructions, any game logic should be put in the `update` method. - -Now that we have a better understanding of how the game structure works, lets start to plan our -game. - -So, lets think on what variables and data structure we would need. For that, lets recap what we are -building: A simple game where a square keeps bouncing forever from one side of the screen to the -other. Thinking about this, we will need: - - - A constant to tell us the speed of the square in logical pixels per second. - - A variable to keep track of which direction the square is moving. - - A structure to represent our square, which has a position and dimensions. - -With that in mind, check the example below, note the comments for explanations on each code section. - - -```dart -class MyGame extends Game { - // A constant speed, represented in logical pixels per second - static const int squareSpeed = 400; - - // To represent our square we are using the Rect class from dart:ui - // which is a handy class to represent this type of data. We will be - // seeing other types of data classes in the future, but for this - // example, Rect will do fine for us. - late Rect squarePos; - - // To represent our direction, we will be using an int value, where 1 means - // going to the right, and -1 going to the left, this may seems like a too much - // simple way of representing a direction, and indeed it is, but this will - // will work fine for our small example and will make more sense when we implement - // the update method - int squareDirection = 1; - - // The onLoad method is where all of the game initialization is supposed to go - // For this example, you may think that this square could just be initialized on the field - // declaration, and you are right, but for learning purposes and to present the life cycle method - // for this example we will be initializing this field here. - @override - Future onLoad() async { - squarePos = Rect.fromLTWH(0, 0, 100, 100); - } - - // Update and render omitted -} -``` - -Right, now we have all the data and variables we need to start implementing our game. For the next -step, lets draw our little square on the screen. - -```dart -class MyGame extends Game { - // BasicPalette is a help class from Flame, which provides default, pre-built instances - // of Paint that can be used by your game - static final squarePaint = BasicPalette.white.paint(); - - // Update method omitted - - @override - void render(Canvas canvas) { - // Canvas is a class from dart:ui and is it responsible for all the rendering inside of Flame - canvas.drawRect(squarePos, squarePaint); - } -} -``` - -You may now be seeing a static white square being rendered on the top left corner of your screen, -which is not that impressive, so, to finish our example, lets add some movement to the game and -implement our update method: - -```dart - @override - void update(double dt) { - // Here we move our square by calculating our movement using - // the iteration delta time and our speed variable and direction. - // Note that the Rect class is immutable and the translate method returns a new Rect instance - // for us, so we just re-assign it to our square variable. - // - // It is important to remember that the result of the execution of this method, - // must be the game state (in our case our rect, speed and direction variables) updated to be - // consistent of how it should be after the amount of time stored on the dt variable, - // that way your game will always run smooth and consistent even when a FPS drop or peak happen. - // - // To illustrate this, if our square moves at 200 logical pixels per second, and half a second - // has passed, our square should have moved 100 logical pixels on this iteration - squarePos = squarePos.translate(squareSpeed * squareDirection * dt, 0); - - // This simple condition verifies if the square is going right, and has reached the end of the - // screen and if so, we invert the direction. - // - // Note here that we have used the variable size, which is a variable provided - // by the Game class which contains the size in logical pixels that the game is currently using. - if (squareDirection == 1 && squarePos.right > size.x) { - squareDirection = -1; - // This does the same, but now checking the left direction - } else if (squareDirection == -1 && squarePos.left < 0) { - squareDirection = 1; - } - } -``` - -If we run our game again, we should see our square bouncing back and forth like we wanted from the -beginning. - -And that is it for this basic tutorial, in which we have covered the basics of Flame, its basic -classes and some basic rendering. From that we can start to build more complex things and more -exciting games. diff --git a/tutorials/1_basic_square/code/.gitignore b/tutorials/1_basic_square/code/.gitignore deleted file mode 100644 index d270a1a75..000000000 --- a/tutorials/1_basic_square/code/.gitignore +++ /dev/null @@ -1,50 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -android -ios -web diff --git a/tutorials/1_basic_square/code/.metadata b/tutorials/1_basic_square/code/.metadata deleted file mode 100644 index cd8c5b8b7..000000000 --- a/tutorials/1_basic_square/code/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 044f2cf5607a26f8818dab0f766400e85c52bdff - channel: beta - -project_type: app diff --git a/tutorials/1_basic_square/code/README.md b/tutorials/1_basic_square/code/README.md deleted file mode 100644 index cff43688e..000000000 --- a/tutorials/1_basic_square/code/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# basic_square - -Code for the Basic Square tutorial diff --git a/tutorials/1_basic_square/code/lib/main.dart b/tutorials/1_basic_square/code/lib/main.dart deleted file mode 100644 index 006e3f8c9..000000000 --- a/tutorials/1_basic_square/code/lib/main.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flame/palette.dart'; -import 'package:flutter/material.dart'; -import 'package:flame/game.dart'; - -void main() { - final myGame = MyGame(); - runApp( - GameWidget( - game: myGame, - ), - ); -} - -class MyGame extends Game { - static const int squareSpeed = 400; - static final squarePaint = BasicPalette.white.paint(); - late Rect squarePos; - int squareDirection = 1; - - @override - Future onLoad() async { - squarePos = Rect.fromLTWH(0, 0, 100, 100); - } - - @override - void update(double dt) { - squarePos = squarePos.translate(squareSpeed * squareDirection * dt, 0); - - if (squareDirection == 1 && squarePos.right > size.x) { - squareDirection = -1; - } else if (squareDirection == -1 && squarePos.left < 0) { - squareDirection = 1; - } - } - - @override - void render(Canvas canvas) { - canvas.drawRect(squarePos, squarePaint); - } -} diff --git a/tutorials/1_basic_square/code/pubspec.yaml b/tutorials/1_basic_square/code/pubspec.yaml deleted file mode 100644 index e5970425e..000000000 --- a/tutorials/1_basic_square/code/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: basic_square -description: Example code for the the tutorial (Basic Rendering a simple square on the screen) - -version: 1.0.0+1 - -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - -dependencies: - flame: - path: ../../../packages/flame - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - dart_code_metrics: ^2.4.0 diff --git a/tutorials/1_basic_square/media/preview.gif b/tutorials/1_basic_square/media/preview.gif deleted file mode 100644 index 9c56c8eba..000000000 Binary files a/tutorials/1_basic_square/media/preview.gif and /dev/null differ diff --git a/tutorials/2_sprite_animations_gestures/README.md b/tutorials/2_sprite_animations_gestures/README.md deleted file mode 100644 index 8b8c18684..000000000 --- a/tutorials/2_sprite_animations_gestures/README.md +++ /dev/null @@ -1,230 +0,0 @@ -# Basic: Sprites, Animations and Gestures - -This tutorial will introduce you to: - - - `Sprite`: Sprites are how we draw images, or portions of an image in Flame. - - `SpriteAnimation`: SpriteAnimations are animations composed from sprites, where each sprite represents a frame. - - Gesture input: the most basic input for your game, the tap detector. - -All the code of this tutorial code can be found [here](./code). - -By the end of this tutorial, you will have built a simple game which renders a button that, when pressed, makes a small vampire robot run. It will look like this: - -![Preview](./media/preview.gif) - -## Sprite - -Before starting coding our game, it is important to understand what sprites are and what they are used for. - -Sprites are images, or a portion of an image, loaded into the memory, and can then be used to render graphics on your game canvas. - -Sprites can be (and usually are) bundled into single images, called Sprite Sheets. That is a very useful technique as it lowers the amount of I/O operations needed to load the game assets because it is faster to load 1 image of 10 KB than to load 10 images of 1 KB each (among other advantages). - -For example, on this tutorial, we will have a button that makes our robot run. This button needs two sprites, for the unpressed and pressed states. We can have the following image containing both, this technique is often called sprite sheet. - -![Sprite example](code/assets/images/buttons.png) - - -## Animations - -Animation is what gives 2D games life. Flame provides a handy class called `SpriteAnimation` which lets you create an animation out of a list of sprites representing each frame, in sequence. Animations are usually bundled in a single Sprite Sheet, like this one: - -![Animation example](code/assets/images/robot.png) - -Flame provides a way for easily turning that Sprite Sheet into an animation (we will see how in a few moments). - -## Hands on - -To get started, let's get a Flame `Game` instance running with a structure similar to our [first tutorial](https://github.com/flame-engine/flame/tree/main/tutorials/1_basic_square#building-your-first-game) (if you haven't yet, you can follow it to better understand this initial setup). - -```dart -void main() { - final myGame = MyGame(); - runApp(GameWidget(game: myGame)); -} - -class MyGame extends Game { - - @override - void update(double dt) { - } - - @override - void render(Canvas canvas) { - } - - @override - Color backgroundColor() => const Color(0xFF222222); -} -``` - -Great! This will just gets us a plain, almost black screen. Now lets add our running robot: - -```dart -class MyGame extends Game { - late SpriteAnimation runningRobot; - - // Vector2 is a class from `package:vector_math/vector_math_64.dart` and is widely used - // in Flame to represent vectors. Here we need two vectors, one to define where we are - // going to draw our robot and another one to define its size - final robotPosition = Vector2(240, 50); - final robotSize = Vector2(48, 60); - - // Now, on the `onLoad` method, we need to load our animation. To do that we can use the - // `loadSpriteAnimation` method, which is present on our game class. - @override - Future onLoad() async { - runningRobot = await loadSpriteAnimation( - 'robot.png', - // `SpriteAnimationData` is a class used to tell Flame how the animation Sprite Sheet - // is organized. In this case we are describing that our frames are laid out in a horizontal - // sequence on the image, that there are 8 frames, that each frame is a sprite of 16x18 pixels, - // and, finally, that each frame should appear for 0.1 seconds when the animation is running. - SpriteAnimationData.sequenced( - amount: 8, - textureSize: Vector2(16, 18), - stepTime: 0.1, - ), - ); - } - - @override - void update(double dt) { - // Here we just need to "hook" our animation into the game loop update method so the current frame is updated with the specified frequency - runningRobot.update(dt); - } - - @override - void render(Canvas canvas) { - // Since an animation is basically a list of sprites, to render it, we just need to get its - // current sprite and render it on our canvas. Which frame is the current sprite is updated on the `update` method. - runningRobot - .getSprite() - .render(canvas, position: robotPosition, size: robotSize); - } - - @override - Color backgroundColor() => const Color(0xFF222222); -} -``` - -When running the game now, you should see our vampire robot running endlessly on the screen. - -For the next step, let's implement our on/off button and render it on the screen. - -The first thing we need to do is to add a couple of variables needed to reference our button: - -```dart - // One sprite for each button state - late Sprite pressedButton; - late Sprite unpressedButton; - // Just like our robot needs its position and size, here we create two - // variables for the button as well - final buttonPosition = Vector2(200, 120); - final buttonSize = Vector2(120, 30); - // Simple boolean variable to tell if the button is pressed or not - bool isPressed = false; -``` - -Next, we can load our two sprites: - -```dart - @override - Future onLoad() async { - // runningRobot loading omitted - - // Just like we have a `loadSpriteAnimation` function, here we can use - // `loadSprite`. To use it, we just need to inform the asset's path - // and the position and size defining the section of the whole image - // that we want. If we wanted to have a sprite with the full image, `srcPosition` - // and `srcSize` could just be omitted - unpressedButton = await loadSprite( - 'buttons.png', - // `srcPosition` and `srcSize` here tells `loadSprite` that we want - // just a rect (starting at (0, 0) with the dimensions (60, 20)) of the image - // which gives us only the first button - srcPosition: Vector2.zero(), // this is zero by default - srcSize: Vector2(60, 20), - ); - - pressedButton = await loadSprite( - 'buttons.png', - // Same thing here, but now a rect starting at (0, 20) - // which gives us only the second button - srcPosition: Vector2(0, 20), - srcSize: Vector2(60, 20), - ); - } -``` - -Finally, we just render it on the game `render` function: - -```dart - @override - void render(Canvas canvas) { - // Running robot render omitted - - final button = isPressed ? pressedButton : unpressedButton; - button.render(canvas, position: buttonPosition, size: buttonSize); - } -``` - -You now should see the button on the screen, but right now, it is pretty much useless as it has no action at all. - -So, to change that, we will now add some interactivity to our game and make the button tappable/clickable. - -Flame provides several input handlers, about which you can check with more in depth on [our docs](https://github.com/flame-engine/flame/blob/main/doc/gesture-input.md). -For this tutorial, we will be using the `TapDetector` which enables us to detect taps on the screen, as well as mouse click when running on web or desktop. - -All Flame input detectors are mixins which can be added to your game, enabling you to override listener methods related to that detector. For the `TapDetector`, we will need to override three methods: - - - `onTapDown`: Called when touch/click has started, i.e., the user just touced the screen or clicked the mouse button. - - `onTapUp`: Called when the touch/click has stop occurring because the event was released, i.e., the user lifted the finger from the screen or released the mouse button. - - `onTapCancel`: Called when the event was cancelled. This can happen for several reasons; one of the most common is when the event has changed into another type, for example the user started to move the finger/mouse and the touch event now turned into a pan/drag. Usually, we can just treat this event as being the same as `onTapUp`. - -Now that we have a better understanding of `TapDetector` and the events that we will need to handle, let's implement it on the game: - -```dart -// We need to add our `TapDetector` mixin here -class MyGame extends Game with TapDetector { - // Variables declaration, onLoad and render methods omited... - - @override - void onTapDown(TapDownInfo event) { - // On tap down we need to check if the event ocurred on the - // button area. There are several ways of doing it, for this - // tutorial we do that by transforming ours position and size - // vectors into a dart:ui Rect by using the `&` operator, and - // with that rect we can use its `contains` method which checks - // if a point (Offset) is inside that rect - final buttonArea = buttonPosition & buttonSize; - - isPressed = buttonArea.contains(event.eventPosition.game.toOffset()); - } - - // On both tap up and tap cancel we just set the isPressed - // variable to false - @override - void onTapUp(TapUpInfo event) { - isPressed = false; - } - - @override - void onTapCancel() { - isPressed = false; - } - - // Finally, we just modify our update method so the animation is - // updated only if the button is pressed - @override - void update(double dt) { - if (isPressed) { - runningRobot.update(dt); - } - } -} -``` - -If we run our game again, we should be able to see the complete example, with our on/off button for our little vampire robot. - -And with that, we finished this tutorial. Now, with an understanding of sprites, animations and gestures, we can start on building more interactive and beautiful games. diff --git a/tutorials/2_sprite_animations_gestures/code/.gitignore b/tutorials/2_sprite_animations_gestures/code/.gitignore deleted file mode 100644 index d270a1a75..000000000 --- a/tutorials/2_sprite_animations_gestures/code/.gitignore +++ /dev/null @@ -1,50 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -android -ios -web diff --git a/tutorials/2_sprite_animations_gestures/code/.metadata b/tutorials/2_sprite_animations_gestures/code/.metadata deleted file mode 100644 index cd8c5b8b7..000000000 --- a/tutorials/2_sprite_animations_gestures/code/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 044f2cf5607a26f8818dab0f766400e85c52bdff - channel: beta - -project_type: app diff --git a/tutorials/2_sprite_animations_gestures/code/README.md b/tutorials/2_sprite_animations_gestures/code/README.md deleted file mode 100644 index a6e52d75d..000000000 --- a/tutorials/2_sprite_animations_gestures/code/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# basic_sprites_animations - -Code for the Basic Sprites, Animations and Gestures diff --git a/tutorials/2_sprite_animations_gestures/code/assets/images/buttons.png b/tutorials/2_sprite_animations_gestures/code/assets/images/buttons.png deleted file mode 100644 index 4d55dfbae..000000000 Binary files a/tutorials/2_sprite_animations_gestures/code/assets/images/buttons.png and /dev/null differ diff --git a/tutorials/2_sprite_animations_gestures/code/assets/images/robot.png b/tutorials/2_sprite_animations_gestures/code/assets/images/robot.png deleted file mode 100644 index 78cdf5077..000000000 Binary files a/tutorials/2_sprite_animations_gestures/code/assets/images/robot.png and /dev/null differ diff --git a/tutorials/2_sprite_animations_gestures/code/lib/main.dart b/tutorials/2_sprite_animations_gestures/code/lib/main.dart deleted file mode 100644 index 64c00a17d..000000000 --- a/tutorials/2_sprite_animations_gestures/code/lib/main.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flame/game.dart'; -import 'package:flame/input.dart'; -import 'package:flame/sprite.dart'; -import 'package:flutter/material.dart'; - -void main() { - final myGame = MyGame(); - runApp(GameWidget(game: myGame)); -} - -class MyGame extends Game with TapDetector { - late SpriteAnimation runningRobot; - late Sprite pressedButton; - late Sprite unpressedButton; - - bool isPressed = false; - - final buttonPosition = Vector2(200, 120); - final buttonSize = Vector2(120, 30); - - final robotPosition = Vector2(240, 50); - final robotSize = Vector2(48, 60); - - @override - Future onLoad() async { - runningRobot = await loadSpriteAnimation( - 'robot.png', - SpriteAnimationData.sequenced( - amount: 8, - stepTime: 0.1, - textureSize: Vector2(16, 18), - ), - ); - - unpressedButton = await loadSprite( - 'buttons.png', - srcPosition: Vector2.zero(), - srcSize: Vector2(60, 20), - ); - - pressedButton = await loadSprite( - 'buttons.png', - srcPosition: Vector2(0, 20), - srcSize: Vector2(60, 20), - ); - } - - @override - void onTapDown(TapDownInfo info) { - final buttonArea = buttonPosition & buttonSize; - - isPressed = buttonArea.contains(info.eventPosition.game.toOffset()); - } - - @override - void onTapUp(TapUpInfo info) { - isPressed = false; - } - - @override - void onTapCancel() { - isPressed = false; - } - - @override - void update(double dt) { - if (isPressed) { - runningRobot.update(dt); - } - } - - @override - void render(Canvas canvas) { - runningRobot - .getSprite() - .render(canvas, position: robotPosition, size: robotSize); - - final button = isPressed ? pressedButton : unpressedButton; - button.render(canvas, position: buttonPosition, size: buttonSize); - } - - @override - Color backgroundColor() => const Color(0xFF222222); -} diff --git a/tutorials/2_sprite_animations_gestures/code/pubspec.yaml b/tutorials/2_sprite_animations_gestures/code/pubspec.yaml deleted file mode 100644 index c2e5622a1..000000000 --- a/tutorials/2_sprite_animations_gestures/code/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: basic_sprites_animations -description: Example code for the tutorial - -version: 1.0.0+1 - -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - -dependencies: - flame: - path: ../../../packages/flame - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - dart_code_metrics: ^2.4.0 - -flutter: - assets: - - assets/images/ diff --git a/tutorials/2_sprite_animations_gestures/media/preview.gif b/tutorials/2_sprite_animations_gestures/media/preview.gif deleted file mode 100644 index 8cc21aedd..000000000 Binary files a/tutorials/2_sprite_animations_gestures/media/preview.gif and /dev/null differ diff --git a/tutorials/README.md b/tutorials/README.md deleted file mode 100644 index dae4600d2..000000000 --- a/tutorials/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Flame tutorials - -This repository includes a collection of official tutorials created and maintained by the Flame team. Use the index below to navigate through the available tutorials. - - - [1. Basic: Rendering a simple square on the screen](./1_basic_square/README.md) - - [2. Basic: Sprites, Animations and Gestures](./2_sprite_animations_gestures/README.md)