mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 20:36:31 +08:00
Feat: Add keyboard with focus node implementation (#909)
* Add keyboard with focus node implementation * a * format and make it stabel compatible * Add mixin to game * fixes * add tests * format * docs * more docs * Update doc/keyboard-input.md Co-authored-by: Erick <erickzanardoo@gmail.com> * rename test * Apply suggestions from code review Co-authored-by: Luan Nico <luanpotter27@gmail.com> * fix test * Update tutorials/2_sprite_animations_gestures/README.md Co-authored-by: Luan Nico <luanpotter27@gmail.com> * docs * Apply suggestions from code review Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net> * yo Co-authored-by: Erick <erickzanardoo@gmail.com> Co-authored-by: Luan Nico <luanpotter27@gmail.com> Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net>
This commit is contained in:
@ -1,6 +1,13 @@
|
|||||||
# Input
|
# Gesture Input
|
||||||
|
|
||||||
## Gestures
|
This includes documentation for gesture inputs, which is, mouse and touch pointers.
|
||||||
|
|
||||||
|
For other input documents, see also:
|
||||||
|
|
||||||
|
- [Keyboard Input](keyboard-input.md): for keystrokes
|
||||||
|
- [Other Inputs](other-inputs.md): For joysticks, game pads, etc.
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
Inside `package:flame/gestures.dart` you can find a whole set of `mixin`s which can be included on
|
Inside `package:flame/gestures.dart` you can find a whole set of `mixin`s which can be included on
|
||||||
your game class instance to be able to receive touch input events. Below you can see the full list
|
your game class instance to be able to receive touch input events. Below you can see the full list
|
||||||
@ -302,150 +309,4 @@ for the event to be counted on your component.
|
|||||||
An example of you to use it can be seen
|
An example of you to use it can be seen
|
||||||
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/).
|
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/).
|
||||||
|
|
||||||
## Keyboard
|
|
||||||
|
|
||||||
Flame provides a simple way to access Flutter's features regarding accessing Keyboard input events.
|
|
||||||
|
|
||||||
To use it, just add the `KeyboardEvents` mixin to your game class.
|
|
||||||
When doing this you will need to implement the `onKeyEvent` method, this method is called every time
|
|
||||||
a keyboard event happens, and it receives an instance of the Flutter class `RawKeyEvent`.
|
|
||||||
This event can be used to get information about what occurred, such as if it was a key down or key
|
|
||||||
up event, and which key was pressed etc.
|
|
||||||
|
|
||||||
Minimal example:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flame/input.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
class MyGame extends Game with KeyboardEvents {
|
|
||||||
// update and render omitted
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onKeyEvent(e) {
|
|
||||||
final bool isKeyDown = e is RawKeyDownEvent;
|
|
||||||
print(" Key: ${e.data.keyLabel} - isKeyDown: $isKeyDown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also check a more complete example
|
|
||||||
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/keyboard.dart).
|
|
||||||
|
|
||||||
## Joystick
|
|
||||||
|
|
||||||
Flame provides a component capable of creating a virtual joystick for taking input for your game.
|
|
||||||
To use this feature you need to create a `JoystickComponent`, configure it the way you want, and
|
|
||||||
add it to your game.
|
|
||||||
|
|
||||||
To receive the inputs from the joystick component, pass your `JoystickComponent` to the component
|
|
||||||
that you want it to control, or simply act upon input from it in the update-loop of your game.
|
|
||||||
|
|
||||||
Check this example to get a better understanding:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class MyGame extends BaseGame with HasDraggableComponents {
|
|
||||||
|
|
||||||
MyGame() {
|
|
||||||
joystick.addObserver(player);
|
|
||||||
add(player);
|
|
||||||
add(joystick);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
super.onLoad();
|
|
||||||
final image = await images.load('joystick.png');
|
|
||||||
final sheet = SpriteSheet.fromColumnsAndRows(
|
|
||||||
image: image,
|
|
||||||
columns: 6,
|
|
||||||
rows: 1,
|
|
||||||
);
|
|
||||||
final joystick = JoystickComponent(
|
|
||||||
knob: SpriteComponent(
|
|
||||||
sprite: sheet.getSpriteById(1),
|
|
||||||
size: Vector2.all(100),
|
|
||||||
),
|
|
||||||
background: SpriteComponent(
|
|
||||||
sprite: sheet.getSpriteById(0),
|
|
||||||
size: Vector2.all(150),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(left: 40, bottom: 40),
|
|
||||||
);
|
|
||||||
|
|
||||||
final player = Player(joystick);
|
|
||||||
add(player);
|
|
||||||
add(joystick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class JoystickPlayer extends SpriteComponent with HasGameRef {
|
|
||||||
/// Pixels/s
|
|
||||||
double maxSpeed = 300.0;
|
|
||||||
|
|
||||||
final JoystickComponent joystick;
|
|
||||||
|
|
||||||
JoystickPlayer(this.joystick)
|
|
||||||
: super(
|
|
||||||
size: Vector2.all(100.0),
|
|
||||||
) {
|
|
||||||
anchor = Anchor.center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
super.onLoad();
|
|
||||||
sprite = await gameRef.loadSprite('layers/player.png');
|
|
||||||
position = gameRef.size / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(double dt) {
|
|
||||||
super.update(dt);
|
|
||||||
if (joystick.direction != JoystickDirection.idle) {
|
|
||||||
position.add(joystick.velocity * maxSpeed * dt);
|
|
||||||
angle = joystick.delta.screenAngle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
So in this example we create the classes `MyGame` and `Player`. `MyGame` creates a joystick which is
|
|
||||||
passed to the `Player` when it is created. In the `Player` class we act upon the current state of
|
|
||||||
the joystick.
|
|
||||||
|
|
||||||
The joystick has a few fields that change depending on what state it is in.
|
|
||||||
These are the fields that should be used to know the state of the joystick:
|
|
||||||
- `intensity`: The percentage [0.0, 1.0] that the knob is dragged from the epicenter to the edge of
|
|
||||||
the joystick (or `knobRadius` if that is set).
|
|
||||||
- `delta`: The absolute amount (defined as a `Vector2`) that the knob is dragged from its epicenter.
|
|
||||||
- `velocity`: The percentage, presented as a `Vector2`, and direction that the knob is currently
|
|
||||||
pulled from its base position to a edge of the joystick.
|
|
||||||
|
|
||||||
If you want to create buttons to go with your joystick, check out
|
|
||||||
[`MarginButtonComponent`](#HudButtonComponent).
|
|
||||||
|
|
||||||
A full examples of how to use it can be found
|
|
||||||
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/joystick.dart).
|
|
||||||
And it can be seen running [here](https://examples.flame-engine.org/#/Controls_Joystick).
|
|
||||||
|
|
||||||
## HudButtonComponent
|
|
||||||
A `HudButtonComponent` is a button that can be defined with margins to the edge of the `Viewport`
|
|
||||||
instead of with a position. It takes two `PositionComponent`s. `button` and `buttonDown`, the first
|
|
||||||
is used for when the button is idle and the second is shown when the button is being pressed. The
|
|
||||||
second one is optional if you don't want to change the look of the button when it is pressed, or if
|
|
||||||
you handle this through the `button` component.
|
|
||||||
|
|
||||||
As the name suggests this button is a hud by default, which means that it will be static on your
|
|
||||||
screen even if the camera for the game moves around. You can also use this component as a non-hud by
|
|
||||||
setting `hudButtonComponent.isHud = false;`.
|
|
||||||
|
|
||||||
If you want to act upon the button being pressed (which I guess that you do) you can either pass in
|
|
||||||
a callback function as the `onPressed` argument, or you extend the component and override
|
|
||||||
`onTapDown`, `onTapUp` and/or `onTapCancel` and implement your logic in there.
|
|
||||||
|
|
||||||
## Gamepad
|
|
||||||
|
|
||||||
Flame has a separate plugin for gamepad support, you can checkout the plugin
|
|
||||||
[here](https://github.com/flame-engine/flame_gamepad) for more information.
|
|
||||||
94
doc/keyboard-input.md
Normal file
94
doc/keyboard-input.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Keyboard Input
|
||||||
|
|
||||||
|
This includes documentation for keyboard inputs.
|
||||||
|
|
||||||
|
For other input documents, see also:
|
||||||
|
|
||||||
|
- [Gesture Input](gesture-input.md): for mouse and touch pointer gestures
|
||||||
|
- [Other Inputs](other-inputs.md): For joysticks, game pads, etc.
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
There are two ways a game can be sensitive to key strokes; at the game level and at a component level.
|
||||||
|
For each we have a mixin that can me added to `Game`s and `BaseComponent`s, respectively.
|
||||||
|
|
||||||
|
### Receive keyboard events in a game level
|
||||||
|
|
||||||
|
To make a `Game` sub class sensitive to key stroke, mix it with `KeyboardEvents`.
|
||||||
|
|
||||||
|
After that, it will be possible to override an `onKeyEvent` method.
|
||||||
|
|
||||||
|
This method receives two parameters, first the [`RawKeyEvent`](https://api.flutter.dev/flutter/services/RawKeyEvent-class.html)
|
||||||
|
that triggered the callback in the first place. The second is a set of the currently pressed [`LogicalKeyboardKey`](https://api.flutter.dev/flutter/widgets/KeyEventResult-class.html).
|
||||||
|
|
||||||
|
The return value should be a [`KeyEventResult`](https://api.flutter.dev/flutter/widgets/KeyEventResult-class.html).
|
||||||
|
|
||||||
|
`KeyEventResult.handled` will tell the framework that the key stroke was resolved inside of Flame and skip any other keyboard handler widgets apart of `GameWidget`.
|
||||||
|
|
||||||
|
`KeyEventResult.ignored` will tell the framework to keep testing this event in any other keyboard handler widget apart of `GameWidget`. If the event is not resolved by any handler, the framework will trigger `SystemSoundType.alert`.
|
||||||
|
|
||||||
|
`KeyEventResult.skipRemainingHandlers` is very similar to `.ignored`, apart from the fact that will skip any other handler widget and will straight up play the alert sound.
|
||||||
|
|
||||||
|
Minimal example:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyGame extends Game with KeyboardEvents {
|
||||||
|
// ...
|
||||||
|
@override
|
||||||
|
KeyEventResult onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
final isKeyDown = event is RawKeyDownEvent;
|
||||||
|
|
||||||
|
final isSpace = event == LogicalKeyboardKey.space;
|
||||||
|
|
||||||
|
if (isSpace && isKeyDown) {
|
||||||
|
if (keysPressed.contains(LogicalKeyboardKey.altLeft) ||
|
||||||
|
keysPressed.contains(LogicalKeyboardKey.altRight)) {
|
||||||
|
this.shootHarder();
|
||||||
|
} else {
|
||||||
|
this.shoot();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Receive keyboard events in a component level
|
||||||
|
|
||||||
|
To receive keyboard events directly in components, there is the mixin `KeyboardHandler`.
|
||||||
|
|
||||||
|
Similarly to `Tappable` and `Draggable`, `KeyboardHandler` can be mixed into any `BaseComponent`
|
||||||
|
subclass.
|
||||||
|
|
||||||
|
KeyboardHandlers must only be added to games that are mixed with `HasKeyboardHandlerComponents`.
|
||||||
|
|
||||||
|
> ⚠️ Attention: If `HasKeyboardHandlerComponents` is used, you must remove `KeyboardEvents`
|
||||||
|
> from the game mixin list to avoid conflicts.
|
||||||
|
|
||||||
|
After applying `HasKeyboardHandlerComponents`, it will be possible to override an `onKeyEvent` method.
|
||||||
|
|
||||||
|
This method receives two parameters. First the [`RawKeyEvent`](https://api.flutter.dev/flutter/services/RawKeyEvent-class.html)
|
||||||
|
that triggered the callback in the first place. The second is a set of the currently pressed [`LogicalKeyboardKey`](https://api.flutter.dev/flutter/widgets/KeyEventResult-class.html)s.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### Controlling focus
|
||||||
|
|
||||||
|
On the widget level, it is possible to use the [`FocusNode`](https://api.flutter.dev/flutter/widgets/FocusNode-class.html) API to control whether the game is focused or not.
|
||||||
|
|
||||||
|
`GameWidget` has an optional `focusNode` parameter that allow its focus to be controlled externally.
|
||||||
|
|
||||||
|
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
|
||||||
|
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/keyboard.dart).
|
||||||
123
doc/other-inputs.md
Normal file
123
doc/other-inputs.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Other inputs
|
||||||
|
|
||||||
|
This includes documentation for input methods besides keyboard and mouse.
|
||||||
|
|
||||||
|
For other input documents, see also:
|
||||||
|
|
||||||
|
- [Gesture Input](gesture-input.md): for mouse and touch pointer gestures
|
||||||
|
- [Keyboard Input](keyboard-input.md): for keystrokes
|
||||||
|
|
||||||
|
## Joystick
|
||||||
|
|
||||||
|
Flame provides a component capable of creating a virtual joystick for taking input for your game.
|
||||||
|
To use this feature, you need to create a `JoystickComponent`, configure it the way you want, and
|
||||||
|
add it to your game.
|
||||||
|
|
||||||
|
Check this example to get a better understanding:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyGame extends BaseGame with HasDraggableComponents {
|
||||||
|
|
||||||
|
MyGame() {
|
||||||
|
joystick.addObserver(player);
|
||||||
|
add(player);
|
||||||
|
add(joystick);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
super.onLoad();
|
||||||
|
final image = await images.load('joystick.png');
|
||||||
|
final sheet = SpriteSheet.fromColumnsAndRows(
|
||||||
|
image: image,
|
||||||
|
columns: 6,
|
||||||
|
rows: 1,
|
||||||
|
);
|
||||||
|
final joystick = JoystickComponent(
|
||||||
|
knob: SpriteComponent(
|
||||||
|
sprite: sheet.getSpriteById(1),
|
||||||
|
size: Vector2.all(100),
|
||||||
|
),
|
||||||
|
background: SpriteComponent(
|
||||||
|
sprite: sheet.getSpriteById(0),
|
||||||
|
size: Vector2.all(150),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(left: 40, bottom: 40),
|
||||||
|
);
|
||||||
|
|
||||||
|
final player = Player(joystick);
|
||||||
|
add(player);
|
||||||
|
add(joystick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JoystickPlayer extends SpriteComponent with HasGameRef {
|
||||||
|
/// Pixels/s
|
||||||
|
double maxSpeed = 300.0;
|
||||||
|
|
||||||
|
final JoystickComponent joystick;
|
||||||
|
|
||||||
|
JoystickPlayer(this.joystick)
|
||||||
|
: super(
|
||||||
|
size: Vector2.all(100.0),
|
||||||
|
) {
|
||||||
|
anchor = Anchor.center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
super.onLoad();
|
||||||
|
sprite = await gameRef.loadSprite('layers/player.png');
|
||||||
|
position = gameRef.size / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
if (joystick.direction != JoystickDirection.idle) {
|
||||||
|
position.add(joystick.velocity * maxSpeed * dt);
|
||||||
|
angle = joystick.delta.screenAngle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So in this example, we create the classes `MyGame` and `Player`. `MyGame` creates a joystick which is
|
||||||
|
passed to the `Player` when it is created. In the `Player` class we act upon the current state of
|
||||||
|
the joystick.
|
||||||
|
|
||||||
|
The joystick has a few fields that change depending on what state it is in.
|
||||||
|
These are the fields that should be used to know the state of the joystick:
|
||||||
|
- `intensity`: The percentage [0.0, 1.0] that the knob is dragged from the epicenter to the edge of
|
||||||
|
the joystick (or `knobRadius` if that is set).
|
||||||
|
- `delta`: The absolute amount (defined as a `Vector2`) that the knob is dragged from its epicenter.
|
||||||
|
- `velocity`: The percentage, presented as a `Vector2`, and direction that the knob is currently
|
||||||
|
pulled from its base position to a edge of the joystick.
|
||||||
|
|
||||||
|
If you want to create buttons to go with your joystick, check out
|
||||||
|
[`MarginButtonComponent`](#HudButtonComponent).
|
||||||
|
|
||||||
|
A full examples of how to use it can be found
|
||||||
|
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/joystick.dart).
|
||||||
|
And it can be seen running [here](https://examples.flame-engine.org/#/Controls_Joystick).
|
||||||
|
|
||||||
|
## HudButtonComponent
|
||||||
|
|
||||||
|
A `HudButtonComponent` is a button that can be defined with margins to the edge of the `Viewport`
|
||||||
|
instead of with a position. It takes two `PositionComponent`s. `button` and `buttonDown`, the first
|
||||||
|
is used for when the button is idle and the second is shown when the button is being pressed. The
|
||||||
|
second one is optional if you don't want to change the look of the button when it is pressed, or if
|
||||||
|
you handle this through the `button` component.
|
||||||
|
|
||||||
|
As the name suggests this button is a hud by default, which means that it will be static on your
|
||||||
|
screen even if the camera for the game moves around. You can also use this component as a non-hud by
|
||||||
|
setting `hudButtonComponent.isHud = false;`.
|
||||||
|
|
||||||
|
If you want to act upon the button being pressed (which would be the common thing to do) you can either pass in
|
||||||
|
a callback function as the `onPressed` argument, or you extend the component and override
|
||||||
|
`onTapDown`, `onTapUp` and/or `onTapCancel` and implement your logic in there.
|
||||||
|
|
||||||
|
## Gamepad
|
||||||
|
|
||||||
|
Flame has a separate plugin to support external game controllers (gamepads), checkout
|
||||||
|
[here](https://github.com/flame-engine/flame_gamepad) for more information.
|
||||||
@ -4,12 +4,16 @@
|
|||||||
- [File structure](structure.md)
|
- [File structure](structure.md)
|
||||||
- [Game loop](game.md)
|
- [Game loop](game.md)
|
||||||
- [Components](components.md)
|
- [Components](components.md)
|
||||||
- [Input](input.md)
|
|
||||||
- [Platforms](platforms.md)
|
- [Platforms](platforms.md)
|
||||||
- [Collision detection](collision_detection.md)
|
- [Collision detection](collision_detection.md)
|
||||||
- [Effects](effects.md)
|
- [Effects](effects.md)
|
||||||
- [Camera & Viewport](camera_and_viewport.md)
|
- [Camera & Viewport](camera_and_viewport.md)
|
||||||
|
|
||||||
|
- Inputs
|
||||||
|
- [Gesture Input](gesture-input.md)
|
||||||
|
- [Keyboard Input](keyboard-input.md)
|
||||||
|
- [Other Inputs](other-inputs.md)
|
||||||
|
|
||||||
- Audio
|
- Audio
|
||||||
- [General Audio](audio.md)
|
- [General Audio](audio.md)
|
||||||
- [Looping Background Music](bgm.md)
|
- [Looping Background Music](bgm.md)
|
||||||
|
|||||||
@ -211,22 +211,28 @@ class CoordinateSystemsGame extends BaseGame
|
|||||||
|
|
||||||
/// Camera controls.
|
/// Camera controls.
|
||||||
@override
|
@override
|
||||||
void onKeyEvent(RawKeyEvent e) {
|
KeyEventResult onKeyEvent(
|
||||||
final isKeyDown = e is RawKeyDownEvent;
|
RawKeyEvent event,
|
||||||
if (e.data.keyLabel == 'a') {
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
final isKeyDown = event is RawKeyDownEvent;
|
||||||
|
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.keyA) {
|
||||||
cameraVelocity.x = isKeyDown ? -1 : 0;
|
cameraVelocity.x = isKeyDown ? -1 : 0;
|
||||||
} else if (e.data.keyLabel == 'd') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
|
||||||
cameraVelocity.x = isKeyDown ? 1 : 0;
|
cameraVelocity.x = isKeyDown ? 1 : 0;
|
||||||
} else if (e.data.keyLabel == 'w') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
|
||||||
cameraVelocity.y = isKeyDown ? -1 : 0;
|
cameraVelocity.y = isKeyDown ? -1 : 0;
|
||||||
} else if (e.data.keyLabel == 's') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
|
||||||
cameraVelocity.y = isKeyDown ? 1 : 0;
|
cameraVelocity.y = isKeyDown ? 1 : 0;
|
||||||
} else if (isKeyDown) {
|
} else if (isKeyDown) {
|
||||||
if (e.data.keyLabel == 'q') {
|
if (event.logicalKey == LogicalKeyboardKey.keyQ) {
|
||||||
camera.zoom *= 2;
|
camera.zoom *= 2;
|
||||||
} else if (e.data.keyLabel == 'e') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyE) {
|
||||||
camera.zoom /= 2;
|
camera.zoom /= 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:flame/components.dart';
|
|||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/geometry.dart';
|
import 'package:flame/geometry.dart';
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@ -12,7 +13,11 @@ import '../../commons/square_component.dart';
|
|||||||
final R = Random();
|
final R = Random();
|
||||||
|
|
||||||
class MovableSquare extends SquareComponent
|
class MovableSquare extends SquareComponent
|
||||||
with Hitbox, Collidable, HasGameRef<CameraAndViewportGame> {
|
with
|
||||||
|
Hitbox,
|
||||||
|
Collidable,
|
||||||
|
HasGameRef<CameraAndViewportGame>,
|
||||||
|
KeyboardHandler {
|
||||||
static const double speed = 300;
|
static const double speed = 300;
|
||||||
static final TextPaint textRenderer = TextPaint(
|
static final TextPaint textRenderer = TextPaint(
|
||||||
config: const TextPaintConfig(
|
config: const TextPaintConfig(
|
||||||
@ -55,6 +60,27 @@ class MovableSquare extends SquareComponent
|
|||||||
timer.start();
|
timer.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
||||||
|
final isKeyDown = event is RawKeyDownEvent;
|
||||||
|
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.keyA) {
|
||||||
|
velocity.x = isKeyDown ? -1 : 0;
|
||||||
|
return false;
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
|
||||||
|
velocity.x = isKeyDown ? 1 : 0;
|
||||||
|
return false;
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
|
||||||
|
velocity.y = isKeyDown ? -1 : 0;
|
||||||
|
return false;
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
|
||||||
|
velocity.y = isKeyDown ? 1 : 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onKeyEvent(event, keysPressed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Map extends Component {
|
class Map extends Component {
|
||||||
@ -112,7 +138,7 @@ class Rock extends SquareComponent with Hitbox, Collidable, Tappable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CameraAndViewportGame extends BaseGame
|
class CameraAndViewportGame extends BaseGame
|
||||||
with KeyboardEvents, HasCollidables, HasTappableComponents {
|
with HasCollidables, HasTappableComponents, HasKeyboardHandlerComponents {
|
||||||
late MovableSquare square;
|
late MovableSquare square;
|
||||||
|
|
||||||
final Vector2 viewportResolution;
|
final Vector2 viewportResolution;
|
||||||
@ -134,18 +160,4 @@ class CameraAndViewportGame extends BaseGame
|
|||||||
add(Rock(Vector2(Map.genCoord(), Map.genCoord())));
|
add(Rock(Vector2(Map.genCoord(), Map.genCoord())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void onKeyEvent(RawKeyEvent e) {
|
|
||||||
final isKeyDown = e is RawKeyDownEvent;
|
|
||||||
if (e.data.keyLabel == 'a') {
|
|
||||||
square.velocity.x = isKeyDown ? -1 : 0;
|
|
||||||
} else if (e.data.keyLabel == 'd') {
|
|
||||||
square.velocity.x = isKeyDown ? 1 : 0;
|
|
||||||
} else if (e.data.keyLabel == 'w') {
|
|
||||||
square.velocity.y = isKeyDown ? -1 : 0;
|
|
||||||
} else if (e.data.keyLabel == 's') {
|
|
||||||
square.velocity.y = isKeyDown ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
|
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
import 'package:flutter/services.dart' show RawKeyDownEvent, RawKeyEvent;
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class KeyboardGame extends Game with KeyboardEvents {
|
class KeyboardGame extends Game with KeyboardEvents {
|
||||||
static final Paint white = BasicPalette.white.paint();
|
static final Paint white = BasicPalette.white.paint();
|
||||||
@ -24,16 +26,22 @@ class KeyboardGame extends Game with KeyboardEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onKeyEvent(RawKeyEvent e) {
|
KeyEventResult onKeyEvent(
|
||||||
final isKeyDown = e is RawKeyDownEvent;
|
RawKeyEvent event,
|
||||||
if (e.data.keyLabel == 'a') {
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
final isKeyDown = event is RawKeyDownEvent;
|
||||||
|
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.keyA) {
|
||||||
velocity.x = isKeyDown ? -1 : 0;
|
velocity.x = isKeyDown ? -1 : 0;
|
||||||
} else if (e.data.keyLabel == 'd') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
|
||||||
velocity.x = isKeyDown ? 1 : 0;
|
velocity.x = isKeyDown ? 1 : 0;
|
||||||
} else if (e.data.keyLabel == 'w') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
|
||||||
velocity.y = isKeyDown ? -1 : 0;
|
velocity.y = isKeyDown ? -1 : 0;
|
||||||
} else if (e.data.keyLabel == 's') {
|
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
|
||||||
velocity.y = isKeyDown ? 1 : 0;
|
velocity.y = isKeyDown ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return super.onKeyEvent(event, keysPressed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/parallax.dart';
|
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame/parallax.dart';
|
||||||
|
|
||||||
class AdvancedParallaxGame extends BaseGame {
|
class AdvancedParallaxGame extends BaseGame {
|
||||||
final _layersMeta = {
|
final _layersMeta = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/extensions.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/parallax.dart';
|
import 'package:flame/parallax.dart';
|
||||||
import 'package:flame/extensions.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// This examples serves to test the Parallax feature outside of the
|
/// This examples serves to test the Parallax feature outside of the
|
||||||
|
|||||||
@ -41,6 +41,8 @@
|
|||||||
- Update `Camera` docs to showcase usage with `Game` class
|
- Update `Camera` docs to showcase usage with `Game` class
|
||||||
- Fixed a bug with `worldBounds` being set to `null` in `Camera`
|
- Fixed a bug with `worldBounds` being set to `null` in `Camera`
|
||||||
- `MockCanvas` is now strongly typed and matches numeric coordinates up to a tolerance
|
- `MockCanvas` is now strongly typed and matches numeric coordinates up to a tolerance
|
||||||
|
- Reviewed the keyboard API with new mixins (`KeyboardHandler` and `HasKeyboardHandlerComponents`)
|
||||||
|
- Added `FocusNode` on the game widget and improved keyboard handling in the game.
|
||||||
|
|
||||||
## [1.0.0-releasecandidate.13]
|
## [1.0.0-releasecandidate.13]
|
||||||
- Fix camera not ending up in the correct position on long jumps
|
- Fix camera not ending up in the correct position on long jumps
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../assets/assets_cache.dart';
|
import '../assets/assets_cache.dart';
|
||||||
@ -13,7 +12,6 @@ import '../extensions/vector2.dart';
|
|||||||
import '../sprite.dart';
|
import '../sprite.dart';
|
||||||
import '../sprite_animation.dart';
|
import '../sprite_animation.dart';
|
||||||
import 'game_render_box.dart';
|
import 'game_render_box.dart';
|
||||||
import 'mixins/keyboard.dart';
|
|
||||||
import 'projector.dart';
|
import 'projector.dart';
|
||||||
|
|
||||||
/// Represents a generic game.
|
/// Represents a generic game.
|
||||||
@ -87,10 +85,6 @@ abstract class Game extends Projector {
|
|||||||
/// Use for calculating the FPS.
|
/// Use for calculating the FPS.
|
||||||
void onTimingsCallback(List<FrameTiming> timings) {}
|
void onTimingsCallback(List<FrameTiming> timings) {}
|
||||||
|
|
||||||
void _handleKeyEvent(RawKeyEvent e) {
|
|
||||||
(this as KeyboardEvents).onKeyEvent(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks game as not attached tto any widget tree.
|
/// Marks game as not attached tto any widget tree.
|
||||||
///
|
///
|
||||||
/// Should be called manually.
|
/// Should be called manually.
|
||||||
@ -107,11 +101,7 @@ abstract class Game extends Projector {
|
|||||||
|
|
||||||
// Called when the Game widget is attached
|
// Called when the Game widget is attached
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void onAttach() {
|
void onAttach() {}
|
||||||
if (this is KeyboardEvents) {
|
|
||||||
RawKeyboard.instance.addListener(_handleKeyEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks game as not attached tto any widget tree.
|
/// Marks game as not attached tto any widget tree.
|
||||||
///
|
///
|
||||||
@ -125,12 +115,6 @@ abstract class Game extends Projector {
|
|||||||
// Called when the Game widget is detached
|
// Called when the Game widget is detached
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void onDetach() {
|
void onDetach() {
|
||||||
// Keeping this here, because if we leave this on HasWidgetsOverlay
|
|
||||||
// and somebody overrides this and forgets to call the stream close
|
|
||||||
// we can face some leaks.
|
|
||||||
if (this is KeyboardEvents) {
|
|
||||||
RawKeyboard.instance.removeListener(_handleKeyEvent);
|
|
||||||
}
|
|
||||||
images.clearCache();
|
images.clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../../../extensions.dart';
|
import '../../../extensions.dart';
|
||||||
|
import '../../../input.dart';
|
||||||
import '../../extensions/size.dart';
|
import '../../extensions/size.dart';
|
||||||
import '../game.dart';
|
import '../game.dart';
|
||||||
import '../game_render_box.dart';
|
import '../game_render_box.dart';
|
||||||
@ -58,6 +60,14 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
|||||||
/// - [Game.overlays]
|
/// - [Game.overlays]
|
||||||
final List<String>? initialActiveOverlays;
|
final List<String>? initialActiveOverlays;
|
||||||
|
|
||||||
|
/// The [FocusNode] to control the games focus to receive event inputs.
|
||||||
|
/// If omitted, defaults to an internally controlled focus node.
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
/// Whether the [focusNode] requests focus once the game is mounted.
|
||||||
|
/// Defaults to true.
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
/// Renders a [game] in a flutter widget tree.
|
/// Renders a [game] in a flutter widget tree.
|
||||||
///
|
///
|
||||||
/// Ex:
|
/// Ex:
|
||||||
@ -104,6 +114,8 @@ class GameWidget<T extends Game> extends StatefulWidget {
|
|||||||
this.backgroundBuilder,
|
this.backgroundBuilder,
|
||||||
this.overlayBuilderMap,
|
this.overlayBuilderMap,
|
||||||
this.initialActiveOverlays,
|
this.initialActiveOverlays,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Renders a [game] in a flutter widget tree alongside widgets overlays.
|
/// Renders a [game] in a flutter widget tree alongside widgets overlays.
|
||||||
@ -117,6 +129,7 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
Set<String> initialActiveOverlays = {};
|
Set<String> initialActiveOverlays = {};
|
||||||
|
|
||||||
Future<void>? _gameLoaderFuture;
|
Future<void>? _gameLoaderFuture;
|
||||||
|
|
||||||
Future<void> get _gameLoaderFutureCache =>
|
Future<void> get _gameLoaderFutureCache =>
|
||||||
_gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad());
|
_gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad());
|
||||||
|
|
||||||
@ -187,6 +200,14 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEventResult _handleKeyEvent(FocusNode focusNode, RawKeyEvent event) {
|
||||||
|
final game = widget.game;
|
||||||
|
if (game is KeyboardEvents) {
|
||||||
|
return game.onKeyEvent(event, RawKeyboard.instance.keysPressed);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget internalGameWidget = _GameRenderObjectWidget(widget.game);
|
Widget internalGameWidget = _GameRenderObjectWidget(widget.game);
|
||||||
@ -228,7 +249,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
// We can use Directionality.maybeOf when that method lands on stable
|
// We can use Directionality.maybeOf when that method lands on stable
|
||||||
final textDir = widget.textDirection ?? TextDirection.ltr;
|
final textDir = widget.textDirection ?? TextDirection.ltr;
|
||||||
|
|
||||||
return Directionality(
|
return Focus(
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
onKey: _handleKeyEvent,
|
||||||
|
child: Directionality(
|
||||||
textDirection: textDir,
|
textDirection: textDir,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: widget.game.backgroundColor(),
|
color: widget.game.backgroundColor(),
|
||||||
@ -239,10 +264,11 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
future: _gameLoaderFutureCache,
|
future: _gameLoaderFutureCache,
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
if (widget.errorBuilder == null) {
|
final errorBuilder = widget.errorBuilder;
|
||||||
|
if (errorBuilder == null) {
|
||||||
throw snapshot.error!;
|
throw snapshot.error!;
|
||||||
} else {
|
} else {
|
||||||
return widget.errorBuilder!(context, snapshot.error!);
|
return errorBuilder(context, snapshot.error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
@ -254,6 +280,7 @@ class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import '../../../components.dart';
|
||||||
|
import '../../../game.dart';
|
||||||
|
|
||||||
import '../game.dart';
|
/// A [BaseComponent] mixin to add keyboard handling capability to components.
|
||||||
|
/// Must be used in components that can only be added to games that are mixed
|
||||||
mixin KeyboardEvents on Game {
|
/// with [HasKeyboardHandlerComponents].
|
||||||
void onKeyEvent(RawKeyEvent event);
|
mixin KeyboardHandler on BaseComponent {
|
||||||
|
bool onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [BaseGame] mixin that implements [KeyboardEvents] with keyboard event
|
||||||
|
/// propagation to components that are mixed with [KeyboardHandler].
|
||||||
|
///
|
||||||
|
/// Attention: should not be used alongside [KeyboardEvents] in a game subclass.
|
||||||
|
/// Using this mixin remove the necessity of [KeyboardEvents].
|
||||||
|
mixin HasKeyboardHandlerComponents on BaseGame implements KeyboardEvents {
|
||||||
|
bool _handleKeyboardEvent(
|
||||||
|
bool Function(KeyboardHandler child) keyboardEventHandler,
|
||||||
|
) {
|
||||||
|
var shouldContinue = true;
|
||||||
|
for (final c in components.toList().reversed) {
|
||||||
|
if (c is BaseComponent) {
|
||||||
|
shouldContinue = c.propagateToChildren<KeyboardHandler>(
|
||||||
|
keyboardEventHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (c is KeyboardHandler && shouldContinue) {
|
||||||
|
shouldContinue = keyboardEventHandler(c);
|
||||||
|
}
|
||||||
|
if (!shouldContinue) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
KeyEventResult onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
final blockedPropagation = !_handleKeyboardEvent(
|
||||||
|
(KeyboardHandler child) => child.onKeyEvent(event, keysPressed),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If any component received the event, return handled,
|
||||||
|
// otherwise, ignore it.
|
||||||
|
if (blockedPropagation) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [Game] mixin to make a game subclass sensitive to keyboard events.
|
||||||
|
///
|
||||||
|
/// Override [onKeyEvent] to customize the keyboard handling behavior.
|
||||||
|
mixin KeyboardEvents on Game {
|
||||||
|
KeyEventResult onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
this is HasKeyboardHandlerComponents,
|
||||||
|
'A keyboard event was registered by KeyboardEvents for a game also '
|
||||||
|
'mixed with HasKeyboardHandlerComponents. Do not mix with both, '
|
||||||
|
'HasKeyboardHandlerComponents removes the necessity of KeyboardEvents',
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import 'package:flame/geometry.dart';
|
|
||||||
import 'package:flame/extensions.dart';
|
import 'package:flame/extensions.dart';
|
||||||
|
import 'package:flame/geometry.dart';
|
||||||
import 'package:flame/geometry.dart' as geometry;
|
import 'package:flame/geometry.dart' as geometry;
|
||||||
import 'package:flame/src/geometry/circle.dart';
|
import 'package:flame/src/geometry/circle.dart';
|
||||||
import 'package:flame/src/geometry/line_segment.dart';
|
|
||||||
import 'package:flame/src/geometry/line.dart';
|
import 'package:flame/src/geometry/line.dart';
|
||||||
|
import 'package:flame/src/geometry/line_segment.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@ -0,0 +1,153 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
Vector2 size = Vector2(1.0, 1.0);
|
||||||
|
|
||||||
|
class _KeyboardEventsGame extends Game with KeyboardEvents {
|
||||||
|
final List<String> keysPressed = [];
|
||||||
|
|
||||||
|
_KeyboardEventsGame();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
KeyEventResult onKeyEvent(
|
||||||
|
RawKeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
this.keysPressed.add(event.character ?? 'none');
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeyboardHandlerComponent extends BaseComponent with KeyboardHandler {
|
||||||
|
final List<String> keysPressed = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
||||||
|
this.keysPressed.add(event.character ?? 'none');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HasKeyboardHandlerComponentsGame extends BaseGame
|
||||||
|
with HasKeyboardHandlerComponents {
|
||||||
|
_HasKeyboardHandlerComponentsGame();
|
||||||
|
|
||||||
|
late _KeyboardHandlerComponent keyboardHandler;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
keyboardHandler = _KeyboardHandlerComponent();
|
||||||
|
add(keyboardHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GamePage extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _GamePage({Key? key, required this.child}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: ElevatedButton(
|
||||||
|
child: const Text('Back'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
testWidgets('Adds focus', (tester) async {
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
final game = _KeyboardEventsGame();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_GamePage(
|
||||||
|
child: GameWidget(
|
||||||
|
game: game,
|
||||||
|
focusNode: focusNode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(focusNode.hasFocus, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KeyboardEvents receives keys', (tester) async {
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
final game = _KeyboardEventsGame();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_GamePage(
|
||||||
|
child: GameWidget(
|
||||||
|
game: game,
|
||||||
|
focusNode: focusNode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
||||||
|
|
||||||
|
expect(game.keysPressed, ['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('HasKeyboardHandlerComponents receives keys', (tester) async {
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
final game = _HasKeyboardHandlerComponentsGame();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_GamePage(
|
||||||
|
child: GameWidget(
|
||||||
|
game: game,
|
||||||
|
focusNode: focusNode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.onResize(size);
|
||||||
|
game.update(0.1);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyZ);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyF);
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyI);
|
||||||
|
|
||||||
|
expect(game.keyboardHandler.keysPressed, ['z', 'f', 'i']);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flame/extensions.dart';
|
||||||
import 'package:flame/image_composition.dart';
|
import 'package:flame/image_composition.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:flame/extensions.dart';
|
|
||||||
|
|
||||||
class MockImage extends Mock implements Image {}
|
class MockImage extends Mock implements Image {}
|
||||||
|
|
||||||
|
|||||||
@ -162,7 +162,7 @@ Finally, we just render it on the game `render` function:
|
|||||||
```dart
|
```dart
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
// Running robot render omited
|
// Running robot render omitted
|
||||||
|
|
||||||
final button = isPressed ? pressedButton : unpressedButton;
|
final button = isPressed ? pressedButton : unpressedButton;
|
||||||
button.render(canvas, position: buttonPosition, size: buttonSize);
|
button.render(canvas, position: buttonPosition, size: buttonSize);
|
||||||
@ -173,7 +173,8 @@ You now should see the button on the screen, but right now, it is pretty much us
|
|||||||
|
|
||||||
So, to change that, we will now add some interactivity to our game and make the button tappable/clickable.
|
So, to change that, we will now add some interactivity to our game and make the button tappable/clickable.
|
||||||
|
|
||||||
Flame provides several input handlers, which you can check with more in depth on [our docs](https://github.com/flame-engine/flame/blob/main/doc/input.md). For this tutorial, we will be using the `TapDetector` which enables us to detect taps on the screen, as well as mouse click when running on web or desktop.
|
Flame provides several input handlers, about which you can check with more in depth on [our docs](https://github.com/flame-engine/flame/blob/main/doc/gesture-input.md).
|
||||||
|
For this tutorial, we will be using the `TapDetector` which enables us to detect taps on the screen, as well as mouse click when running on web or desktop.
|
||||||
|
|
||||||
All Flame input detectors are mixins which can be added to your game, enabling you to override listener methods related to that detector. For the `TapDetector`, we will need to override three methods:
|
All Flame input detectors are mixins which can be added to your game, enabling you to override listener methods related to that detector. For the `TapDetector`, we will need to override three methods:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user