Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added HardwareKeyboardDetector (#2257)
This creates a new component HardwareKeyboardDetector, which is a more advanced version of the KeyboardEvents mixin: HardwareKeyboardDetector is a component instead of a mixin, which means it can be added/removed by the user at any point; multiple such detectors can be attached to a game - for example, in a 2-player game one component may be paying attention to arrow keys, while another to WASD keys; the new component uses Flutter's HardwareKeyboard interface, bypassing the need for a Focus widget; the component keeps the ordered list of keys that are currently being pressed, which is helpful for games where this order is important; there is the ability to temporarily pause the reception of key events using keyEventsPaused property.
- Loading branch information
Showing
9 changed files
with
591 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ arities | |
arity | ||
autofocus | ||
backpressure | ||
backquote | ||
backtick | ||
backticks | ||
bitfield | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# HardwareKeyboardDetector | ||
|
||
```{dartdoc} | ||
:file: src/events/hardware_keyboard_detector.dart | ||
:symbol: HardwareKeyboardDetector | ||
:package: flame | ||
[HardwareKeyboard]: https://api.flutter.dev/flutter/services/HardwareKeyboard-class.html | ||
[KeyDownEvent]: https://api.flutter.dev/flutter/services/KeyDownEvent-class.html | ||
[KeyUpEvent]: https://api.flutter.dev/flutter/services/KeyUpEvent-class.html | ||
[KeyRepeatEvent]: https://api.flutter.dev/flutter/services/KeyRepeatEvent-class.html | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
256 changes: 256 additions & 0 deletions
256
examples/lib/stories/input/hardware_keyboard_example.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
import 'package:flame/components.dart'; | ||
import 'package:flame/events.dart'; | ||
import 'package:flame/experimental.dart'; | ||
import 'package:flame/game.dart'; | ||
import 'package:flame/text.dart'; | ||
import 'package:flutter/rendering.dart'; | ||
import 'package:flutter/services.dart'; | ||
|
||
class HardwareKeyboardExample extends FlameGame { | ||
static String description = ''' | ||
This example uses the HardwareKeyboardDetector mixin in order to keep | ||
track of which keys on a keyboard are currently pressed. | ||
Tap as many keys on the keyboard at once as you want, and see whether the | ||
system can detect them or not. | ||
'''; | ||
|
||
/// The list of [KeyboardKey] components currently shown on the screen. This | ||
/// list is re-generated on every RawKeyEvent. These components are also | ||
/// attached as children. | ||
List<KeyboardKey> _keyComponents = []; | ||
|
||
void replaceKeyComponents(List<KeyboardKey> newComponents) { | ||
for (final key in _keyComponents) { | ||
key.visible = false; | ||
key.removeFromParent(); | ||
} | ||
_keyComponents = newComponents; | ||
addAll(_keyComponents); | ||
} | ||
|
||
@override | ||
void onLoad() { | ||
add(MyKeyboardDetector()); | ||
add( | ||
TextComponent( | ||
text: 'Press any key(s)', | ||
textRenderer: TextPaint( | ||
style: const TextStyle( | ||
fontSize: 12, | ||
color: Color(0x77ffffff), | ||
), | ||
), | ||
)..position = Vector2(80, 60), | ||
); | ||
} | ||
} | ||
|
||
class MyKeyboardDetector extends HardwareKeyboardDetector | ||
with HasGameReference<HardwareKeyboardExample> { | ||
@override | ||
void onKeyEvent(KeyEvent event) { | ||
final newComponents = <KeyboardKey>[]; | ||
var x0 = 80.0; | ||
const y0 = 100.0; | ||
for (final key in physicalKeysPressed) { | ||
final keyComponent = KeyboardKey( | ||
text: keyNames[key] ?? '[${key.usbHidUsage} ${key.debugName}]', | ||
position: Vector2(x0, y0), | ||
); | ||
newComponents.add(keyComponent); | ||
x0 += keyComponent.width + 10; | ||
} | ||
game.replaceKeyComponents(newComponents); | ||
} | ||
|
||
/// The names of keyboard keys (at least the most important ones). We can't | ||
/// rely on `key.debugName` because this property is not available in release | ||
/// builds. | ||
static Map<PhysicalKeyboardKey, String> keyNames = { | ||
PhysicalKeyboardKey.hyper: 'Hyper', | ||
PhysicalKeyboardKey.superKey: 'Super', | ||
PhysicalKeyboardKey.fn: 'Fn', | ||
PhysicalKeyboardKey.fnLock: 'FnLock', | ||
PhysicalKeyboardKey.gameButton1: 'Game 1', | ||
PhysicalKeyboardKey.gameButton2: 'Game 2 ', | ||
PhysicalKeyboardKey.gameButton3: 'Game 3', | ||
PhysicalKeyboardKey.gameButton4: 'Game 4', | ||
PhysicalKeyboardKey.gameButton5: 'Game 5', | ||
PhysicalKeyboardKey.gameButton6: 'Game 6', | ||
PhysicalKeyboardKey.gameButton7: 'Game 7', | ||
PhysicalKeyboardKey.gameButton8: 'Game 8', | ||
PhysicalKeyboardKey.gameButtonA: 'Game A', | ||
PhysicalKeyboardKey.gameButtonB: 'Game B', | ||
PhysicalKeyboardKey.gameButtonC: 'Game C', | ||
PhysicalKeyboardKey.gameButtonLeft1: 'Game L1', | ||
PhysicalKeyboardKey.gameButtonLeft2: 'Game L2', | ||
PhysicalKeyboardKey.gameButtonMode: 'Game Mode', | ||
PhysicalKeyboardKey.gameButtonRight1: 'Game R1', | ||
PhysicalKeyboardKey.gameButtonRight2: 'Game R2', | ||
PhysicalKeyboardKey.gameButtonSelect: 'Game Select', | ||
PhysicalKeyboardKey.gameButtonStart: 'Game Start', | ||
PhysicalKeyboardKey.gameButtonThumbLeft: 'Game LThumb', | ||
PhysicalKeyboardKey.gameButtonThumbRight: 'Game RThumb', | ||
PhysicalKeyboardKey.gameButtonX: 'Game X', | ||
PhysicalKeyboardKey.gameButtonY: 'Game Y', | ||
PhysicalKeyboardKey.gameButtonZ: 'Game Z', | ||
PhysicalKeyboardKey.keyA: 'A', | ||
PhysicalKeyboardKey.keyB: 'B', | ||
PhysicalKeyboardKey.keyC: 'C', | ||
PhysicalKeyboardKey.keyD: 'D', | ||
PhysicalKeyboardKey.keyE: 'E', | ||
PhysicalKeyboardKey.keyF: 'F', | ||
PhysicalKeyboardKey.keyG: 'G', | ||
PhysicalKeyboardKey.keyH: 'H', | ||
PhysicalKeyboardKey.keyI: 'I', | ||
PhysicalKeyboardKey.keyJ: 'J', | ||
PhysicalKeyboardKey.keyK: 'K', | ||
PhysicalKeyboardKey.keyL: 'L', | ||
PhysicalKeyboardKey.keyM: 'M', | ||
PhysicalKeyboardKey.keyN: 'N', | ||
PhysicalKeyboardKey.keyO: 'O', | ||
PhysicalKeyboardKey.keyP: 'P', | ||
PhysicalKeyboardKey.keyQ: 'Q', | ||
PhysicalKeyboardKey.keyR: 'R', | ||
PhysicalKeyboardKey.keyS: 'S', | ||
PhysicalKeyboardKey.keyT: 'T', | ||
PhysicalKeyboardKey.keyU: 'U', | ||
PhysicalKeyboardKey.keyV: 'V', | ||
PhysicalKeyboardKey.keyW: 'W', | ||
PhysicalKeyboardKey.keyX: 'X', | ||
PhysicalKeyboardKey.keyY: 'Y', | ||
PhysicalKeyboardKey.keyZ: 'Z', | ||
PhysicalKeyboardKey.digit1: '1', | ||
PhysicalKeyboardKey.digit2: '2', | ||
PhysicalKeyboardKey.digit3: '3', | ||
PhysicalKeyboardKey.digit4: '4', | ||
PhysicalKeyboardKey.digit5: '5', | ||
PhysicalKeyboardKey.digit6: '6', | ||
PhysicalKeyboardKey.digit7: '7', | ||
PhysicalKeyboardKey.digit8: '8', | ||
PhysicalKeyboardKey.digit9: '9', | ||
PhysicalKeyboardKey.digit0: '0', | ||
PhysicalKeyboardKey.enter: 'Enter', | ||
PhysicalKeyboardKey.escape: 'Esc', | ||
PhysicalKeyboardKey.backspace: 'Backspace', | ||
PhysicalKeyboardKey.tab: 'Tab', | ||
PhysicalKeyboardKey.space: 'Space', | ||
PhysicalKeyboardKey.minus: '-', | ||
PhysicalKeyboardKey.equal: '=', | ||
PhysicalKeyboardKey.bracketLeft: '[', | ||
PhysicalKeyboardKey.bracketRight: ']', | ||
PhysicalKeyboardKey.backslash: r'\', | ||
PhysicalKeyboardKey.semicolon: ';', | ||
PhysicalKeyboardKey.quote: "'", | ||
PhysicalKeyboardKey.backquote: '`', | ||
PhysicalKeyboardKey.comma: ',', | ||
PhysicalKeyboardKey.period: '.', | ||
PhysicalKeyboardKey.slash: '/', | ||
PhysicalKeyboardKey.capsLock: 'CapsLock', | ||
PhysicalKeyboardKey.f1: 'F1', | ||
PhysicalKeyboardKey.f2: 'F2', | ||
PhysicalKeyboardKey.f3: 'F3', | ||
PhysicalKeyboardKey.f4: 'F4', | ||
PhysicalKeyboardKey.f5: 'F5', | ||
PhysicalKeyboardKey.f6: 'F6', | ||
PhysicalKeyboardKey.f7: 'F7', | ||
PhysicalKeyboardKey.f8: 'F8', | ||
PhysicalKeyboardKey.f9: 'F9', | ||
PhysicalKeyboardKey.f10: 'F10', | ||
PhysicalKeyboardKey.f11: 'F11', | ||
PhysicalKeyboardKey.f12: 'F12', | ||
PhysicalKeyboardKey.f13: 'F13', | ||
PhysicalKeyboardKey.f14: 'F14', | ||
PhysicalKeyboardKey.f15: 'F15', | ||
PhysicalKeyboardKey.f16: 'F16', | ||
PhysicalKeyboardKey.printScreen: 'PrintScreen', | ||
PhysicalKeyboardKey.scrollLock: 'ScrollLock', | ||
PhysicalKeyboardKey.pause: 'Pause', | ||
PhysicalKeyboardKey.insert: 'Insert', | ||
PhysicalKeyboardKey.home: 'Home', | ||
PhysicalKeyboardKey.pageUp: 'PageUp', | ||
PhysicalKeyboardKey.delete: 'Delete', | ||
PhysicalKeyboardKey.end: 'End', | ||
PhysicalKeyboardKey.pageDown: 'PageDown', | ||
PhysicalKeyboardKey.arrowRight: 'ArrowRight', | ||
PhysicalKeyboardKey.arrowLeft: 'ArrowLeft', | ||
PhysicalKeyboardKey.arrowDown: 'ArrowDown', | ||
PhysicalKeyboardKey.arrowUp: 'ArrowUp', | ||
PhysicalKeyboardKey.numLock: 'NumLock', | ||
PhysicalKeyboardKey.numpadDivide: 'Num /', | ||
PhysicalKeyboardKey.numpadMultiply: 'Num *', | ||
PhysicalKeyboardKey.numpadSubtract: 'Num -', | ||
PhysicalKeyboardKey.numpadAdd: 'Num +', | ||
PhysicalKeyboardKey.numpadEnter: 'Num Enter', | ||
PhysicalKeyboardKey.numpad1: 'Num 1', | ||
PhysicalKeyboardKey.numpad2: 'Num 2', | ||
PhysicalKeyboardKey.numpad3: 'Num 3', | ||
PhysicalKeyboardKey.numpad4: 'Num 4', | ||
PhysicalKeyboardKey.numpad5: 'Num 5', | ||
PhysicalKeyboardKey.numpad6: 'Num 6', | ||
PhysicalKeyboardKey.numpad7: 'Num 7', | ||
PhysicalKeyboardKey.numpad8: 'Num 8', | ||
PhysicalKeyboardKey.numpad9: 'Num 9', | ||
PhysicalKeyboardKey.numpad0: 'Num 0', | ||
PhysicalKeyboardKey.numpadDecimal: 'Num .', | ||
PhysicalKeyboardKey.contextMenu: 'ContextMenu', | ||
PhysicalKeyboardKey.controlLeft: 'LControl', | ||
PhysicalKeyboardKey.shiftLeft: 'LShift', | ||
PhysicalKeyboardKey.altLeft: 'LAlt', | ||
PhysicalKeyboardKey.metaLeft: 'LMeta', | ||
PhysicalKeyboardKey.controlRight: 'RControl', | ||
PhysicalKeyboardKey.shiftRight: 'RShift', | ||
PhysicalKeyboardKey.altRight: 'RAlt', | ||
PhysicalKeyboardKey.metaRight: 'RMeta', | ||
}; | ||
} | ||
|
||
class KeyboardKey extends PositionComponent { | ||
KeyboardKey({required this.text, super.position}) { | ||
textElement = textRenderer.formatter.format(text); | ||
width = textElement.metrics.width + padding.x; | ||
height = textElement.metrics.height + padding.y; | ||
textElement.translate( | ||
padding.x / 2, | ||
padding.y / 2 + textElement.metrics.ascent, | ||
); | ||
rect = RRect.fromLTRBR(0, 0, width, height, const Radius.circular(8)); | ||
} | ||
|
||
final String text; | ||
late final TextElement textElement; | ||
late final RRect rect; | ||
|
||
/// The RawKeyEvents may occur very fast, and out of sync with the game loop. | ||
/// On each such event we remove old KeyboardKey components, and add new ones. | ||
/// However, since multiple RawKeyEvents may occur within a single game tick, | ||
/// we end up adding/removing components many times within that tick, and for | ||
/// a brief moment there could be a situation that the old components still | ||
/// haven't been removed while the new ones were already added. In order to | ||
/// prevent this from happening, we mark all components that are about to be | ||
/// removed as "not visible", which prevents them from being rendered while | ||
/// they are waiting to be removed. | ||
bool visible = true; | ||
|
||
static Vector2 padding = Vector2(24, 12); | ||
static Paint borderPaint = Paint() | ||
..style = PaintingStyle.stroke | ||
..strokeWidth = 3 | ||
..color = const Color(0xffb5ffd0); | ||
static TextPaint textRenderer = TextPaint( | ||
style: const TextStyle( | ||
fontSize: 20, | ||
fontWeight: FontWeight.bold, | ||
color: Color(0xffb5ffd0), | ||
), | ||
); | ||
|
||
@override | ||
void render(Canvas canvas) { | ||
if (visible) { | ||
canvas.drawRRect(rect, borderPaint); | ||
textElement.render(canvas); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.