From c887c3616e9f65209b8e29cb8575a0052db3e2bb Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 23 May 2022 03:32:10 -0300 Subject: [PATCH] feat: adding KeyboardListenerComponent (#1594) --- doc/flame/inputs/keyboard-input.md | 30 +++- examples/lib/stories/input/input.dart | 7 + .../keyboard_listener_component_example.dart | 132 ++++++++++++++++++ packages/flame/lib/components.dart | 1 + .../input/keyboard_listener_component.dart | 38 +++++ .../keyboard_listener_component_test.dart | 76 ++++++++++ 6 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 examples/lib/stories/input/keyboard_listener_component_example.dart create mode 100644 packages/flame/lib/src/components/input/keyboard_listener_component.dart create mode 100644 packages/flame/test/components/keyboard_listener_component_test.dart diff --git a/doc/flame/inputs/keyboard-input.md b/doc/flame/inputs/keyboard-input.md index 97349ca2d..3f413c05a 100644 --- a/doc/flame/inputs/keyboard-input.md +++ b/doc/flame/inputs/keyboard-input.md @@ -9,7 +9,7 @@ For other input documents, see also: ## Intro -The keyboard API on flame relies on the +The keyboard API on flame relies on the [Flutter's Focus widget](https://api.flutter.dev/flutter/widgets/Focus-class.html). To customize focus behavior, see [Controlling focus](#controlling-focus). @@ -91,6 +91,32 @@ that triggered the callback in the first place. The second is a set of the curre The returned value should be `true` to allow the continuous propagation of the key event among other components. To not allow any other component to receive the event, return `false`. +Flame also provides a default implementation called `KeyboardListenerComponent` which can be used +to handle keyboard events. Like any other component, it can be added as a child to a `FlameGame` +or another `Component`: + +For example, imagine a `PositionComponent` which has methods to move on the X and Y axis, +then the following code could be used to bind those methods to key events: + +```dart +add( + KeyboardListenerComponent( + keyUp: { + LogicalKeyboardKey.keyA: (keysPressed) { ... }, + LogicalKeyboardKey.keyD: (keysPressed) { ... }, + LogicalKeyboardKey.keyW: (keysPressed) { ... }, + LogicalKeyboardKey.keyS: (keysPressed) { ... }, + }, + keyDown: { + LogicalKeyboardKey.keyA: (keysPressed) { ... }, + LogicalKeyboardKey.keyD: (keysPressed) { ... }, + LogicalKeyboardKey.keyW: (keysPressed) { ... }, + LogicalKeyboardKey.keyS: (keysPressed) { ... }, + }, + ), +); +``` + ### Controlling focus On the widget level, it is possible to use the @@ -102,5 +128,5 @@ the game is focused or not. By default `GameWidget` has its `autofocus` set to true, which means it will get focused once it is mounted. To override that behavior, set `autofocus` to false. -For a more complete example see +For a more complete example see [here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/keyboard.dart). diff --git a/examples/lib/stories/input/input.dart b/examples/lib/stories/input/input.dart index 8cadfc064..793f4fb73 100644 --- a/examples/lib/stories/input/input.dart +++ b/examples/lib/stories/input/input.dart @@ -6,6 +6,7 @@ import 'package:examples/stories/input/hoverables_example.dart'; import 'package:examples/stories/input/joystick_advanced_example.dart'; import 'package:examples/stories/input/joystick_example.dart'; import 'package:examples/stories/input/keyboard_example.dart'; +import 'package:examples/stories/input/keyboard_listener_component_example.dart'; import 'package:examples/stories/input/mouse_cursor_example.dart'; import 'package:examples/stories/input/mouse_movement_example.dart'; import 'package:examples/stories/input/multitap_advanced_example.dart'; @@ -48,6 +49,12 @@ void addInputStories(Dashbook dashbook) { codeLink: baseLink('input/keyboard_example.dart'), info: KeyboardExample.description, ) + ..add( + 'Keyboard (Component)', + (_) => GameWidget(game: KeyboardListenerComponentExample()), + codeLink: baseLink('input/keyboard_component_example.dart'), + info: KeyboardListenerComponentExample.description, + ) ..add( 'Mouse Movement', (_) => GameWidget(game: MouseMovementExample()), diff --git a/examples/lib/stories/input/keyboard_listener_component_example.dart b/examples/lib/stories/input/keyboard_listener_component_example.dart new file mode 100644 index 000000000..ead626224 --- /dev/null +++ b/examples/lib/stories/input/keyboard_listener_component_example.dart @@ -0,0 +1,132 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class KeyboardListenerComponentExample extends FlameGame + with HasKeyboardHandlerComponents { + static const String description = ''' + Similar to the default Keyboard example, but shows a different + implementation approach, which uses Flame's + KeyboardListenerComponent to handle input. + Usage: Use A S D W to steer Ember. + '''; + + static final Paint white = BasicPalette.white.paint(); + static const int speed = 200; + + late final Ember ember; + final Vector2 velocity = Vector2(0, 0); + + @override + Future onLoad() async { + ember = Ember(position: size / 2, size: Vector2.all(100)); + add(ember); + + add( + KeyboardListenerComponent( + keyUp: { + LogicalKeyboardKey.keyA: (keys) => _handleKey( + false, + LogicalKeyboardKey.keyA, + keys, + ), + LogicalKeyboardKey.keyD: (keys) => _handleKey( + false, + LogicalKeyboardKey.keyD, + keys, + ), + LogicalKeyboardKey.keyW: (keys) => _handleKey( + false, + LogicalKeyboardKey.keyW, + keys, + ), + LogicalKeyboardKey.keyS: (keys) => _handleKey( + false, + LogicalKeyboardKey.keyS, + keys, + ), + }, + keyDown: { + LogicalKeyboardKey.keyA: (keys) => _handleKey( + true, + LogicalKeyboardKey.keyA, + keys, + ), + LogicalKeyboardKey.keyD: (keys) => _handleKey( + true, + LogicalKeyboardKey.keyD, + keys, + ), + LogicalKeyboardKey.keyW: (keys) => _handleKey( + true, + LogicalKeyboardKey.keyW, + keys, + ), + LogicalKeyboardKey.keyS: (keys) => _handleKey( + true, + LogicalKeyboardKey.keyS, + keys, + ), + }, + ), + ); + } + + bool _handleKey( + bool isDown, + LogicalKeyboardKey key, + Set keysPressed, + ) { + const w = LogicalKeyboardKey.keyW; + const a = LogicalKeyboardKey.keyA; + const s = LogicalKeyboardKey.keyS; + const d = LogicalKeyboardKey.keyD; + + if (key == w) { + if (isDown) { + velocity.y = -1; + } else if (keysPressed.contains(s)) { + velocity.y = 1; + } else { + velocity.y = 0; + } + } else if (key == s) { + if (isDown) { + velocity.y = 1; + } else if (keysPressed.contains(w)) { + velocity.y = -1; + } else { + velocity.y = 0; + } + } else if (key == a) { + if (isDown) { + velocity.x = -1; + } else if (keysPressed.contains(d)) { + velocity.x = 1; + } else { + velocity.x = 0; + } + } else if (key == d) { + if (isDown) { + velocity.x = 1; + } else if (keysPressed.contains(a)) { + velocity.x = -1; + } else { + velocity.x = 0; + } + } + + return true; + } + + @override + void update(double dt) { + super.update(dt); + final displacement = velocity * (speed * dt); + ember.position.add(displacement); + } +} diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index ca368115d..ce4870c76 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -8,6 +8,7 @@ export 'src/components/custom_painter_component.dart'; export 'src/components/fps_component.dart'; export 'src/components/fps_text_component.dart'; export 'src/components/input/joystick_component.dart'; +export 'src/components/input/keyboard_listener_component.dart'; export 'src/components/isometric_tile_map_component.dart'; export 'src/components/mixins/draggable.dart'; export 'src/components/mixins/gesture_hitboxes.dart'; diff --git a/packages/flame/lib/src/components/input/keyboard_listener_component.dart b/packages/flame/lib/src/components/input/keyboard_listener_component.dart new file mode 100644 index 000000000..5fd65de4f --- /dev/null +++ b/packages/flame/lib/src/components/input/keyboard_listener_component.dart @@ -0,0 +1,38 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/src/game/mixins/keyboard.dart'; +import 'package:flutter/services.dart'; + +/// The signature for a key handle function +typedef KeyHandlerCallback = bool Function(Set); + +/// {@template keyboard_listener_component} +/// A [Component] that receives keyboard input and executes registered methods. +/// This component is based on [KeyboardHandler], which requires the [FlameGame] +/// which is used to be mixed with [HasKeyboardHandlerComponents]. +/// {@endtemplate} +class KeyboardListenerComponent extends Component with KeyboardHandler { + /// {@macro keyboard_listener_component} + KeyboardListenerComponent({ + Map keyUp = const {}, + Map keyDown = const {}, + }) : _keyUp = keyUp, + _keyDown = keyDown; + + final Map _keyUp; + final Map _keyDown; + + @override + bool onKeyEvent(RawKeyEvent event, Set keysPressed) { + final isUp = event is RawKeyUpEvent; + + final handlers = isUp ? _keyUp : _keyDown; + final handler = handlers[event.logicalKey]; + + if (handler != null) { + return handler(keysPressed); + } + + return true; + } +} diff --git a/packages/flame/test/components/keyboard_listener_component_test.dart b/packages/flame/test/components/keyboard_listener_component_test.dart new file mode 100644 index 000000000..5ee72b872 --- /dev/null +++ b/packages/flame/test/components/keyboard_listener_component_test.dart @@ -0,0 +1,76 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +abstract class _KeyCallStub { + bool onCall(Set keysPressed); +} + +class KeyCallStub extends Mock implements _KeyCallStub {} + +class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +RawKeyUpEvent _mockKeyUp(LogicalKeyboardKey key) { + final event = MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} + +void main() { + group('KeyboardListenerComponent', () { + test('calls registered handlers', () { + final stub = KeyCallStub(); + when(() => stub.onCall(any())).thenReturn(true); + + final input = KeyboardListenerComponent( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}); + verify(() => stub.onCall({})).called(1); + }); + + test( + 'returns false the handler return value', + () { + final stub = KeyCallStub(); + when(() => stub.onCall(any())).thenReturn(false); + + final input = KeyboardListenerComponent( + keyUp: { + LogicalKeyboardKey.arrowUp: stub.onCall, + }, + ); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isFalse, + ); + }, + ); + + test( + 'returns true (allowing event to bubble) when no handler is registered', + () { + final stub = KeyCallStub(); + when(() => stub.onCall(any())).thenReturn(true); + + final input = KeyboardListenerComponent(); + + expect( + input.onKeyEvent(_mockKeyUp(LogicalKeyboardKey.arrowUp), {}), + isTrue, + ); + }, + ); + }); +}