From 80c460252f99420c50b133c07cffc37e515f8101 Mon Sep 17 00:00:00 2001 From: Jochum van der Ploeg Date: Tue, 26 Apr 2022 15:22:27 +0200 Subject: [PATCH] feat: add mobile touch controls --- lib/game/pinball_game.dart | 73 +++++++++++- test/game/pinball_game_test.dart | 192 +++++++++++++++++++++++++++++++ test/helpers/mocks.dart | 8 +- 3 files changed, 267 insertions(+), 6 deletions(-) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 55a6b23a..bb143f13 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -6,6 +6,7 @@ import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; @@ -18,7 +19,8 @@ class PinballGame extends Forge2DGame with FlameBloc, HasKeyboardHandlerComponents, - Controls<_GameBallsController> { + Controls<_GameBallsController>, + TapDetector { PinballGame({ required this.theme, required this.audio, @@ -74,6 +76,65 @@ class PinballGame extends Forge2DGame await super.onLoad(); } + BoardSide? boardSideActive; + + @override + void onTapDown(TapDownInfo info) { + if (info.raw.kind == PointerDeviceKind.touch) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + descendants().whereType().first.pull(); + } else { + final leftSide = info.eventPosition.widget.x < canvasSize.x / 2; + boardSideActive = leftSide ? BoardSide.left : BoardSide.right; + final flippers = descendants().whereType().where((flipper) { + return flipper.side == boardSideActive; + }); + + for (final flipper in flippers) { + flipper.moveUp(); + } + } + } + + super.onTapDown(info); + } + + @override + void onTapUp(TapUpInfo info) { + final rocket = descendants().whereType().first; + final bounds = rocket.topLeftPosition & rocket.size; + + if (bounds.contains(info.eventPosition.game.toOffset())) { + descendants().whereType().first.release(); + } else { + _moveFlippersDown(); + } + super.onTapUp(info); + } + + @override + void onTapCancel() { + descendants().whereType().first.release(); + + _moveFlippersDown(); + super.onTapCancel(); + } + + void _moveFlippersDown() { + if (boardSideActive != null) { + final flippers = descendants().whereType().where((flipper) { + return flipper.side == boardSideActive; + }); + for (final flipper in flippers) { + flipper.moveDown(); + } + boardSideActive = null; + } + } + void _addContactCallbacks() { addContactCallback(BallScorePointsCallback(this)); addContactCallback(BottomWallBallContactCallback()); @@ -135,7 +196,7 @@ class _GameBallsController extends ComponentController } } -class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { +class DebugPinballGame extends PinballGame with FPSCounter { DebugPinballGame({ required PinballTheme theme, required PinballAudio audio, @@ -172,9 +233,11 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { @override void onTapUp(TapUpInfo info) { - add( - ControlledBall.debug()..initialPosition = info.eventPosition.game, - ); + super.onTapUp(info); + + if (info.raw.kind == PointerDeviceKind.mouse) { + add(ControlledBall.debug()..initialPosition = info.eventPosition.game); + } } } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index aebb31f9..a24d1909 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -199,6 +200,193 @@ void main() { ); }); }); + + group('flipper control', () { + flameTester.test('move left flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('move right flipper up', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(game.canvasSize); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.right, + ); + + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isNegative); + }); + + flameTester.test('tap up moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + + flameTester.test('tap cancel moves flipper down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.zero()); + when(() => eventPosition.widget).thenReturn(Vector2.zero()); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final flippers = game.descendants().whereType().where( + (flipper) => flipper.side == BoardSide.left, + ); + + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isNegative); + + final tapUpEvent = MockTapUpInfo(); + + game.onTapCancel(); + await game.ready(); + + expect(flippers.first.body.linearVelocity.y, isPositive); + }); + }); + + group('plunger control', () { + flameTester.test('tap down moves plunger down', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(plunger.body.linearVelocity.y, equals(7)); + }); + + flameTester.test('tap up releases plunger', (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(plunger.body.linearVelocity.y, equals(7)); + + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + + flameTester.test('tap cancel releases plunger', (game) async { + await game.ready(); + + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2(40, 60)); + + final raw = MockTapDownDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.touch); + + final tapDownEvent = MockTapDownInfo(); + when(() => tapDownEvent.eventPosition).thenReturn(eventPosition); + when(() => tapDownEvent.raw).thenReturn(raw); + + final plunger = game.descendants().whereType().first; + game.onTapDown(tapDownEvent); + await game.ready(); + + expect(plunger.body.linearVelocity.y, equals(7)); + + game.onTapCancel(); + await game.ready(); + + expect(plunger.body.linearVelocity.y, equals(0)); + }); + }); }); group('DebugPinballGame', () { @@ -208,8 +396,12 @@ void main() { final eventPosition = MockEventPosition(); when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final raw = MockTapUpDetails(); + when(() => raw.kind).thenReturn(PointerDeviceKind.mouse); + final tapUpEvent = MockTapUpInfo(); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + when(() => tapUpEvent.raw).thenReturn(raw); final previousBalls = game.descendants().whereType().toList(); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 9b0f67c9..4e4dffd7 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,7 +2,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -55,8 +55,14 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { } } +class MockTapDownInfo extends Mock implements TapDownInfo {} + +class MockTapDownDetails extends Mock implements TapDownDetails {} + class MockTapUpInfo extends Mock implements TapUpInfo {} +class MockTapUpDetails extends Mock implements TapUpDetails {} + class MockEventPosition extends Mock implements EventPosition {} class MockFilter extends Mock implements Filter {}