From 9a1a7f1479bd8c0dc73a9a76aed6fd469fecc399 Mon Sep 17 00:00:00 2001 From: projectitis Date: Sat, 26 Aug 2023 15:33:38 +1200 Subject: [PATCH 1/6] HasVisibility Mixin. Test. Example. Docs. --- doc/flame/components.md | 76 +++++++++++++++++- .../lib/stories/components/components.dart | 7 ++ .../components/has_visibility_example.dart | 31 +++++++ packages/flame/lib/components.dart | 1 + .../src/components/mixins/has_visibility.dart | 32 ++++++++ .../flame/test/_goldens/visibility_test_1.png | Bin 0 -> 1453 bytes .../mixins/has_visibility_test.dart | 59 ++++++++++++++ 7 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 examples/lib/stories/components/has_visibility_example.dart create mode 100644 packages/flame/lib/src/components/mixins/has_visibility.dart create mode 100644 packages/flame/test/_goldens/visibility_test_1.png create mode 100644 packages/flame/test/components/mixins/has_visibility_test.dart diff --git a/doc/flame/components.md b/doc/flame/components.md index 08a00088ab8..cf3aa638f3d 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -363,10 +363,82 @@ Do note that this setting is only respected if the component is added directly t `FlameGame` and not as a child component of another component. +### Visibility of components + +The recommended way to hide or show a component is usually to add or remove it from the tree +using the `add` and `remove` methods. + +However, adding and removing components from the tree will trigger lifecycle steps for that +component (such as calling `onRemove` and `onMount`). It is also an asynchronous process and care +needs to be taken to ensure the component has finished removing before it is added again if you +are removing and adding a component in quick succession. + +```dart +/// Example of handling the removal and adding of a child component +/// in quick succession +void show() async { + // Need to await the [removed] future first, just in case the + // component is still in the process of being removed. + await myChildComponent.removed; + add(myChildComponent); +} + +void hide() { + remove(myChildComponent); +} +``` + +These behaviors are not always desirable. + +An alternative method to show and hide a component is to use the `HasVisibility` mixin, which may +be used on any class that inherits from `Component`. This mixin introduces the `isVisible` property. +Simply set `isVisible` to `false` to hide the component, and `true` to show it again, without +removing it from the tree. This affects the visibility of the component and all it's descendants +(children). + +```dart +/// Example that implements HasVisibility +class MyComponent extends PositionComponent with HasVisibility {} + +/// Usage of the isVisible property +final myComponent = MyComponent(); +add(myComponent); + +myComponent.isVisible = false; +``` + +The mixin only affects whether the component is rendered, and will not affect other behaviors. + +```{note} +Important! Even when the component is not visible, it is still in the tree and will continue to +receive calls to 'update' and all other lifecycle events. It will still respond to input events, +and will still interact with other components, such as collision detection for example. +``` + +The mixin works by preventing the `renderTree` method, therefore if `renderTree` is being +implemented, a manual check for `isVisible` should be included to retain this functionality. + +```dart +class MyComponent extends PositionComponent with HasVisibility { + + @override + void renderTree(Canvas canvas) { + // Check for visibility + if (isVisible) { + // Custom code here + + // Continue rendering the tree + super.renderTree(canvas); + } + } +} +``` + ## PositionComponent -This class represent a positioned object on the screen, being a floating rectangle or a rotating -sprite. It can also represent a group of positioned components if children are added to it. +This class represents a positioned object on the screen, being a floating rectangle, a rotating +sprite, or anything else with position and size. It can also represent a group of positioned +components if children are added to it. The base of the `PositionComponent` is that it has a `position`, `size`, `scale`, `angle` and `anchor` which transforms how the component is rendered. diff --git a/examples/lib/stories/components/components.dart b/examples/lib/stories/components/components.dart index 8a344721060..ed0c31c28e1 100644 --- a/examples/lib/stories/components/components.dart +++ b/examples/lib/stories/components/components.dart @@ -5,6 +5,7 @@ import 'package:examples/stories/components/components_notifier_example.dart'; import 'package:examples/stories/components/components_notifier_provider_example.dart'; import 'package:examples/stories/components/composability_example.dart'; import 'package:examples/stories/components/debug_example.dart'; +import 'package:examples/stories/components/has_visibility_example.dart'; import 'package:examples/stories/components/keys_example.dart'; import 'package:examples/stories/components/look_at_example.dart'; import 'package:examples/stories/components/look_at_smooth_example.dart'; @@ -76,5 +77,11 @@ void addComponentsStories(Dashbook dashbook) { (_) => const KeysExampleWidget(), codeLink: baseLink('components/keys_example.dart'), info: KeysExampleWidget.description, + ) + ..add( + 'HasVisibility', + (_) => GameWidget(game: HasVisibilityExample()), + codeLink: baseLink('components/has_visibility_example.dart'), + info: HasVisibilityExample.description, ); } diff --git a/examples/lib/stories/components/has_visibility_example.dart b/examples/lib/stories/components/has_visibility_example.dart new file mode 100644 index 00000000000..9ea67922a89 --- /dev/null +++ b/examples/lib/stories/components/has_visibility_example.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flame/components.dart' hide Timer; +import 'package:flame/game.dart'; + +class HasVisibilityExample extends FlameGame { + static const String description = ''' + In this example we use the `HasVisibility` mixin to toggle the + visibility of a component without removing it from the parent + component. + This is a non-interactive example. + '''; + + @override + Future onLoad() async { + final flameLogoSprite = await loadSprite('flame.png'); + + final flameLogoComponent = LogoComponent(flameLogoSprite); + add(flameLogoComponent); + + const oneSecDuration = Duration(seconds: 1); + Timer.periodic( + oneSecDuration, + (Timer t) => + flameLogoComponent.isVisible = !flameLogoComponent.isVisible); + } +} + +class LogoComponent extends SpriteComponent with HasVisibility { + LogoComponent(Sprite sprite) : super(sprite: sprite, size: sprite.srcSize); +} diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 99ca5a4fb4a..6858a6a2be1 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -30,6 +30,7 @@ export 'src/components/mixins/notifier.dart'; export 'src/components/mixins/parent_is_a.dart'; export 'src/components/mixins/single_child_particle.dart'; export 'src/components/mixins/tappable.dart'; +export 'src/components/mixins/has_visibility.dart'; export 'src/components/nine_tile_box_component.dart'; export 'src/components/parallax_component.dart'; export 'src/components/particle_system_component.dart'; diff --git a/packages/flame/lib/src/components/mixins/has_visibility.dart b/packages/flame/lib/src/components/mixins/has_visibility.dart new file mode 100644 index 00000000000..faecb84dab1 --- /dev/null +++ b/packages/flame/lib/src/components/mixins/has_visibility.dart @@ -0,0 +1,32 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +/// A mixin that allows a component visibility to be toggled +/// without removing it from the tree. Visibility affects +/// the component and all it's children/descendants. +/// +/// Set [isVisible] to false to prevent the component and all +/// it's children from being rendered. +/// +/// The component will still respond as if it is on the tree, +/// including lifecycle and other events, but will simply +/// not render itself or it's children. +/// +/// If you are adding a custom implementation of the +/// [renderTree] method, make sure to wrap your render code +/// in a conditional. i.e.: +/// ``` +/// if (isVisible) { +/// // Custom render code here +/// } +/// ``` +mixin HasVisibility on Component { + bool isVisible = true; + + @override + void renderTree(Canvas canvas) { + if (isVisible) { + super.renderTree(canvas); + } + } +} diff --git a/packages/flame/test/_goldens/visibility_test_1.png b/packages/flame/test/_goldens/visibility_test_1.png new file mode 100644 index 0000000000000000000000000000000000000000..2c0d14e5ac517a721d096b0c51596b913f7eadaf GIT binary patch literal 1453 zcmeAS@N?(olHy`uVBq!ia0y~yVAKKP2^?%d5s}i^Y9Pf}9OUlAufBjDx%krWH+g8gR5Zc<-VQO5>z!c}d zpW(oE7VZ-QIttDk8h8#e8IjLgTe~DWM4f?9YS$ literal 0 HcmV?d00001 diff --git a/packages/flame/test/components/mixins/has_visibility_test.dart b/packages/flame/test/components/mixins/has_visibility_test.dart new file mode 100644 index 00000000000..4d10f18b01d --- /dev/null +++ b/packages/flame/test/components/mixins/has_visibility_test.dart @@ -0,0 +1,59 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../_resources/load_image.dart'; + +void main() { + group('HasVisibility', () { + testGolden( + 'Render a Component with isVisible set to false', + (game) async { + game.add(MyComponent()..mySprite.isVisible = false); + }, + size: Vector2(300, 400), + goldenFile: '../../_goldens/visibility_test_1.png', + ); + }); +} + +class MySpriteComponent extends PositionComponent with HasVisibility { + late final Sprite sprite; + + @override + Future onLoad() async { + sprite = Sprite(await loadImage('flame.png')); + } + + @override + void render(Canvas canvas) { + sprite.render(canvas, anchor: Anchor.center); + } +} + +/// This component contains a [MySpriteComponent]. It first +/// renders a rectangle, and then the children will render. +/// In this test the visibility of [mySprite] is set to +/// false, so only the rectangle is expected to be rendered. +class MyComponent extends PositionComponent { + MyComponent() : super(size: Vector2(300, 400)) { + mySprite = MySpriteComponent()..position = Vector2(150, 200); + add(mySprite); + } + late final MySpriteComponent mySprite; + + @override + void render(Canvas canvas) { + canvas.drawRect( + Rect.fromLTRB(25, 25, size.x - 25, size.y - 25), + Paint() + ..color = const Color(0xffffffff) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + super.render(canvas); + } +} From f21ec402a9cf7965413f9f707533379fd1692c61 Mon Sep 17 00:00:00 2001 From: projectitis Date: Sat, 26 Aug 2023 15:49:54 +1200 Subject: [PATCH 2/6] Markdown linting --- doc/flame/components.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/flame/components.md b/doc/flame/components.md index cf3aa638f3d..bfbb9abfcb6 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -410,9 +410,10 @@ myComponent.isVisible = false; The mixin only affects whether the component is rendered, and will not affect other behaviors. ```{note} -Important! Even when the component is not visible, it is still in the tree and will continue to -receive calls to 'update' and all other lifecycle events. It will still respond to input events, -and will still interact with other components, such as collision detection for example. +Important! Even when the component is not visible, it is still in the tree and +will continue to receive calls to 'update' and all other lifecycle events. It +will still respond to input events, and will still interact with other +components, such as collision detection for example. ``` The mixin works by preventing the `renderTree` method, therefore if `renderTree` is being @@ -434,6 +435,7 @@ class MyComponent extends PositionComponent with HasVisibility { } ``` + ## PositionComponent This class represents a positioned object on the screen, being a floating rectangle, a rotating From d898e6b989b2fa9d9534cbefab447cb7bd4a941a Mon Sep 17 00:00:00 2001 From: projectitis Date: Sat, 26 Aug 2023 15:53:16 +1200 Subject: [PATCH 3/6] Simplify example --- examples/lib/stories/components/has_visibility_example.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/lib/stories/components/has_visibility_example.dart b/examples/lib/stories/components/has_visibility_example.dart index 9ea67922a89..abe59170a24 100644 --- a/examples/lib/stories/components/has_visibility_example.dart +++ b/examples/lib/stories/components/has_visibility_example.dart @@ -13,11 +13,10 @@ class HasVisibilityExample extends FlameGame { @override Future onLoad() async { - final flameLogoSprite = await loadSprite('flame.png'); - - final flameLogoComponent = LogoComponent(flameLogoSprite); + final flameLogoComponent = LogoComponent(await loadSprite('flame.png')); add(flameLogoComponent); + // Toggle visibility every second const oneSecDuration = Duration(seconds: 1); Timer.periodic( oneSecDuration, From 487e83fc92001c7132b1d50fefaa64e92c76b384 Mon Sep 17 00:00:00 2001 From: projectitis Date: Sat, 26 Aug 2023 16:12:27 +1200 Subject: [PATCH 4/6] Lint warnings --- examples/lib/stories/components/has_visibility_example.dart | 2 +- packages/flame/lib/components.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/lib/stories/components/has_visibility_example.dart b/examples/lib/stories/components/has_visibility_example.dart index abe59170a24..7efbc8cc4f5 100644 --- a/examples/lib/stories/components/has_visibility_example.dart +++ b/examples/lib/stories/components/has_visibility_example.dart @@ -21,7 +21,7 @@ class HasVisibilityExample extends FlameGame { Timer.periodic( oneSecDuration, (Timer t) => - flameLogoComponent.isVisible = !flameLogoComponent.isVisible); + flameLogoComponent.isVisible = !flameLogoComponent.isVisible,); } } diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 6858a6a2be1..227b4a19138 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -24,13 +24,13 @@ export 'src/components/mixins/has_decorator.dart' show HasDecorator; export 'src/components/mixins/has_game_ref.dart' show HasGameRef; export 'src/components/mixins/has_paint.dart'; export 'src/components/mixins/has_time_scale.dart'; +export 'src/components/mixins/has_visibility.dart'; export 'src/components/mixins/hoverable.dart'; export 'src/components/mixins/keyboard_handler.dart'; export 'src/components/mixins/notifier.dart'; export 'src/components/mixins/parent_is_a.dart'; export 'src/components/mixins/single_child_particle.dart'; export 'src/components/mixins/tappable.dart'; -export 'src/components/mixins/has_visibility.dart'; export 'src/components/nine_tile_box_component.dart'; export 'src/components/parallax_component.dart'; export 'src/components/particle_system_component.dart'; From 800cc8c39e7b08607c1436e6e4328574e253c87f Mon Sep 17 00:00:00 2001 From: projectitis Date: Sat, 26 Aug 2023 16:16:44 +1200 Subject: [PATCH 5/6] Linting again --- examples/lib/stories/components/has_visibility_example.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/lib/stories/components/has_visibility_example.dart b/examples/lib/stories/components/has_visibility_example.dart index 7efbc8cc4f5..d44f8f5fc01 100644 --- a/examples/lib/stories/components/has_visibility_example.dart +++ b/examples/lib/stories/components/has_visibility_example.dart @@ -19,9 +19,9 @@ class HasVisibilityExample extends FlameGame { // Toggle visibility every second const oneSecDuration = Duration(seconds: 1); Timer.periodic( - oneSecDuration, - (Timer t) => - flameLogoComponent.isVisible = !flameLogoComponent.isVisible,); + oneSecDuration, + (Timer t) => flameLogoComponent.isVisible = !flameLogoComponent.isVisible, + ); } } From 035ddd0b5e389268d614ec2a89e9c1d8ec216d5f Mon Sep 17 00:00:00 2001 From: Peter Vullings Date: Sat, 26 Aug 2023 22:23:53 +1200 Subject: [PATCH 6/6] Update doc/flame/components.md Co-authored-by: Lukas Klingsbo --- doc/flame/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/flame/components.md b/doc/flame/components.md index bfbb9abfcb6..6922ef5ce61 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -417,7 +417,7 @@ components, such as collision detection for example. ``` The mixin works by preventing the `renderTree` method, therefore if `renderTree` is being -implemented, a manual check for `isVisible` should be included to retain this functionality. +overridden, a manual check for `isVisible` should be included to retain this functionality. ```dart class MyComponent extends PositionComponent with HasVisibility {