mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-31 00:48:47 +08:00
330 lines
9.1 KiB
Markdown
330 lines
9.1 KiB
Markdown
# 5. Controlling Movement
|
|
|
|
If you were waiting for some serious coding, this chapter is it. Prepare yourself as we dive in!
|
|
|
|
|
|
## Keyboard Controls
|
|
|
|
The first step will be to allow control of Ember via the keyboard. We need to start by adding the
|
|
appropriate mixins to the game class and Ember. Add the following:
|
|
|
|
`lib/ember_quest.dart'
|
|
|
|
```dart
|
|
import 'package:flame/events.dart';
|
|
|
|
class EmberQuestGame extends FlameGame with HasKeyboardHandlerComponents {
|
|
```
|
|
|
|
`lib/actors/ember.dart`
|
|
|
|
```dart
|
|
class EmberPlayer extends SpriteAnimationComponent
|
|
with KeyboardHandler, HasGameRef<EmberQuestGame> {
|
|
```
|
|
|
|
Now we can add a new method:
|
|
|
|
```dart
|
|
@override
|
|
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
|
return true;
|
|
}
|
|
```
|
|
|
|
Like before, if this did not trigger an auto-import, you will need the following:
|
|
|
|
```dart
|
|
import 'package:flutter/services.dart';
|
|
```
|
|
|
|
To control Ember's movement, it is easiest to set a variable where we think of the direction of
|
|
movement like a normalized vector, meaning the value will be restricted to -1, 0, or 1. So let's
|
|
set a variable at the top of the class:
|
|
|
|
```dart
|
|
int horizontalDirection = 0;
|
|
```
|
|
|
|
Now in our `onKeyEvent` method, we can register the key pressed by adding:
|
|
|
|
```dart
|
|
@override
|
|
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
|
horizontalDirection = 0;
|
|
horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) ||
|
|
keysPressed.contains(LogicalKeyboardKey.arrowLeft))
|
|
? -1
|
|
: 0;
|
|
horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) ||
|
|
keysPressed.contains(LogicalKeyboardKey.arrowRight))
|
|
? 1
|
|
: 0;
|
|
|
|
return true;
|
|
}
|
|
```
|
|
|
|
Let's make Ember move by adding a few lines of code and creating our `update` method. First, we
|
|
need to define a velocity variable for Ember. Add the following at the top of the `EmberPlayer`
|
|
class:
|
|
|
|
```dart
|
|
final Vector2 velocity = Vector2.zero();
|
|
final double moveSpeed = 200;
|
|
```
|
|
|
|
This establishes a base velocity of 0 and stores `moveSpeed` so we can adjust as necessary to suit
|
|
how the game-play should be. Next, add the `update` method with the following:
|
|
|
|
|
|
```dart
|
|
@override
|
|
void update(double dt) {
|
|
velocity.x = horizontalDirection * moveSpeed;
|
|
position += velocity * dt;
|
|
super.update(dt);
|
|
}
|
|
```
|
|
|
|
If you run the game now, Ember moves left and right using the arrow keys or the `A` and `D` keys.
|
|
You may have noticed that Ember doesn't look back if you are going left, to fix that, add the
|
|
following code at the end of your `update` method:
|
|
|
|
```dart
|
|
if (horizontalDirection < 0 && scale.x > 0) {
|
|
flipHorizontally();
|
|
} else if (horizontalDirection > 0 && scale.x < 0) {
|
|
flipHorizontally();
|
|
}
|
|
```
|
|
|
|
Now Ember looks in the direction they are traveling.
|
|
|
|
|
|
## Collisions
|
|
|
|
It is time to get into the thick of it with collisions. I highly suggest reading the
|
|
[documentation](../../flame/collision_detection.md) to understand how collisions work in Flame. The
|
|
first thing we need to do is make the game aware that collisions are going to occur using the
|
|
`HasCollisionDetection` mixin. Add that to `lib/ember_quest.dart` like:
|
|
|
|
```dart
|
|
class EmberQuestGame extends FlameGame
|
|
with HasCollisionDetection, HasKeyboardHandlerComponents {
|
|
```
|
|
|
|
Next, add the `CollisionCallbacks` mixin to `lib/actors/ember.dart` like:
|
|
|
|
```dart
|
|
class EmberPlayer extends SpriteAnimationComponent
|
|
with KeyboardHandler, CollisionCallbacks, HasGameRef<EmberQuestGame> {
|
|
```
|
|
|
|
If it did not auto-import, you will need the following:
|
|
|
|
```dart
|
|
import 'package:flame/collisions.dart';
|
|
```
|
|
|
|
Now add the following `onCollision` method:
|
|
|
|
```dart
|
|
@override
|
|
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
|
|
if (other is GroundBlock || other is PlatformBlock) {
|
|
if (intersectionPoints.length == 2) {
|
|
// Calculate the collision normal and separation distance.
|
|
final mid = (intersectionPoints.elementAt(0) +
|
|
intersectionPoints.elementAt(1)) / 2;
|
|
|
|
final collisionNormal = absoluteCenter - mid;
|
|
final separationDistance = (size.x / 2) - collisionNormal.length;
|
|
collisionNormal.normalize();
|
|
|
|
// If collision normal is almost upwards,
|
|
// ember must be on ground.
|
|
if (fromAbove.dot(collisionNormal) > 0.9) {
|
|
isOnGround = true;
|
|
}
|
|
|
|
// Resolve collision by moving ember along
|
|
// collision normal by separation distance.
|
|
position += collisionNormal.scaled(separationDistance);
|
|
}
|
|
}
|
|
|
|
super.onCollision(intersectionPoints, other);
|
|
}
|
|
```
|
|
|
|
You will need to import the following:
|
|
|
|
```dart
|
|
import '../objects/ground_block.dart';
|
|
import '../objects/platform_block.dart';
|
|
```
|
|
|
|
As well as create these class variables:
|
|
|
|
```dart
|
|
final Vector2 fromAbove = Vector2(0, -1);
|
|
bool isOnGround = false;
|
|
```
|
|
|
|
For the collisions to be activated for Ember, we need to add a `CircleHitbox`, so in the `onLoad`
|
|
method, add the following:
|
|
|
|
```dart
|
|
add(
|
|
CircleHitbox(),
|
|
);
|
|
```
|
|
|
|
Now that we have the basic collisions created, we can add gravity so Ember exists in a game world
|
|
with very basic physics. To do that, we need to create some more variables:
|
|
|
|
```dart
|
|
final double gravity = 15;
|
|
final double jumpSpeed = 600;
|
|
final double terminalVelocity = 150;
|
|
|
|
bool hasJumped = false;
|
|
```
|
|
|
|
Now we can add Ember's ability to jump by adding the following to our `onKeyEvent` method:
|
|
|
|
```dart
|
|
hasJumped = keysPressed.contains(LogicalKeyboardKey.space);
|
|
```
|
|
|
|
Finally, in our `update` method we can tie this all together with:
|
|
|
|
```dart
|
|
// Apply basic gravity
|
|
velocity.y += gravity;
|
|
|
|
// Determine if ember has jumped
|
|
if (hasJumped) {
|
|
if (isOnGround) {
|
|
velocity.y = -jumpSpeed;
|
|
isOnGround = false;
|
|
}
|
|
hasJumped = false;
|
|
}
|
|
|
|
// Prevent ember from jumping to crazy fast as well as descending too fast and
|
|
// crashing through the ground or a platform.
|
|
velocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);
|
|
```
|
|
|
|
Earlier I mentioned that Ember was in the center of the grass, to solve this and show how collisions
|
|
and gravity work with Ember, I like to add a little drop-in when you start the game. So in
|
|
`lib/ember_quest.dart` in the `initializeGame` method, change the following:
|
|
|
|
```dart
|
|
_ember = EmberPlayer(
|
|
position: Vector2(128, canvasSize.y - 128),
|
|
);
|
|
```
|
|
|
|
If you run the game now, Ember should be created and fall to the ground; then you can jump around!
|
|
|
|
|
|
### Collisions with Objects
|
|
|
|
Adding the collisions with the other objects is fairly trivial. All we need to do is add the
|
|
following to the bottom of the `onCollision` method:
|
|
|
|
```dart
|
|
if (other is Star) {
|
|
other.removeFromParent();
|
|
}
|
|
|
|
if (other is WaterEnemy) {
|
|
hit();
|
|
}
|
|
```
|
|
|
|
When Ember collides with a star, the game will remove the star, and to implement the `hit` method for
|
|
when Ember collides with an enemy, we need to do the following:
|
|
|
|
Add the following variable at the top of the `EmberPlayer` class:
|
|
|
|
```dart
|
|
bool hitByEnemy = false;
|
|
```
|
|
|
|
Additionally, add this method to the `EmberPlayer` class:
|
|
|
|
```dart
|
|
// This method runs an opacity effect on ember
|
|
// to make it blink.
|
|
void hit() {
|
|
if (!hitByEnemy) {
|
|
hitByEnemy = true;
|
|
}
|
|
add(
|
|
OpacityEffect.fadeOut(
|
|
EffectController(
|
|
alternate: true,
|
|
duration: 0.1,
|
|
repeatCount: 6,
|
|
),
|
|
)..onComplete = () {
|
|
hitByEnemy = false;
|
|
},
|
|
);
|
|
}
|
|
```
|
|
|
|
If the auto-imports did not occur, you will need to add the following imports to your file:
|
|
|
|
```dart
|
|
import 'package:flame/effects.dart';
|
|
|
|
import '../objects/star.dart';
|
|
import 'water_enemy.dart';
|
|
```
|
|
|
|
If you run the game now, you should be able to move around, make stars disappear, and if you
|
|
collide with an enemy, Ember should blink.
|
|
|
|
|
|
## Adding the Scrolling
|
|
|
|
This is our last task with Ember. We need to restrict Ember's movement because as of now, Ember can
|
|
go off-screen and we never move the map. So to implement this feature, we simply need to add the
|
|
following to our `update` method:
|
|
|
|
```dart
|
|
game.objectSpeed = 0;
|
|
// Prevent ember from going backwards at screen edge.
|
|
if (position.x - 36 <= 0 && horizontalDirection < 0) {
|
|
velocity.x = 0;
|
|
}
|
|
// Prevent ember from going beyond half screen.
|
|
if (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {
|
|
velocity.x = 0;
|
|
game.objectSpeed = -moveSpeed;
|
|
}
|
|
```
|
|
|
|
If you run the game now, Ember can't move off-screen to the left, and as Ember moves to the right,
|
|
once they get to the middle of the screen, the rest of the objects scroll by. This is because we
|
|
are now updating `game.objectSpeed` which we established early on in the series. Additionally,
|
|
you will see the next random segment be generated and added to the level based on the work we did in
|
|
Ground Block.
|
|
|
|
```{note}
|
|
As I mentioned earlier, I would add a section on how this game could be adapted
|
|
to a traditional level game. As we built the segments in [](step_3.md), we
|
|
could add a segment that has a door or a special block. For every `X` number of
|
|
segments loaded, we could then add that special segment. When Ember reaches that
|
|
object, we could reload the level and start all over maintaining the stars
|
|
collected and health.
|
|
```
|
|
|
|
We are almost done! In [](step_6.md), we will add the health system, keep track of
|
|
the score, and provide a HUD to relay that information to the player.
|