-
-
Notifications
You must be signed in to change notification settings - Fork 899
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
167 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |