Skip to content

Commit

Permalink
feat: Add HasWorldReference mixin (#2746)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spydon committed Sep 20, 2023
1 parent d2c9dce commit 9105411
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
22 changes: 22 additions & 0 deletions doc/flame/components.md
Expand Up @@ -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<MyWorld>,
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
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Expand Up @@ -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';
Expand Down
41 changes: 41 additions & 0 deletions 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<T extends World> 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;
}
}
103 changes: 103 additions & 0 deletions 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<World>();
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<MockWorld>());
});
});
}

class _ReferenceWorld extends World {
bool calledFoo = false;
void foo() => calledFoo = true;
}

class _Component<T extends World> extends Component with HasWorldReference<T> {}

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 {}

0 comments on commit 9105411

Please sign in to comment.