refactor: implemented `CameraFocusingBehavior` (#346)

pull/350/head
Alejandro Santiago 3 years ago committed by GitHub
parent 79da2e9234
commit 3465c9f1e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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));
}
}

@ -1,7 +1,6 @@
export 'android_acres/android_acres.dart'; export 'android_acres/android_acres.dart';
export 'backbox/backbox.dart'; export 'backbox/backbox.dart';
export 'bottom_group.dart'; export 'bottom_group.dart';
export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';
export 'controlled_flipper.dart'; export 'controlled_flipper.dart';
export 'controlled_plunger.dart'; export 'controlled_plunger.dart';

@ -18,7 +18,6 @@ class GameBlocStatusListener extends Component
break; break;
case GameStatus.playing: case GameStatus.playing:
gameRef.player.play(PinballAudio.backgroundMusic); gameRef.player.play(PinballAudio.backgroundMusic);
gameRef.firstChild<CameraController>()?.focusOnGame();
gameRef.overlays.remove(PinballGame.playButtonOverlay); gameRef.overlays.remove(PinballGame.playButtonOverlay);
break; break;
case GameStatus.gameOver: case GameStatus.gameOver:
@ -27,7 +26,6 @@ class GameBlocStatusListener extends Component
score: state.displayScore, score: state.displayScore,
character: gameRef.characterTheme, character: gameRef.characterTheme,
); );
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
break; break;
} }
} }

@ -48,8 +48,6 @@ class PinballGame extends PinballForge2DGame
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await add(CameraController(this));
final machine = [ final machine = [
BoardBackgroundSpriteComponent(), BoardBackgroundSpriteComponent(),
Boundaries(), Boundaries(),
@ -75,6 +73,7 @@ class PinballGame extends PinballForge2DGame
await addAll( await addAll(
[ [
GameBlocStatusListener(), GameBlocStatusListener(),
CameraFocusingBehavior(),
CanvasComponent( CanvasComponent(
onSpritePainted: (paint) { onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) { if (paint.filterQuality != FilterQuality.medium) {

@ -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));
});
});
});
}

@ -11,8 +11,6 @@ class _MockPinballGame extends Mock implements PinballGame {}
class _MockBackbox extends Mock implements Backbox {} class _MockBackbox extends Mock implements Backbox {}
class _MockCameraController extends Mock implements CameraController {}
class _MockActiveOverlaysNotifier extends Mock class _MockActiveOverlaysNotifier extends Mock
implements ActiveOverlaysNotifier {} implements ActiveOverlaysNotifier {}
@ -46,20 +44,18 @@ void main() {
group('onNewState', () { group('onNewState', () {
late PinballGame game; late PinballGame game;
late Backbox backbox; late Backbox backbox;
late CameraController cameraController; late GameBlocStatusListener gameBlocStatusListener;
late GameBlocStatusListener gameFlowController;
late PinballPlayer pinballPlayer; late PinballPlayer pinballPlayer;
late ActiveOverlaysNotifier overlays; late ActiveOverlaysNotifier overlays;
setUp(() { setUp(() {
game = _MockPinballGame(); game = _MockPinballGame();
backbox = _MockBackbox(); backbox = _MockBackbox();
cameraController = _MockCameraController(); gameBlocStatusListener = GameBlocStatusListener();
gameFlowController = GameBlocStatusListener();
overlays = _MockActiveOverlaysNotifier(); overlays = _MockActiveOverlaysNotifier();
pinballPlayer = _MockPinballPlayer(); pinballPlayer = _MockPinballPlayer();
gameFlowController.mockGameRef(game); gameBlocStatusListener.mockGameRef(game);
when( when(
() => backbox.requestInitials( () => backbox.requestInitials(
@ -67,22 +63,18 @@ void main() {
character: any(named: 'character'), character: any(named: 'character'),
), ),
).thenAnswer((_) async {}); ).thenAnswer((_) async {});
when(cameraController.focusOnWaitingBackbox).thenAnswer((_) async {});
when(cameraController.focusOnGame).thenAnswer((_) async {});
when(() => overlays.remove(any())).thenAnswer((_) => true); when(() => overlays.remove(any())).thenAnswer((_) => true);
when(() => game.descendants().whereType<Backbox>()) when(() => game.descendants().whereType<Backbox>())
.thenReturn([backbox]); .thenReturn([backbox]);
when(game.firstChild<CameraController>).thenReturn(cameraController);
when(() => game.overlays).thenReturn(overlays); when(() => game.overlays).thenReturn(overlays);
when(() => game.characterTheme).thenReturn(DashTheme()); when(() => game.characterTheme).thenReturn(DashTheme());
when(() => game.player).thenReturn(pinballPlayer); when(() => game.player).thenReturn(pinballPlayer);
}); });
test( test(
'changes the backbox display and camera correctly ' 'changes the backbox display when the game is over',
'when the game is over',
() { () {
final state = GameState( final state = GameState(
totalScore: 0, totalScore: 0,
@ -92,7 +84,7 @@ void main() {
bonusHistory: const [], bonusHistory: const [],
status: GameStatus.gameOver, status: GameStatus.gameOver,
); );
gameFlowController.onNewState(state); gameBlocStatusListener.onNewState(state);
verify( verify(
() => backbox.requestInitials( () => backbox.requestInitials(
@ -100,18 +92,16 @@ void main() {
character: any(named: 'character'), character: any(named: 'character'),
), ),
).called(1); ).called(1);
verify(cameraController.focusOnGameOverBackbox).called(1);
}, },
); );
test( test(
'changes the backbox and camera correctly when it is not a game over', 'changes the backbox when it is not a game over',
() { () {
gameFlowController.onNewState( gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.playing), GameState.initial().copyWith(status: GameStatus.playing),
); );
verify(cameraController.focusOnGame).called(1);
verify(() => overlays.remove(PinballGame.playButtonOverlay)) verify(() => overlays.remove(PinballGame.playButtonOverlay))
.called(1); .called(1);
}, },
@ -120,7 +110,7 @@ void main() {
test( test(
'plays the background music on start', 'plays the background music on start',
() { () {
gameFlowController.onNewState( gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.playing), GameState.initial().copyWith(status: GameStatus.playing),
); );
@ -132,7 +122,7 @@ void main() {
test( test(
'plays the game over voice over when it is game over', 'plays the game over voice over when it is game over',
() { () {
gameFlowController.onNewState( gameBlocStatusListener.onNewState(
GameState.initial().copyWith(status: GameStatus.gameOver), GameState.initial().copyWith(status: GameStatus.gameOver),
); );

Loading…
Cancel
Save