Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions doc/flame/inputs/keyboard_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,42 @@ add(
```


### KeyboardCallbacks

Add the `KeyboardCallbacks` mixin to any `Component`, including the `FlameGame` itself, to receive
keyboard events while the game has focus. It adds three callbacks, each receiving its own event
type:

- `onKeyPress(KeyPressEvent)` when a key is pressed down.
- `onKeyHold(KeyHoldEvent)` repeatedly while a key is held down, at the OS's repeat rate.
- `onKeyRelease(KeyReleaseEvent)` when a key is released.

All three extend `KeyboardEvent`, which carries the `logicalKey` and `physicalKey` of the key
stroke, the `keysPressed` at the time, and helpers such as `isShiftPressed`, `isControlPressed`,
`isAltPressed`, `isMetaPressed` and `isCapsLockOn`. Press and hold events also carry the
`character` the key stroke produced, if any. The original Flutter
[`KeyEvent`](https://api.flutter.dev/flutter/services/KeyEvent-class.html) is available as `raw`.

```dart
class Player extends PositionComponent with KeyboardCallbacks {
@override
void onKeyPress(KeyPressEvent event) {
if (event.logicalKey == LogicalKeyboardKey.space) {
event.isShiftPressed ? jumpHigher() : jump();
}
}
}
```

To handle every kind of key event in one place, override `onKeyboardEvent(KeyboardEvent)` instead.
Its default implementation is what calls the three callbacks above, so overriding it without
calling `super` means they are no longer called.

Unlike tap events, keyboard events have no position, so they are delivered to **every**
`KeyboardCallbacks` component (or game) by default. A component can opt to set
`event.continuePropagation = false` to stop the event from reaching the components after it.


### Controlling focus

On the widget level, it is possible to use the
Expand Down
14 changes: 4 additions & 10 deletions examples/games/trex/lib/trex_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/text.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart' hide Image;
import 'package:trex_game/background/horizon.dart';
import 'package:trex_game/game_over.dart';
import 'package:trex_game/player.dart';
Expand All @@ -21,7 +19,7 @@ enum GameState() {

class TRexGame()
extends FlameGame
with KeyboardEvents, TapCallbacks, HasCollisionDetection {
with KeyboardCallbacks, TapCallbacks, HasCollisionDetection {
static const String description = '''
A game similar to the game in chrome that you get to play while offline.
Press space or tap/click the screen to jump, the more obstacles you manage
Expand Down Expand Up @@ -94,15 +92,11 @@ class TRexGame()
bool get isIntro => state == GameState.intro;

@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (keysPressed.contains(LogicalKeyboardKey.enter) ||
keysPressed.contains(LogicalKeyboardKey.space)) {
void onKeyPress(KeyPressEvent event) {
if (event.logicalKey
case LogicalKeyboardKey.enter || LogicalKeyboardKey.space) {
onAction();
}
return KeyEventResult.handled;
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import 'dart:math';
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';

class CameraFollowAndWorldBoundsExample()
extends FlameGame
with HasKeyboardHandlerComponents {
class CameraFollowAndWorldBoundsExample() extends FlameGame {
static const description = '''
This example demonstrates camera following the player, but also obeying the
world bounds (which are set up to leave a small margin around the visible
Expand Down Expand Up @@ -64,7 +62,7 @@ class Ground() extends PositionComponent {
}
}

class Player() extends PositionComponent with KeyboardHandler {
class Player() extends PositionComponent with KeyboardCallbacks {
this
: body = Path()
..moveTo(10, 0)
Expand Down Expand Up @@ -141,8 +139,7 @@ class Player() extends PositionComponent with KeyboardHandler {
}

@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isKeyDown = event is KeyDownEvent;
void onKeyPress(KeyPressEvent event) {
final keyLeft =
(event.logicalKey == LogicalKeyboardKey.arrowLeft) ||
(event.logicalKey == LogicalKeyboardKey.keyA);
Expand All @@ -153,32 +150,39 @@ class Player() extends PositionComponent with KeyboardHandler {
(event.logicalKey == LogicalKeyboardKey.arrowUp) ||
(event.logicalKey == LogicalKeyboardKey.keyW);

if (isKeyDown) {
if (keyLeft) {
velocity.x = -runSpeed;
} else if (keyRight) {
velocity.x = runSpeed;
} else if (keyUp && nJumpsLeft > 0) {
velocity.y = -jumpSpeed;
nJumpsLeft -= 1;
}
if (keyLeft) {
velocity.x = -runSpeed;
} else if (keyRight) {
velocity.x = runSpeed;
} else if (keyUp && nJumpsLeft > 0) {
velocity.y = -jumpSpeed;
nJumpsLeft -= 1;
}
_updateFacing();
}

@override
void onKeyRelease(KeyReleaseEvent event) {
final keysPressed = event.keysPressed;
final hasLeft =
keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
keysPressed.contains(LogicalKeyboardKey.keyA);
final hasRight =
keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
keysPressed.contains(LogicalKeyboardKey.keyD);
if (hasLeft && hasRight) {
// Leave the current speed unchanged
} else if (hasLeft) {
velocity.x = -runSpeed;
} else if (hasRight) {
velocity.x = runSpeed;
} else {
final hasLeft =
keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
keysPressed.contains(LogicalKeyboardKey.keyA);
final hasRight =
keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
keysPressed.contains(LogicalKeyboardKey.keyD);
if (hasLeft && hasRight) {
// Leave the current speed unchanged
} else if (hasLeft) {
velocity.x = -runSpeed;
} else if (hasRight) {
velocity.x = runSpeed;
} else {
velocity.x = 0;
}
velocity.x = 0;
}
_updateFacing();
}

void _updateFacing() {
if ((velocity.x > 0) && !facingRight) {
facingRight = true;
flipHorizontally();
Expand All @@ -187,6 +191,5 @@ class Player() extends PositionComponent with KeyboardHandler {
facingRight = false;
flipHorizontally();
}
return super.onKeyEvent(event, keysPressed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import 'package:flame/events.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';

class FollowComponentExample({required final Vector2 viewportResolution})
extends FlameGame
with HasCollisionDetection, HasKeyboardHandlerComponents {
with HasCollisionDetection {
static const String description = '''
Move around with W, A, S, D and notice how the camera follows the ember
sprite.\n
Expand Down Expand Up @@ -48,7 +47,7 @@ class FollowComponentExample({required final Vector2 viewportResolution})

class MovableEmber()
extends Ember<FollowComponentExample>
with CollisionCallbacks, KeyboardHandler {
with CollisionCallbacks, KeyboardCallbacks {
static const double speed = 300;
static final TextPaint textRenderer = TextPaint(
style: const TextStyle(color: Colors.white70, fontSize: 12),
Expand Down Expand Up @@ -100,35 +99,28 @@ class MovableEmber()
}

@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
if (event is KeyRepeatEvent) {
return super.onKeyEvent(event, keysPressed);
}
final isKeyDown = event is KeyDownEvent;
void onKeyPress(KeyPressEvent event) {
_move(event.logicalKey, isKeyDown: true);
}

@override
void onKeyRelease(KeyReleaseEvent event) {
_move(event.logicalKey, isKeyDown: false);
}

final bool handled;
if (event.logicalKey == LogicalKeyboardKey.keyA) {
void _move(LogicalKeyboardKey key, {required bool isKeyDown}) {
if (key == LogicalKeyboardKey.keyA) {
velocity.x = isKeyDown ? -1 : 0;
handled = true;
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
} else if (key == LogicalKeyboardKey.keyD) {
velocity.x = isKeyDown ? 1 : 0;
handled = true;
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
} else if (key == LogicalKeyboardKey.keyW) {
velocity.y = isKeyDown ? -1 : 0;
handled = true;
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
} else if (key == LogicalKeyboardKey.keyS) {
velocity.y = isKeyDown ? 1 : 0;
handled = true;
} else {
handled = false;
}

if (handled) {
angle = -velocity.angleToSigned(Vector2(1, 0));
return false;
} else {
return super.onKeyEvent(event, keysPressed);
return;
}
angle = -velocity.angleToSigned(Vector2(1, 0));
}
}

Expand Down
41 changes: 18 additions & 23 deletions examples/lib/stories/input/keyboard_example.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import 'package:examples/commons/ember.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

class KeyboardExample() extends FlameGame with KeyboardEvents {
class KeyboardExample() extends FlameGame with KeyboardCallbacks {
static const String description = '''
Example showcasing how to act on keyboard events.
It also briefly showcases how to create a game without the FlameGame.
Expand Down Expand Up @@ -38,26 +37,22 @@ class KeyboardExample() extends FlameGame with KeyboardEvents {
}

@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final isKeyDown = event is KeyDownEvent;

// Avoiding repeat event as we are interested only in
// key up and key down event.
if (key is! KeyRepeatEvent) {
if (event.logicalKey == LogicalKeyboardKey.keyA) {
_direction.x += isKeyDown ? -1 : 1;
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
_direction.x += isKeyDown ? 1 : -1;
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
_direction.y += isKeyDown ? -1 : 1;
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
_direction.y += isKeyDown ? 1 : -1;
}
}
void onKeyPress(KeyPressEvent event) => _steer(event.logicalKey, 1);

return super.onKeyEvent(event, keysPressed);
@override
void onKeyRelease(KeyReleaseEvent event) => _steer(event.logicalKey, -1);

// A press adds the key's direction and its release takes it back off, so
// holding opposite keys cancels out
void _steer(LogicalKeyboardKey key, double sign) {
if (key == LogicalKeyboardKey.keyA) {
_direction.x -= sign;
} else if (key == LogicalKeyboardKey.keyD) {
_direction.x += sign;
} else if (key == LogicalKeyboardKey.keyW) {
_direction.y -= sign;
} else if (key == LogicalKeyboardKey.keyS) {
_direction.y += sign;
}
}
}
5 changes: 5 additions & 0 deletions packages/flame/lib/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'src/events/callbacks/double_tap_callbacks.dart' show DoubleTapCallbacks;
export 'src/events/callbacks/drag_callbacks.dart' show DragCallbacks;
export 'src/events/callbacks/hover_callbacks.dart' show HoverCallbacks;
export 'src/events/callbacks/input_callbacks.dart' show InputCallbacks;
export 'src/events/callbacks/keyboard_callbacks.dart' show KeyboardCallbacks;
export 'src/events/callbacks/long_press_callbacks.dart' show LongPressCallbacks;
export 'src/events/callbacks/mouse_move_callbacks.dart' show MouseMoveCallbacks;
export 'src/events/callbacks/pointer_input_callbacks.dart'
Expand All @@ -16,6 +17,8 @@ export 'src/events/callbacks/tertiary_tap_callbacks.dart'
export 'src/events/dispatchers/dispatcher.dart' show Dispatcher;
export 'src/events/dispatchers/double_tap_dispatcher.dart'
show DoubleTapDispatcher, DoubleTapDispatcherKey;
export 'src/events/dispatchers/keyboard_dispatcher.dart'
show KeyboardDispatcher, KeyboardDispatcherKey;
export 'src/events/dispatchers/long_press_dispatcher.dart'
show LongPressDispatcher, LongPressDispatcherKey;
export 'src/events/dispatchers/mouse_move_dispatcher.dart'
Expand All @@ -40,6 +43,8 @@ export 'src/events/messages/drag_end_event.dart' show DragEndEvent;
export 'src/events/messages/drag_start_event.dart' show DragStartEvent;
export 'src/events/messages/drag_update_event.dart' show DragUpdateEvent;
export 'src/events/messages/event.dart' show Event;
export 'src/events/messages/keyboard_event.dart'
show KeyHoldEvent, KeyPressEvent, KeyReleaseEvent, KeyboardEvent;
export 'src/events/messages/location_context_event.dart'
show LocationContextEvent;
export 'src/events/messages/long_press_cancel_event.dart'
Expand Down
48 changes: 48 additions & 0 deletions packages/flame/lib/src/events/callbacks/keyboard_callbacks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:meta/meta.dart';

/// This mixin can be added to a [Component] allowing it to receive keyboard
/// events while the game has focus.
///
/// Every [KeyboardCallbacks] component receives every key event, regardless of
/// its position. Set [Event.continuePropagation] to false inside a handler to
/// stop the event from reaching the components after this one.
///
/// Override [onKeyPress], [onKeyHold] and [onKeyRelease] to react to a single
/// kind of key event, or [onKeyboardEvent] to receive all of them in one place.
///
/// This callback uses [KeyboardDispatcher] to route events.
mixin KeyboardCallbacks on Component implements InputCallbacks {
/// Receives every keyboard event delivered to this component.
///
/// By default it forwards the event to [onKeyPress], [onKeyHold] or
/// [onKeyRelease]. Overriding it without calling `super` means those three
/// are no longer called.
void onKeyboardEvent(KeyboardEvent event) {
switch (event) {
case final KeyPressEvent event:
onKeyPress(event);
case final KeyHoldEvent event:
onKeyHold(event);
case final KeyReleaseEvent event:
onKeyRelease(event);
}
}

/// Called when a key is pressed down.
void onKeyPress(KeyPressEvent event) {}

/// Called repeatedly while a key is held down.
void onKeyHold(KeyHoldEvent event) {}

/// Called when a key is released.
void onKeyRelease(KeyReleaseEvent event) {}

@override
@mustCallSuper
void onMount() {
super.onMount();
KeyboardDispatcher.addDispatcher(this);
}
}
Loading
Loading