Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added HardwareKeyboardDetector #2257

Merged
merged 27 commits into from
Jan 27, 2023
Merged
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0b401cc
RawKeyboardDetector
st-pasha Jan 7, 2023
6fb5fc1
remove an argument
st-pasha Jan 7, 2023
f6351dd
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 11, 2023
ff19335
rename onKeyEvent->onRawKeyEvent
st-pasha Jan 12, 2023
4d0cd11
start working on example
st-pasha Jan 12, 2023
fb88e15
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 12, 2023
02b33d0
Added example
st-pasha Jan 14, 2023
e3016e6
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 14, 2023
8ba5028
add a dictionary word
st-pasha Jan 14, 2023
38dad23
rename RawKeyboardDetector->HardwareKeyboardDetector, and added tests
st-pasha Jan 15, 2023
3065ac8
format
st-pasha Jan 15, 2023
c51f5d3
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 15, 2023
a886391
added pauseKeyEvents property
st-pasha Jan 15, 2023
eb844c2
update doccomments
st-pasha Jan 15, 2023
aaec880
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 26, 2023
e156502
analyze warnings
st-pasha Jan 26, 2023
e59cf4e
more robust pauseKeyEvents
st-pasha Jan 26, 2023
b7c8d27
add external links
st-pasha Jan 26, 2023
767fab5
convert HardwareKeyboardDetector into a component
st-pasha Jan 26, 2023
87a096e
analysis
st-pasha Jan 26, 2023
44bc77b
normalize events upon mounting/dismounting
st-pasha Jan 26, 2023
0ae6828
comment
st-pasha Jan 26, 2023
a64075d
minor
st-pasha Jan 26, 2023
81bcf7b
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 26, 2023
a3d485d
fix tests
st-pasha Jan 26, 2023
6628295
Merge branch 'main' into ps.raw-keyboard-events
st-pasha Jan 27, 2023
a87a013
Merge branch 'main' into ps.raw-keyboard-events
spydon Jan 27, 2023
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
102 changes: 102 additions & 0 deletions packages/flame/lib/src/events/game_mixins/raw_keyboard_detector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:flame/src/game/game.dart';
import 'package:flutter/services.dart';

/// This mixin allows listening to raw keyboard events, bypassing the `Focus`
/// widget in Flutter.
///
/// The mixin provides two event handlers that can be overridden in your game:
/// - [onKeyEvent] fires whenever the user presses or releases any key on a
/// keyboard. Use this to handle "one-off" key events, such as the user
/// pressing <Space> to jump, or <Esc> to open a menu.
/// - [onKeysPressed] fires on every game tick as long as the user presses at
/// least some keys on the keyboard. Use this to reliably handle repeating
/// key events, such as user pressing arrow keys to move their character, or
/// holding down <Ctrl> to shoot continuously.
mixin RawKeyboardDetector on Game {
/// The list of keys that are currently being pressed on the keyboard (or a
/// keyboard-like device). The keys are listed in the order in which they
/// were pressed, except for the modifier keys which may be listed
/// out-of-order on some systems.
List<PhysicalKeyboardKey> physicalKeysPressed = [];

/// The set of logical keys that are currently being pressed on the keyboard.
/// This set corresponds to the [physicalKeysPressed] list, and can be used
/// to search for keys in a keyboard-layout-independent way.
Set<LogicalKeyboardKey> logicalKeysPressed = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this issue flutter/flutter#99330 I would not duplicate this info that is already present and accessible on RawKeyboard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is copied from the RawKeyboard on every key event:

logicalKeysPressed = RawKeyboard.instance.keysPressed;

Thus, it is always kept in sync with the information available in RawKeyboard. The reason I decided to have this copy is because in RawKeyboard this field is defined as follows:

Set<LogicalKeyboardKey> get keysPressed => _keysPressed.values.toSet();

That is, they return a copy of the internal state on every access. This is good for safety, but not so much for efficiency: if your code needs to check whether a number of certain keys are pressed, there will be a new set created on every check. Even such simple methods as isShiftPressed/isControlPressed/isAltPressed each create two new copies of this set.

So I thought that a good solution to this is to simply keep a local copy of the set of logical keys, and make sure it is always kept up-to-date with RawKeyboard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for physicalKeysPressed -- I believe it's important to know not only which keys are currently being pressed, but also in what order they were pressed. This information is currently missing in RawKeyboard, they return physical keys as an orderless set, and keep it internally as a dictionary.

The reason why the order may be important is a scenario like this: suppose the user presses [Left Arrow], holds it, and then presses [Right Arrow]. Now two arrows are pressed simultaneously, and the game needs to decide how to respond to this. One possible choice is to make the player character stand still; another choice is to make the player character go right, since the right arrow was pressed last. By keeping the physical keys in a list instead of a set, we give the game an opportunity to choose which behavior to implement (my personal preference is for the latter).


/// Is the <Ctrl> key currently being pressed (either left or right)?
bool get isControlPressed =>
logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) ||
logicalKeysPressed.contains(LogicalKeyboardKey.controlRight);

/// Is the <Shift> key currently being pressed (either left or right)?
bool get isShiftPressed =>
logicalKeysPressed.contains(LogicalKeyboardKey.shiftLeft) ||
logicalKeysPressed.contains(LogicalKeyboardKey.shiftRight);

/// Is the <Alt> key currently being pressed (either left or right)?
bool get isAltPressed =>
logicalKeysPressed.contains(LogicalKeyboardKey.altLeft) ||
logicalKeysPressed.contains(LogicalKeyboardKey.altRight);

/// Override this event handler if you want to get notified whenever any key
/// on a keyboard is pressed or released. The [event] will be either a
/// [RawKeyDownEvent] or a [RawKeyUpEvent], respectively.
///
/// This event may also fire when the user presses a key and then holds it
/// down. In such a case `event.repeat` property will be set to `true`.
/// However, this should not be used for character navigation, since this
/// behavior may not be reliable, and the frequency of such events is system-
/// dependent. Use [onKeysPressed] event handler instead.
void onKeyEvent(RawKeyEvent event) {}

/// Override this event handler if you want to get notified whenever any keys
/// are being pressed. This event handler is fired at the start of every game
/// tick.
///
/// The list of keys currently being pressed can be accessed via the
/// [physicalKeysPressed] or [logicalKeysPressed] properties.
void onKeysPressed() {}

/// Internal handler of raw key events.
void _onRawKeyEvent(RawKeyEvent event) {
logicalKeysPressed = RawKeyboard.instance.keysPressed;
if (event is RawKeyDownEvent) {
physicalKeysPressed.add(event.physicalKey);
} else if (event is RawKeyUpEvent) {
physicalKeysPressed.remove(event.physicalKey);
}
// The list of physical keys may need to be reconciled with the RawKeyboard
if (physicalKeysPressed.length != logicalKeysPressed.length) {
final rawPhysicalKeysPressed = RawKeyboard.instance.physicalKeysPressed;
physicalKeysPressed
..removeWhere((key) => !rawPhysicalKeysPressed.contains(key))
..addAll(
rawPhysicalKeysPressed
.where((key) => !physicalKeysPressed.contains(key))
.toList(),
);
}
onKeyEvent(event);
}

@override
void onMount() {
super.onMount();
RawKeyboard.instance.addListener(_onRawKeyEvent);
}

@override
void onRemove() {
super.onRemove();
RawKeyboard.instance.removeListener(_onRawKeyEvent);
}

@override
void update(double dt) {
if (physicalKeysPressed.isNotEmpty) {
onKeysPressed();
}
super.update(dt);
}
}