From 9105411d46e097d4b5bf84ee8921c146dcf5a6cd Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 20 Sep 2023 15:22:57 +0200 Subject: [PATCH] feat: Add `HasWorldReference` mixin (#2746) Adds a mixin for components similar to `HasAncestor`, `HasParent` and `HasGameReference` but which provides access to the `World` which the component has as an ancestor. --- doc/flame/components.md | 22 ++++ packages/flame/lib/components.dart | 1 + .../lib/src/components/mixins/has_world.dart | 41 +++++++ .../test/experimental/has_world_test.dart | 103 ++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 packages/flame/lib/src/components/mixins/has_world.dart create mode 100644 packages/flame/test/experimental/has_world_test.dart diff --git a/doc/flame/components.md b/doc/flame/components.md index 06afa86c0..36d14dc51 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -189,6 +189,28 @@ that they will appear in the children list in the same order as they were scheduled for addition. +### Access to the World from a Component + +If a component that has a `World` as an ancestor and requires access to that `World` object, one can +use the `HasWorldReference` mixin. + +Example: + +```dart +class MyComponent extends Component with HasWorldReference, + TapCallbacks { + @override + void onTapDown(TapDownEvent info) { + // world is of type MyWorld + world.add(AnotherComponent()); + } +} +``` + +If you try to access `world` from a component that doesn't have a `World` +ancestor of the correct type an assertion error will be thrown. + + ### Ensuring a component has a given parent When a component requires to be added to a specific parent type the diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index f97d2ebea..dfd4834b9 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -27,6 +27,7 @@ export 'src/components/mixins/has_game_reference.dart' show HasGameReference; 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/has_world.dart'; export 'src/components/mixins/hoverable.dart'; export 'src/components/mixins/keyboard_handler.dart'; export 'src/components/mixins/notifier.dart'; diff --git a/packages/flame/lib/src/components/mixins/has_world.dart b/packages/flame/lib/src/components/mixins/has_world.dart new file mode 100644 index 000000000..0efa66ed7 --- /dev/null +++ b/packages/flame/lib/src/components/mixins/has_world.dart @@ -0,0 +1,41 @@ +import 'package:collection/collection.dart'; +import 'package:flame/camera.dart'; +import 'package:flame/src/components/core/component.dart'; +import 'package:meta/meta.dart'; + +/// [HasWorldReference] mixin provides the [world] property, which is the cached +/// accessor for the world instance that this component belongs to. +/// +/// The type [T] on the mixin is the type of your world class. This type will be +/// the type of the [world] reference, and the mixin will check at runtime that +/// the actual type matches the expectation. +mixin HasWorldReference on Component { + T? _world; + + /// Reference to the [World] instance that this component belongs to. + T get world => _world ??= _findWorldAndCheck(); + + /// Allows you to set the world instance explicitly. + /// This may be useful in tests. + @visibleForTesting + set world(T? value) => _world = value; + + T? findWorld() { + return ancestors(includeSelf: true) + .firstWhereOrNull((ancestor) => ancestor is T) as T?; + } + + T _findWorldAndCheck() { + final world = findWorld(); + assert( + world != null, + 'Could not find a World instance of type $T', + ); + return world!; + } + + @override + void onRemove() { + _world = null; + } +} diff --git a/packages/flame/test/experimental/has_world_test.dart b/packages/flame/test/experimental/has_world_test.dart new file mode 100644 index 000000000..ecf0e007e --- /dev/null +++ b/packages/flame/test/experimental/has_world_test.dart @@ -0,0 +1,103 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +void main() { + group('HasWorldReference', () { + testWithGame( + 'component with default HasWorldReference', + () => FlameGame(world: _ReferenceWorld()), + (game) async { + final component1 = _Component(); + final component2 = _Component<_ReferenceWorld>(); + game.world.addAll([component1, component2]); + expect(component1.world, game.world); + expect(component2.world, game.world); + }, + ); + + testWithGame<_MyGame>( + 'component with typed HasWorldReference', + _MyGame.new, + (game) async { + final component = _Component<_ReferenceWorld>(); + game.world.ensureAdd(component); + expect(component.world, game.world); + }, + ); + + testWithFlameGame( + 'world reference accessed too early', + (game) async { + final component = _Component(); + expect( + () => component.world, + failsAssert('Could not find a World instance of type World'), + ); + }, + ); + + testWithFlameGame( + 'game reference of wrong type', + (game) async { + final component = _Component<_ReferenceWorld>(); + game.world.add(component); + expect( + () => component.world, + failsAssert( + 'Could not find a World instance of type _ReferenceWorld', + ), + ); + }, + ); + + testWithFlameGame( + 'game reference propagates quickly', + (game) async { + final component1 = _Component()..addToParent(game.world); + final component2 = _Component()..addToParent(component1); + final component3 = _Component()..addToParent(component2); + expect(component3.world, game.world); + }, + ); + + testWithGame<_MyGame>('simple test', _MyGame.new, (game) async { + final c = _FooComponent(); + game.world.add(c); + c.foo(); + expect(c.world.calledFoo, isTrue); + }); + + testWithGame<_MyGame>('gameRef can be mocked', _MyGame.new, (game) async { + final component = _BarComponent(); + await game.world.ensureAdd(component); + + component.world = MockWorld(); + + expect(component.world, isA()); + }); + }); +} + +class _ReferenceWorld extends World { + bool calledFoo = false; + void foo() => calledFoo = true; +} + +class _Component extends Component with HasWorldReference {} + +class _MyGame extends FlameGame { + _MyGame() : super(world: _ReferenceWorld()); +} + +class _FooComponent extends Component with HasWorldReference<_ReferenceWorld> { + void foo() { + world.foo(); + } +} + +class _BarComponent extends Component with HasWorldReference<_ReferenceWorld> {} + +class MockWorld extends Mock implements _ReferenceWorld {}