Skip to content

Commit

Permalink
feat: Added HasGameReference mixin (#1828)
Browse files Browse the repository at this point in the history
This almost exactly like the current HasGameRef mixin, except that:

    The property is called game instead of gameRef (the "gameRef" violates Dart naming conventions against using abbreviations in variable names);
    The template type T supports all Games, not only FlameGames;
    Better integration with the SingleGameInstance mixin;
    The new mixin is within experimental, to reduce chance that it will confuse the users.
  • Loading branch information
st-pasha authored Oct 3, 2022
1 parent 45a9d79 commit 12ce270
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 52 deletions.
8 changes: 4 additions & 4 deletions doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ children are rendered and updated with the same conditions.
Example of usage, where visibility of two components are handled by a wrapper:

```dart
class GameOverPanel extends PositionComponent with HasGameRef<MyGame> {
class GameOverPanel extends PositionComponent {
bool visible = false;
final Image spriteImage;
Expand Down Expand Up @@ -436,10 +436,10 @@ class MyGame extends FlameGame {
// Vector2(0.0, 0.0) by default, can also be set in the constructor
player.position = ...
// 0 by default, can also be set in the constructor
player.angle = ...
// Adds the component
add(player);
}
Expand Down Expand Up @@ -746,7 +746,7 @@ Advanced example:
```dart
final images = [
loadParallaxImage(
'stars.jpg',
'stars.jpg',
repeat: ImageRepeat.repeat,
alignment: Alignment.center,
fill: LayerFill.width,
Expand Down
9 changes: 3 additions & 6 deletions doc/flame/examples/lib/value_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ class ValueRouteExample extends FlameGame with HasTappableComponents {
}
}

mixin HasGameReference on Component {
ValueRouteExample get game => findGame()! as ValueRouteExample;
}

class HomePage extends Component with HasGameReference {
class HomePage extends Component with HasGameReference<ValueRouteExample> {
@override
Future<void> onLoad() async {
add(
Expand All @@ -48,7 +44,8 @@ class HomePage extends Component with HasGameReference {
}
}

class RateRoute extends ValueRoute<int> with HasGameReference {
class RateRoute extends ValueRoute<int>
with HasGameReference<ValueRouteExample> {
RateRoute() : super(value: -1, transparent: true);

@override
Expand Down
15 changes: 6 additions & 9 deletions doc/flame/inputs/other-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,25 @@ class MyGame extends FlameGame with HasDraggables {
}
class JoystickPlayer extends SpriteComponent with HasGameRef {
JoystickPlayer(this.joystick)
: super(
anchor: Anchor.center,
size: Vector2.all(100.0),
);
/// Pixels/s
double maxSpeed = 300.0;
final JoystickComponent joystick;
JoystickPlayer(this.joystick)
: super(
size: Vector2.all(100.0),
) {
anchor = Anchor.center;
}
@override
Future<void> onLoad() async {
super.onLoad();
sprite = await gameRef.loadSprite('layers/player.png');
position = gameRef.size / 2;
}
@override
void update(double dt) {
super.update(dt);
if (joystick.direction != JoystickDirection.idle) {
position.add(joystick.velocity * maxSpeed * dt);
angle = joystick.delta.screenAngle();
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export 'src/components/mixins/draggable.dart';
export 'src/components/mixins/gesture_hitboxes.dart';
export 'src/components/mixins/has_ancestor.dart';
export 'src/components/mixins/has_decorator.dart' show HasDecorator;
export 'src/components/mixins/has_game_ref.dart';
export 'src/components/mixins/has_game_ref.dart' show HasGameRef;
export 'src/components/mixins/has_paint.dart';
export 'src/components/mixins/hoverable.dart';
export 'src/components/mixins/keyboard_handler.dart';
Expand Down
1 change: 1 addition & 0 deletions packages/flame/lib/experimental.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export 'src/experimental/geometry/shapes/rectangle.dart' show Rectangle;
export 'src/experimental/geometry/shapes/rounded_rectangle.dart'
show RoundedRectangle;
export 'src/experimental/geometry/shapes/shape.dart' show Shape;
export 'src/experimental/has_game_reference.dart' show HasGameReference;
export 'src/experimental/max_viewport.dart' show MaxViewport;
export 'src/experimental/viewfinder.dart' show Viewfinder;
export 'src/experimental/viewport.dart' show Viewport;
Expand Down
5 changes: 3 additions & 2 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flame/src/cache/value_cache.dart';
import 'package:flame/src/components/core/component_set.dart';
import 'package:flame/src/components/core/position_type.dart';
import 'package:flame/src/components/mixins/coordinate_transform.dart';
import 'package:flame/src/components/mixins/has_game_ref.dart';
import 'package:flame/src/game/flame_game.dart';
import 'package:flame/src/game/game.dart';
import 'package:flame/src/gestures/events.dart';
Expand Down Expand Up @@ -375,8 +376,8 @@ class Component {
/// - it is invoked when the size of the game canvas is already known.
///
/// If your loading logic requires knowing the size of the game canvas, then
/// add `HasGameRef` mixin and then query `gameRef.size` or
/// `gameRef.canvasSize`.
/// add [HasGameRef] mixin and then query `game.size` or
/// `game.canvasSize`.
///
/// The default implementation returns `null`, indicating that there is no
/// need to await anything. When overriding this method, you have a choice
Expand Down
76 changes: 47 additions & 29 deletions packages/flame/lib/src/components/mixins/has_game_ref.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,59 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/experimental/has_game_reference.dart';
import 'package:flame/src/game/flame_game.dart';
import 'package:flame/src/game/game.dart';
import 'package:flame/src/game/mixins/single_game_instance.dart';
import 'package:meta/meta.dart';

/// [HasGameRef] mixin provides property [game] (or [gameRef]), which is the
/// cached accessor for the top-level game instance.
///
/// The type [T] on the mixin is the type of your game class. This type will be
/// the type of the [game] reference, and the mixin will check at runtime that
/// the actual type matches the expectation.
///
/// [HasGameReference] is a newer version of this mixin, and will replace it in
/// Flame v2.0.
mixin HasGameRef<T extends FlameGame> on Component {
T? _gameRef;
T? _game;

T get gameRef {
if (_gameRef == null) {
var c = parent;
while (c != null) {
if (c is HasGameRef<T>) {
_gameRef = c.gameRef;
return _gameRef!;
} else if (c is T) {
_gameRef = c;
return c;
} else {
c = c.parent;
}
}
throw StateError('Cannot find reference $T in the component tree');
}
return _gameRef!;
}
/// Reference to the top-level Game instance that owns this component.
///
/// This property is accessible in the component's `onLoad` and later. It may
/// be accessible earlier too, but only if your game uses the
/// [SingleGameInstance] mixin.
T get game => _game ??= _findGameAndCheck();

@override
void onRemove() {
super.onRemove();
_gameRef = null;
}
/// Allows you to set the game instance explicitly. This may be useful in
/// tests, or if you're planning to move the component to another game
/// instance.
set game(T? value) => _game = value;

/// Equivalent to the [game] property.
T get gameRef => game;

/// Directly assigns (and override if one is already set) a [gameRef] to the
/// component.
///
/// This is meant to be used only for testing purposes.
@visibleForTesting
void mockGameRef(T gameRef) {
_gameRef = gameRef;
@Deprecated('Use .game setter instead. Will be removed in 1.5.0')
void mockGameRef(T gameRef) => _game = gameRef;

@override
Game? findGame() => _game ?? super.findGame();

T _findGameAndCheck() {
final game = findGame();
assert(
game != null,
'Could not find Game instance: the component is detached from the '
'component tree',
);
assert(
game! is T,
'Found game of type ${game.runtimeType}, while type $T was expected',
);
return game! as T;
}
}
46 changes: 46 additions & 0 deletions packages/flame/lib/src/experimental/has_game_reference.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/components/mixins/has_game_ref.dart';
import 'package:flame/src/game/game.dart';
import 'package:flame/src/game/mixins/single_game_instance.dart';

/// [HasGameReference] mixin provides property [game], which is the cached
/// accessor for the top-level game instance.
///
/// The type [T] on the mixin is the type of your game class. This type will be
/// the type of the [game] reference, and the mixin will check at runtime that
/// the actual type matches the expectation.
///
/// This class is equivalent to [HasGameRef] in all respects except that its
/// generic parameter [T] can be any [Game], not just a "FlameGame".
mixin HasGameReference<T extends Game> on Component {
T? _game;

/// Reference to the top-level Game instance that owns this component.
///
/// This property is accessible in the component's `onLoad` and later. It may
/// be accessible earlier too, but only if your game uses the
/// [SingleGameInstance] mixin.
T get game => _game ??= _findGameAndCheck();

/// Allows you to set the game instance explicitly. This may be useful in
/// tests, or if you're planning to move the component to another game
/// instance.
set game(T? value) => _game = value;

@override
Game? findGame() => _game ?? super.findGame();

T _findGameAndCheck() {
final game = findGame();
assert(
game != null,
'Could not find Game instance: the component is detached from the '
'component tree',
);
assert(
game! is T,
'Found game of type ${game.runtimeType}, while type $T was expected',
);
return game! as T;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ void main() {
final component = _BarComponent();
await game.ensureAdd(component);

component.mockGameRef(MockFlameGame());
component.game = MockFlameGame();

expect(component.gameRef, isA<MockFlameGame>());
});
Expand Down
122 changes: 122 additions & 0 deletions packages/flame/test/experimental/has_game_reference_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'package:flame/components.dart';
import 'package:flame/experimental.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('HasGameReference', () {
testWithFlameGame(
'component with default HasGameReference',
(game) async {
final component1 = _Component<FlameGame>();
final component2 = _Component<Game>();
game.addAll([component1, component2]);
expect(component1.game, game);
expect(component2.game, game);
},
);

testWithGame<_MyGame>(
'component with typed HasGameReference',
_MyGame.new,
(game) async {
final component = _Component<_MyGame>();
game.add(component);
expect(component.game, game);
},
);

testWithFlameGame(
'game reference accessed too early',
(game) async {
final component = _Component();
expect(
() => component.game,
failsAssert(
'Could not find Game instance: the component is detached from the '
'component tree',
),
);
},
);

testWithFlameGame(
'game reference of wrong type',
(game) async {
final component = _Component<_MyGame>();
game.add(component);
expect(
() => component.game,
failsAssert(
'Found game of type FlameGame, while type _MyGame was expected',
),
);
},
);

testWithFlameGame(
'game reference can be set explicitly',
(game) async {
final component = _Component<FlameGame>();
component.game = game;
expect(component.game, game);

component.game = null;
expect(
() => component.game,
failsAssert(
'Could not find Game instance: the component is detached from the '
'component tree',
),
);
},
);

testWithFlameGame(
'game reference propagates quickly',
(game) async {
final component1 = _Component()..addToParent(game);
final component2 = _Component()..addToParent(component1);
final component3 = _Component()..addToParent(component2);
expect(component3.game, game);
},
);

testWithGame<_MyGame>('simple test', _MyGame.new, (game) async {
final c = _FooComponent();
game.add(c);
c.foo();
expect(game.calledFoo, true);
});

testWithGame<_MyGame>('gameRef can be mocked', _MyGame.new, (game) async {
final component = _BarComponent();
await game.ensureAdd(component);

component.game = MockFlameGame();

expect(component.game, isA<MockFlameGame>());
});
});
}

class _Component<T extends Game> extends Component with HasGameReference<T> {}

class _MyGame extends FlameGame {
bool calledFoo = false;
void foo() {
calledFoo = true;
}
}

class _FooComponent extends Component with HasGameReference<_MyGame> {
void foo() {
game.foo();
}
}

class _BarComponent extends Component with HasGameReference<_MyGame> {}

class MockFlameGame extends Mock implements _MyGame {}

0 comments on commit 12ce270

Please sign in to comment.