mirror of https://github.com/flutter/pinball.git
refactor: implemented `CameraFocusingBehavior` (#346)
parent
79da2e9234
commit
3465c9f1e7
@ -1,2 +1,3 @@
|
|||||||
export 'bumper_noisy_behavior.dart';
|
export 'bumper_noisy_behavior.dart';
|
||||||
|
export 'camera_focusing_behavior.dart';
|
||||||
export 'scoring_behavior.dart';
|
export 'scoring_behavior.dart';
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_bloc/flame_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
/// {@template focus_data}
|
||||||
|
/// Defines a [Camera] focus point.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class FocusData {
|
||||||
|
/// {@template focus_data}
|
||||||
|
FocusData({
|
||||||
|
required this.zoom,
|
||||||
|
required this.position,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The amount of zoom.
|
||||||
|
final double zoom;
|
||||||
|
|
||||||
|
/// The position of the camera.
|
||||||
|
final Vector2 position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the game focus when the [GameBloc] status changes.
|
||||||
|
class CameraFocusingBehavior extends Component
|
||||||
|
with ParentIsA<FlameGame>, BlocComponent<GameBloc, GameState> {
|
||||||
|
late final Map<String, FocusData> _foci;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool listenWhen(GameState? previousState, GameState newState) {
|
||||||
|
return previousState?.status != newState.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNewState(GameState state) {
|
||||||
|
switch (state.status) {
|
||||||
|
case GameStatus.waiting:
|
||||||
|
break;
|
||||||
|
case GameStatus.playing:
|
||||||
|
_zoom(_foci['game']!);
|
||||||
|
break;
|
||||||
|
case GameStatus.gameOver:
|
||||||
|
_zoom(_foci['backbox']!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
_foci = {
|
||||||
|
'game': FocusData(
|
||||||
|
zoom: parent.size.y / 16,
|
||||||
|
position: Vector2(0, -7.8),
|
||||||
|
),
|
||||||
|
'waiting': FocusData(
|
||||||
|
zoom: parent.size.y / 18,
|
||||||
|
position: Vector2(0, -112),
|
||||||
|
),
|
||||||
|
'backbox': FocusData(
|
||||||
|
zoom: parent.size.y / 10,
|
||||||
|
position: Vector2(0, -111),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
_snap(_foci['waiting']!);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snap(FocusData data) {
|
||||||
|
parent.camera
|
||||||
|
..speed = 100
|
||||||
|
..followVector2(data.position)
|
||||||
|
..zoom = data.zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _zoom(FocusData data) {
|
||||||
|
final zoom = CameraZoom(value: data.zoom);
|
||||||
|
zoom.completed.then((_) {
|
||||||
|
parent.camera.moveTo(data.position);
|
||||||
|
});
|
||||||
|
add(zoom);
|
||||||
|
}
|
||||||
|
}
|
@ -1,93 +0,0 @@
|
|||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// Adds helpers methods to Flame's [Camera].
|
|
||||||
extension CameraX on Camera {
|
|
||||||
/// Instantly apply the point of focus to the [Camera].
|
|
||||||
void snapToFocus(FocusData data) {
|
|
||||||
followVector2(data.position);
|
|
||||||
zoom = data.zoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [CameraZoom] that can be added to a [FlameGame].
|
|
||||||
CameraZoom focusToCameraZoom(FocusData data) {
|
|
||||||
final zoom = CameraZoom(value: data.zoom);
|
|
||||||
zoom.completed.then((_) {
|
|
||||||
moveTo(data.position);
|
|
||||||
});
|
|
||||||
return zoom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template focus_data}
|
|
||||||
/// Model class that defines a focus point of the camera.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class FocusData {
|
|
||||||
/// {@template focus_data}
|
|
||||||
FocusData({
|
|
||||||
required this.zoom,
|
|
||||||
required this.position,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The amount of zoom.
|
|
||||||
final double zoom;
|
|
||||||
|
|
||||||
/// The position of the camera.
|
|
||||||
final Vector2 position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template camera_controller}
|
|
||||||
/// A [Component] that controls its game camera focus.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class CameraController extends ComponentController<FlameGame> {
|
|
||||||
/// {@macro camera_controller}
|
|
||||||
CameraController(FlameGame component) : super(component) {
|
|
||||||
final gameZoom = component.size.y / 16;
|
|
||||||
final waitingBackboxZoom = component.size.y / 18;
|
|
||||||
final gameOverBackboxZoom = component.size.y / 10;
|
|
||||||
|
|
||||||
gameFocus = FocusData(
|
|
||||||
zoom: gameZoom,
|
|
||||||
position: Vector2(0, -7.8),
|
|
||||||
);
|
|
||||||
waitingBackboxFocus = FocusData(
|
|
||||||
zoom: waitingBackboxZoom,
|
|
||||||
position: Vector2(0, -112),
|
|
||||||
);
|
|
||||||
gameOverBackboxFocus = FocusData(
|
|
||||||
zoom: gameOverBackboxZoom,
|
|
||||||
position: Vector2(0, -111),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Game starts with the camera focused on the [Backbox].
|
|
||||||
component.camera
|
|
||||||
..speed = 100
|
|
||||||
..snapToFocus(waitingBackboxFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds the data for the game focus point.
|
|
||||||
late final FocusData gameFocus;
|
|
||||||
|
|
||||||
/// Holds the data for the waiting backbox focus point.
|
|
||||||
late final FocusData waitingBackboxFocus;
|
|
||||||
|
|
||||||
/// Holds the data for the game over backbox focus point.
|
|
||||||
late final FocusData gameOverBackboxFocus;
|
|
||||||
|
|
||||||
/// Move the camera focus to the game board.
|
|
||||||
void focusOnGame() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(gameFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the camera focus to the waiting backbox.
|
|
||||||
void focusOnWaitingBackbox() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(waitingBackboxFocus));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the camera focus to the game over backbox.
|
|
||||||
void focusOnGameOverBackbox() {
|
|
||||||
component.add(component.camera.focusToCameraZoom(gameOverBackboxFocus));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,126 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/behaviors/camera_focusing_behavior.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group(
|
||||||
|
'CameraFocusingBehavior',
|
||||||
|
() {
|
||||||
|
final flameTester = FlameTester(
|
||||||
|
EmptyPinballTestGame.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(
|
||||||
|
CameraFocusingBehavior(),
|
||||||
|
isA<CameraFocusingBehavior>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test('loads', (game) async {
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(behavior);
|
||||||
|
expect(game.contains(behavior), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'changes focus when loaded',
|
||||||
|
(game) async {
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
final previousZoom = game.camera.zoom;
|
||||||
|
expect(game.camera.follow, isNull);
|
||||||
|
|
||||||
|
await game.ensureAdd(behavior);
|
||||||
|
|
||||||
|
expect(game.camera.follow, isNotNull);
|
||||||
|
expect(game.camera.zoom, isNot(equals(previousZoom)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'listenWhen only listens when status changes',
|
||||||
|
(game) async {
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
const waiting = GameState.initial();
|
||||||
|
final playing =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||||
|
final gameOver =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.gameOver);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(waiting, waiting), isFalse);
|
||||||
|
expect(behavior.listenWhen(waiting, playing), isTrue);
|
||||||
|
expect(behavior.listenWhen(waiting, gameOver), isTrue);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(playing, playing), isFalse);
|
||||||
|
expect(behavior.listenWhen(playing, waiting), isTrue);
|
||||||
|
expect(behavior.listenWhen(playing, gameOver), isTrue);
|
||||||
|
|
||||||
|
expect(behavior.listenWhen(gameOver, gameOver), isFalse);
|
||||||
|
expect(behavior.listenWhen(gameOver, waiting), isTrue);
|
||||||
|
expect(behavior.listenWhen(gameOver, playing), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('onNewState', () {
|
||||||
|
flameTester.test(
|
||||||
|
'zooms when started playing',
|
||||||
|
(game) async {
|
||||||
|
final playing =
|
||||||
|
const GameState.initial().copyWith(status: GameStatus.playing);
|
||||||
|
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(behavior);
|
||||||
|
|
||||||
|
behavior.onNewState(playing);
|
||||||
|
final previousPosition = game.camera.position.clone();
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final zoom = behavior.children.whereType<CameraZoom>().single;
|
||||||
|
game.update(zoom.controller.duration!);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
expect(zoom.controller.completed, isTrue);
|
||||||
|
expect(
|
||||||
|
game.camera.position,
|
||||||
|
isNot(equals(previousPosition)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.test(
|
||||||
|
'zooms when game is over',
|
||||||
|
(game) async {
|
||||||
|
final playing = const GameState.initial().copyWith(
|
||||||
|
status: GameStatus.gameOver,
|
||||||
|
);
|
||||||
|
|
||||||
|
final behavior = CameraFocusingBehavior();
|
||||||
|
await game.ensureAdd(behavior);
|
||||||
|
|
||||||
|
behavior.onNewState(playing);
|
||||||
|
final previousPosition = game.camera.position.clone();
|
||||||
|
await game.ready();
|
||||||
|
|
||||||
|
final zoom = behavior.children.whereType<CameraZoom>().single;
|
||||||
|
game.update(zoom.controller.duration!);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
expect(zoom.controller.completed, isTrue);
|
||||||
|
expect(
|
||||||
|
game.camera.position,
|
||||||
|
isNot(equals(previousPosition)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -1,113 +0,0 @@
|
|||||||
// ignore_for_file: cascade_invocations
|
|
||||||
|
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flame_test/flame_test.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:pinball/game/components/camera_controller.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('CameraController', () {
|
|
||||||
late FlameGame game;
|
|
||||||
late CameraController controller;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
game = FlameGame()..onGameResize(Vector2(100, 200));
|
|
||||||
|
|
||||||
controller = CameraController(game);
|
|
||||||
await game.ensureAdd(controller);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loads correctly', () async {
|
|
||||||
expect(game.firstChild<CameraController>(), isNotNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('correctly calculates the zooms', () async {
|
|
||||||
expect(controller.gameFocus.zoom.toInt(), equals(12));
|
|
||||||
expect(controller.waitingBackboxFocus.zoom.toInt(), equals(11));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('correctly sets the initial zoom and position', () async {
|
|
||||||
expect(game.camera.zoom, equals(controller.waitingBackboxFocus.zoom));
|
|
||||||
expect(
|
|
||||||
game.camera.follow,
|
|
||||||
equals(controller.waitingBackboxFocus.position),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('focusOnGame', () {
|
|
||||||
test('changes the zoom', () async {
|
|
||||||
controller.focusOnGame();
|
|
||||||
|
|
||||||
await game.ready();
|
|
||||||
final zoom = game.firstChild<CameraZoom>();
|
|
||||||
expect(zoom, isNotNull);
|
|
||||||
expect(zoom?.value, equals(controller.gameFocus.zoom));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('moves the camera after the zoom is completed', () async {
|
|
||||||
controller.focusOnGame();
|
|
||||||
await game.ready();
|
|
||||||
final cameraZoom = game.firstChild<CameraZoom>()!;
|
|
||||||
final future = cameraZoom.completed;
|
|
||||||
|
|
||||||
game.update(10);
|
|
||||||
game.update(0); // Ensure that the component was removed
|
|
||||||
|
|
||||||
await future;
|
|
||||||
|
|
||||||
expect(game.camera.position, Vector2(-4, -120));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('focusOnWaitingBackbox', () {
|
|
||||||
test('changes the zoom', () async {
|
|
||||||
controller.focusOnWaitingBackbox();
|
|
||||||
|
|
||||||
await game.ready();
|
|
||||||
final zoom = game.firstChild<CameraZoom>();
|
|
||||||
expect(zoom, isNotNull);
|
|
||||||
expect(zoom?.value, equals(controller.waitingBackboxFocus.zoom));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('moves the camera after the zoom is completed', () async {
|
|
||||||
controller.focusOnWaitingBackbox();
|
|
||||||
await game.ready();
|
|
||||||
final cameraZoom = game.firstChild<CameraZoom>()!;
|
|
||||||
final future = cameraZoom.completed;
|
|
||||||
|
|
||||||
game.update(10);
|
|
||||||
game.update(0); // Ensure that the component was removed
|
|
||||||
|
|
||||||
await future;
|
|
||||||
|
|
||||||
expect(game.camera.position, Vector2(-4.5, -121));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('focusOnGameOverBackbox', () {
|
|
||||||
test('changes the zoom', () async {
|
|
||||||
controller.focusOnGameOverBackbox();
|
|
||||||
|
|
||||||
await game.ready();
|
|
||||||
final zoom = game.firstChild<CameraZoom>();
|
|
||||||
expect(zoom, isNotNull);
|
|
||||||
expect(zoom?.value, equals(controller.gameOverBackboxFocus.zoom));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('moves the camera after the zoom is completed', () async {
|
|
||||||
controller.focusOnGameOverBackbox();
|
|
||||||
await game.ready();
|
|
||||||
final cameraZoom = game.firstChild<CameraZoom>()!;
|
|
||||||
final future = cameraZoom.completed;
|
|
||||||
|
|
||||||
game.update(10);
|
|
||||||
game.update(0); // Ensure that the component was removed
|
|
||||||
|
|
||||||
await future;
|
|
||||||
|
|
||||||
expect(game.camera.position, Vector2(-2.5, -117));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in new issue