Fix the Movement Controls
Our movement system broke some of the cardinal rules of making a game. First and foremost, it did nothing to normalize movement on the diagonals. Letting the players cover twice as much ground moving diagonally is a classic early game dev mistake. This is easily solvable with glam, the library we use for our linear algebra, but will require a change to our input logic.
For a refresher here is our current code:
#[system]
fn move_player(mut player: Query<(Mut<Transform>, Ref<Player>)>, input: Ref<InputState>) {
player.for_each(|(mut transform, _)| {
if input.keys[KeyCode::KeyW].pressed() {
transform.position.y += 5.;
}
if input.keys[KeyCode::KeyS].pressed() {
transform.position.y -= 5.;
}
if input.keys[KeyCode::KeyD].pressed() {
transform.position.x += 5.;
}
if input.keys[KeyCode::KeyA].pressed() {
transform.position.x -= 5.;
}
});
}
We're actually concerned more about the individual axes of movement, vertical and horizontal, and can represent that as a Vec2. That then allows us to normalize the magnitude of the vector, preventing the player from getting extra movement by going diagonally. Rather than directly translate "is this button pressed this frame?" to "move in that direction," we want to abstract this a level so that our movement and input aren't as tightly coupled.
There's several ways to do this, but we're going for an easy and fairly comprehensive solution: add a PlayerInput resource to capture the input data the way we want to use it.
Let's first define this resource...
#[derive(Default)]
#[resource]
struct PlayerInput {
move_axes: Vec2,
}
We'll expand on it later, but that's enough for now. Now we need a system to convert the InputState raw input data into our new resource. We should put this first in our order of systems, so that we can have up to date input data at the start of every frame.
#[system]
fn update_player_input(mut player_input: Mut<PlayerInput>, input: Ref<InputState>) {
let key_to_f32 = |key_code| {
if input.keys[key_code].pressed() {
1.0
} else {
0.0
}
};
player_input.move_axes = Vec2::new(
key_to_f32(KeyCode::KeyD) - key_to_f32(KeyCode::KeyA),
key_to_f32(KeyCode::KeyW) - key_to_f32(KeyCode::KeyS),
)
.clamp_length_max(1.);
}
Because checking if a key_code is pressed returns a bool and Rust can't automatically turn that into an f32, we need to convert it instead. Rather than create a whole new util.rs file or something else, we'll just keep it inline for now. There's still one major flaw here: hardcoded input bindings. For now, we're going to accept it. But in a future chapter we'll address it.
We need to update our move_player function to use the new PlayerInput resource.
#[system]
fn move_player(mut player: Query<(Mut<Transform>, Ref<Player>)>, input: Ref<PlayerInput>) {
player.for_each(|(mut transform, _)| {
transform.position += input.move_axes;
});
}
If we were to run this now though, our player will be much slower than before. We could simply multiply by our (arbitrary) old value of 5.0. However, if there is any variance in the amount of time each frame takes to process, the player will move at an uneven pace. We need to introduce another built-in resource, FrameConstants, to get the amount of time that has passed between frames. Because the amount of time is so tiny, our player will barely move at all. So at the same time we're going to add a const to the top of the file called PLAYER_MOVE_SPEED to make it easy to adjust in the future.
const PLAYER_MOVE_SPEED = 250.;
// ...
#[system]
fn move_player(
mut player: Query<(Mut<Transform>, Ref<Player>)>,
input: Ref<PlayerInput>,
frame_constants: Ref<FrameConstants>,
) {
player.for_each(|(mut transform, _)| {
transform.position += input.move_axes * PLAYER_MOVE_SPEED * frame_constants.delta_time;
});
}
I've also reduced the scaling of the player and enemy to use sizes closer to our final plan for the game. So with lib.rs looking like the following, we finally have player movement that feels okay.
use engine::prelude::*;
mod ffi;
const PLAYER_MOVE_SPEED: f32 = 250.;
#[derive(Default)]
#[resource]
struct PlayerInput {
move_axes: Vec2,
}
#[component]
struct Player;
#[system]
fn update_player_input(mut player_input: Mut<PlayerInput>, input: Ref<InputState>) {
let key_to_f32 = |key_code| {
if input.keys[key_code].pressed() {
1.0
} else {
0.0
}
};
player_input.move_axes = Vec2::new(
key_to_f32(KeyCode::KeyD) - key_to_f32(KeyCode::KeyA),
key_to_f32(KeyCode::KeyW) - key_to_f32(KeyCode::KeyS),
)
.clamp_length_max(1.);
}
#[system_once]
fn spawn_players() {
let transform = &Transform {
position: Vec2::new(110., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.96, 0.65, 0.14, 0.9);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
Engine::spawn(bundle!(transform, color, circle_render, &Player));
}
#[system_once]
fn spawn_enemy() {
let transform = &Transform {
position: Vec2::new(-110., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.2, 0.2, 0.9, 0.9);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
Engine::spawn(bundle!(transform, color, circle_render));
}
#[system]
fn move_player(
mut player: Query<(Mut<Transform>, Ref<Player>)>,
input: Ref<PlayerInput>,
frame_constants: Ref<FrameConstants>,
) {
player.for_each(|(mut transform, _)| {
transform.position += input.move_axes * PLAYER_MOVE_SPEED * frame_constants.delta_time;
});
}
Of course, I don't want to settle for movement that just feels "okay." I want the movement to feel great, which we'll need acceleration and velocity to accomplish.