feat: adding panel and camera zoom effects

pull/125/head
Erick Zanardo 4 years ago
parent 84d7ab8564
commit b95d3c2b2b

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:pinball_components/pinball_components.dart';
/// A [Component] that controls its game camera focus
class CameraController extends Component with HasGameRef, KeyboardHandler {
/// The camera position for the board
static final zeroPosition = Vector2(0, -7.8);
/// The camera position for the pinball panel
static final panelPosition = Vector2(0, -100.8);
/// The zoom value for the game mode
late final double gameZoom;
/// The zoom value for the panel mode
late final double panelZoom;
bool _isFocusingOnBoard = false;
@override
Future<void> onLoad() async {
await super.onLoad();
gameZoom = gameRef.size.y / 16;
panelZoom = gameRef.size.y / 12;
// Game starts with the camera focused on the panel
gameRef.camera
..speed = 200
..followVector2(panelPosition)
..zoom = panelZoom;
}
/// Move the camera focus to the game board
Future<void> focusOnBoard() async {
final zoom = CameraZoom(value: gameZoom);
unawaited(gameRef.add(zoom));
await zoom.completed;
gameRef.camera.moveTo(zeroPosition);
}
// TODO(erickzanardo): Just for testing while
// we don't get the panel designs, which will be
// where this event will be generated from
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (!_isFocusingOnBoard) {
if (event is RawKeyUpEvent &&
event.logicalKey == LogicalKeyboardKey.enter) {
_isFocusingOnBoard = true;
focusOnBoard();
return true;
}
}
return super.onKeyEvent(event, keysPressed);
}
}

@ -2,6 +2,7 @@ export 'ball.dart';
export 'baseboard.dart'; export 'baseboard.dart';
export 'board.dart'; export 'board.dart';
export 'bonus_word.dart'; export 'bonus_word.dart';
export 'camera_controller.dart';
export 'chrome_dino.dart'; export 'chrome_dino.dart';
export 'flipper_controller.dart'; export 'flipper_controller.dart';
export 'flutter_forest.dart'; export 'flutter_forest.dart';

@ -7,6 +7,8 @@ import 'package:flame/extensions.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/pinball_components.dart' hide Assets;
@ -41,7 +43,9 @@ class PinballGame extends Forge2DGame
Future<void> onLoad() async { Future<void> onLoad() async {
_addContactCallbacks(); _addContactCallbacks();
unawaited(add(CameraController()));
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(add(Panel(position: Vector2(0, -88))));
unawaited(add(Board())); unawaited(add(Board()));
unawaited(_addPlunger()); unawaited(_addPlunger());
unawaited(_addBonusWord()); unawaited(_addBonusWord());
@ -60,11 +64,6 @@ class PinballGame extends Forge2DGame
), ),
), ),
); );
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
} }
void _addContactCallbacks() { void _addContactCallbacks() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

@ -3,27 +3,18 @@
/// FlutterGen /// FlutterGen
/// ***************************************************** /// *****************************************************
// ignore_for_file: directives_ordering,unnecessary_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
/// File path: assets/images/ball.png
AssetGenImage get ball => const AssetGenImage('assets/images/ball.png'); AssetGenImage get ball => const AssetGenImage('assets/images/ball.png');
$AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen(); $AssetsImagesFlipperGen get flipper => const $AssetsImagesFlipperGen();
/// File path: assets/images/flutter_sign_post.png
AssetGenImage get flutterSignPost => AssetGenImage get flutterSignPost =>
const AssetGenImage('assets/images/flutter_sign_post.png'); const AssetGenImage('assets/images/flutter_sign_post.png');
AssetGenImage get panel => const AssetGenImage('assets/images/panel.png');
/// File path: assets/images/spaceship_bridge.png
AssetGenImage get spaceshipBridge => AssetGenImage get spaceshipBridge =>
const AssetGenImage('assets/images/spaceship_bridge.png'); const AssetGenImage('assets/images/spaceship_bridge.png');
/// File path: assets/images/spaceship_saucer.png
AssetGenImage get spaceshipSaucer => AssetGenImage get spaceshipSaucer =>
const AssetGenImage('assets/images/spaceship_saucer.png'); const AssetGenImage('assets/images/spaceship_saucer.png');
} }
@ -31,11 +22,8 @@ class $AssetsImagesGen {
class $AssetsImagesFlipperGen { class $AssetsImagesFlipperGen {
const $AssetsImagesFlipperGen(); const $AssetsImagesFlipperGen();
/// File path: assets/images/flipper/left.png
AssetGenImage get left => AssetGenImage get left =>
const AssetGenImage('assets/images/flipper/left.png'); const AssetGenImage('assets/images/flipper/left.png');
/// File path: assets/images/flipper/right.png
AssetGenImage get right => AssetGenImage get right =>
const AssetGenImage('assets/images/flipper/right.png'); const AssetGenImage('assets/images/flipper/right.png');
} }

@ -0,0 +1,38 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template camera_zoom}
/// Applies zoom to the camera of the game where this is added to
/// {@endtemplate}
class CameraZoom extends CurveComponent with HasGameRef {
/// {@macro camera_zoom}
CameraZoom({
required this.value,
}) : super(
curve: Curves.easeOut,
duration: 0.4,
);
/// The total zoom value to be applied to the camera
final double value;
late Tween<double> _tween;
@override
Future<void> onLoad() async {
await super.onLoad();
_tween = Tween(
begin: gameRef.camera.zoom,
end: value,
);
}
@override
void apply(double progress) {
gameRef.camera.zoom = _tween.transform(progress);
}
}

@ -1,11 +1,14 @@
export 'ball.dart'; export 'ball.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'camera_zoom.dart';
export 'curve.dart';
export 'fire_effect.dart'; export 'fire_effect.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'flutter_sign_post.dart'; export 'flutter_sign_post.dart';
export 'initial_position.dart'; export 'initial_position.dart';
export 'joint_anchor.dart'; export 'joint_anchor.dart';
export 'layer.dart'; export 'layer.dart';
export 'panel.dart';
export 'ramp_opening.dart'; export 'ramp_opening.dart';
export 'shapes/shapes.dart'; export 'shapes/shapes.dart';
export 'spaceship.dart'; export 'spaceship.dart';

@ -0,0 +1,46 @@
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
/// {@template curve_component}
/// A simple component that runs for the given [duration]
/// following an animation curve.
/// {@endtemplate}
class CurveComponent extends Component {
/// {@macro curve_component}
CurveComponent({required this.curve, required this.duration});
/// Curve of this component
final Curve curve;
/// How many seconds this curve lasts
final double duration;
double _value = 0;
final _completer = Completer<void>();
@override
@mustCallSuper
void update(double dt) {
super.update(dt);
_value += dt;
final progress = curve.transform(min(_value, duration) / duration);
apply(progress);
if (progress == 1) {
removeFromParent();
_completer.complete();
}
}
/// Method called with the proggress (between 0 and 1) of the curve
/// Override this to apply side effects to the game/components
void apply(double progress) {}
/// A future that completes once the curve has completed.
Future<void> get completed => _completer.future;
}

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:pinball_components/gen/assets.gen.dart';
/// {@template panel}
/// The vertical panel of the pinball
/// {@endtemplate}
class Panel extends SpriteComponent with HasGameRef {
///{@macro panel}
Panel({
required Vector2 position,
}) : super(
// TODO(erickzanardo): https://github.com/flame-engine/flame/issues/1132
position: position
..clone().multiply(
Vector2(1, -1),
),
size: Vector2(80, 60),
anchor: Anchor.bottomCenter,
);
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await gameRef.loadSprite(Assets.images.panel.keyName);
}
}

@ -6,7 +6,9 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sandbox/stories/camera/camera.dart';
import 'package:sandbox/stories/effects/effects.dart'; import 'package:sandbox/stories/effects/effects.dart';
import 'package:sandbox/stories/panel/panel.dart';
import 'package:sandbox/stories/spaceship/spaceship.dart'; import 'package:sandbox/stories/spaceship/spaceship.dart';
import 'package:sandbox/stories/stories.dart'; import 'package:sandbox/stories/stories.dart';
@ -18,5 +20,7 @@ void main() {
addEffectsStories(dashbook); addEffectsStories(dashbook);
addFlipperStories(dashbook); addFlipperStories(dashbook);
addSpaceshipStories(dashbook); addSpaceshipStories(dashbook);
addPanelStories(dashbook);
addCameraStories(dashbook);
runApp(dashbook); runApp(dashbook);
} }

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/camera/zoom.dart';
void addCameraStories(Dashbook dashbook) {
dashbook.storiesOf('Camera').add(
'Zoom',
(context) => GameWidget(
game: ZoomCameraGame(),
),
codeLink: buildSourceLink('panel/zoom.dart'),
info: ZoomCameraGame.info,
);
}

@ -0,0 +1,27 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class ZoomCameraGame extends BasicGame with TapDetector {
static const info = 'Shows how the zoom works, tap to zoom in/out';
bool zoomed = false;
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
unawaited(add(Panel(position: Vector2(0, -5))));
}
@override
void onTap() {
if (firstChild<CameraZoom>() == null) {
unawaited(add(CameraZoom(value: zoomed ? 10 : 20)));
zoomed = !zoomed;
}
}
}

@ -0,0 +1,17 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicPanelGame extends BasicGame {
static const info = 'Simple example which renders the Panel';
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
unawaited(add(Panel(position: Vector2(0, -5))));
}
}

@ -0,0 +1,15 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/panel/basic.dart';
void addPanelStories(Dashbook dashbook) {
dashbook.storiesOf('Panel').add(
'Basic',
(context) => GameWidget(
game: BasicPanelGame(),
),
codeLink: buildSourceLink('panel/basic.dart'),
info: BasicPanelGame.info,
);
}

@ -0,0 +1,48 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('CameraZoom', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 1;
await game.ensureAdd(Panel(position: Vector2(0, 10)));
await game.ensureAdd(CameraZoom(value: 8));
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/no_zoom.png'),
);
game.update(0.2);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/in_between.png'),
);
game.update(0.4);
await tester.pump();
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/camera_zoom/finished.png'),
);
game.update(0.1);
await tester.pump();
expect(game.firstChild<CameraZoom>(), isNull);
},
);
});
}

@ -0,0 +1,31 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('CurveComponent', () {
testWithFlameGame('is removed once it finishes', (game) async {
final curve = CurveComponent(curve: Curves.linear, duration: 1);
await game.ensureAdd(curve);
expect(game.firstChild<CurveComponent>(), isNotNull);
game.update(2);
game.update(0);
expect(game.firstChild<CurveComponent>(), isNull);
});
testWithFlameGame('completed completes once it finishes', (game) async {
final curve = CurveComponent(curve: Curves.linear, duration: 1);
await game.ensureAdd(curve);
final completed = curve.completed;
game.update(2);
expect(completed, completes);
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

@ -0,0 +1,29 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('Panel', () {
final tester = FlameTester(TestGame.new);
tester.testGameWidget(
'renders correctly',
setUp: (game, tester) async {
game.camera.followVector2(Vector2.zero());
game.camera.zoom = 5;
await game.ensureAdd(Panel(position: Vector2(0, 10)));
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('golden/panel.png'),
);
},
);
});
}

@ -0,0 +1,83 @@
import 'dart:async';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/components/camera_controller.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
group('CameraController', () {
late FlameGame game;
late CameraController controller;
setUp(() async {
game = FlameGame()..onGameResize(Vector2(100, 200));
controller = CameraController();
await game.ensureAdd(controller);
});
test('loads correctly', () async {
expect(game.firstChild<CameraController>(), isNotNull);
});
test('correctly calculates the zooms', () async {
expect(controller.gameZoom.toInt(), equals(12));
expect(controller.panelZoom.toInt(), equals(16));
});
test('correctly sets the initial zoom and position', () async {
expect(game.camera.zoom, equals(controller.panelZoom));
expect(game.camera.follow, equals(CameraController.panelPosition));
});
group('focusOnBoard', () {
test('changes the zoom', () async {
unawaited(controller.focusOnBoard());
await game.ready();
final zoom = game.firstChild<CameraZoom>();
expect(zoom, isNotNull);
expect(zoom?.value, equals(controller.gameZoom));
});
test('moves the camera after the zoom is completed', () async {
final future = controller.focusOnBoard();
await game.ready();
game.update(10);
await future;
expect(game.camera.position, Vector2(-3, -106.8));
});
test('moves the camera when enter is pressed', () async {
testRawKeyUpEvents([LogicalKeyboardKey.enter], (key) async {
controller.onKeyEvent(key, {});
await game.ready();
game.update(10);
expect(game.camera.position, Vector2(-3, -106.8));
});
});
test('does nothing when another key is pressed', () async {
testRawKeyUpEvents([LogicalKeyboardKey.keyA], (key) async {
final originalPosition = game.camera.position;
controller.onKeyEvent(key, {});
await game.ready();
game.update(10);
expect(game.camera.position, originalPosition);
});
});
});
});
}
Loading…
Cancel
Save