mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 12:28:03 +08:00
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.
This commit is contained in:
@ -189,6 +189,28 @@ that they will appear in the children list in the same order as they were
|
|||||||
scheduled for addition.
|
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
|
### Ensuring a component has a given parent
|
||||||
|
|
||||||
When a component requires to be added to a specific parent type the
|
When a component requires to be added to a specific parent type the
|
||||||
|
|||||||
@ -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_paint.dart';
|
||||||
export 'src/components/mixins/has_time_scale.dart';
|
export 'src/components/mixins/has_time_scale.dart';
|
||||||
export 'src/components/mixins/has_visibility.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/hoverable.dart';
|
||||||
export 'src/components/mixins/keyboard_handler.dart';
|
export 'src/components/mixins/keyboard_handler.dart';
|
||||||
export 'src/components/mixins/notifier.dart';
|
export 'src/components/mixins/notifier.dart';
|
||||||
|
|||||||
41
packages/flame/lib/src/components/mixins/has_world.dart
Normal file
41
packages/flame/lib/src/components/mixins/has_world.dart
Normal file
@ -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
packages/flame/test/experimental/has_world_test.dart
Normal file
103
packages/flame/test/experimental/has_world_test.dart
Normal file
@ -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 {}
|
||||||
Reference in New Issue
Block a user