Skip to content

Commit

Permalink
feat: adding KeyboardListenerComponent (#1594)
Browse files Browse the repository at this point in the history
  • Loading branch information
erickzanardo authored May 23, 2022
1 parent 2e82dc9 commit c887c36
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 2 deletions.
30 changes: 28 additions & 2 deletions doc/flame/inputs/keyboard-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand All @@ -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).
7 changes: 7 additions & 0 deletions examples/lib/stories/input/input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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()),
Expand Down
132 changes: 132 additions & 0 deletions examples/lib/stories/input/keyboard_listener_component_example.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<LogicalKeyboardKey> 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);
}
}
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LogicalKeyboardKey>);

/// {@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<LogicalKeyboardKey, KeyHandlerCallback> keyUp = const {},
Map<LogicalKeyboardKey, KeyHandlerCallback> keyDown = const {},
}) : _keyUp = keyUp,
_keyDown = keyDown;

final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyUp;
final Map<LogicalKeyboardKey, KeyHandlerCallback> _keyDown;

@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isUp = event is RawKeyUpEvent;

final handlers = isUp ? _keyUp : _keyDown;
final handler = handlers[event.logicalKey];

if (handler != null) {
return handler(keysPressed);
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -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<LogicalKeyboardKey> 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,
);
},
);
});
}

0 comments on commit c887c36

Please sign in to comment.