Skip to content

Commit

Permalink
feat: Component visibility (HasVisibility mixin) (#2681)
Browse files Browse the repository at this point in the history
This PR introduces a new HasVisibility mixin on Component. It prevents the renderTree method from progressing if the isVisible property is false. It therefore prevents the component and all it's children from rendering.

The purpose of this mixin is to allow showing and hiding a component without removing it from the tree.

An important note is that the component (and all it's children) will still be on the tree. They will continue to receive update events, and all other lifecycle events. If the user has implemented input such as tap events, or collision detection, or any other interactivity, this will continue to operate without affect (unless it relies on the render step - such as per-pixel collision detection).

This is expected behaviour. If it is not desired, the user needs to account for it (by checking isVisible in their code) or someone needs to create another mixin for HasEnabled or HasActive which would prevent these other actions 😁

I am very happy to make changes, take suggestions, etc!
  • Loading branch information
projectitis committed Aug 26, 2023
1 parent 158fc34 commit 76405da
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 2 deletions.
78 changes: 76 additions & 2 deletions doc/flame/components.md
Expand Up @@ -363,10 +363,84 @@ 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
overridden, 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.
Expand Down
7 changes: 7 additions & 0 deletions examples/lib/stories/components/components.dart
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
);
}
30 changes: 30 additions & 0 deletions examples/lib/stories/components/has_visibility_example.dart
@@ -0,0 +1,30 @@
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<void> onLoad() async {
final flameLogoComponent = LogoComponent(await loadSprite('flame.png'));
add(flameLogoComponent);

// Toggle visibility every second
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);
}
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Expand Up @@ -24,6 +24,7 @@ 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';
Expand Down
32 changes: 32 additions & 0 deletions 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);
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions 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<void> 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);
}
}

0 comments on commit 76405da

Please sign in to comment.