In 2048 the user presses arrow keys and all of the tiles shift in that direction, merging if possible. To represent this we'll write a new system to handle user input and translate it into tile movement called board_shift
.
Keyboard input in Bevy is represented as a resource, so we'll ask Bevy for Res<Input<KeyCode>>
. Input
has a few functions for grabbing which keys are being pressed. input.pressed
will match against key presses every game tick. This is useful for games where you have a character that is running and need to constantly apply movement.
We have a 2d board game, so we'll use just_pressed
instead, which will only match once when a key is pressed. We can apply this function up to four times to figure out which key is pressed.
fn board_shift(input: Res<Input<KeyCode>>) {
if input.just_pressed(KeyCode::Left) {
dbg!("left");
} else if input.just_pressed(KeyCode::Right) {
dbg!("right");
} else if input.just_pressed(KeyCode::Down) {
dbg!("down");
} else if input.just_pressed(KeyCode::Up) {
dbg!("up");
}
}
Adding this system to our App
will let you press keys and see what Bevy sees as it gets logged out.
.add_system(board_shift.system())
This code isn't particularly complicated and we can definitely continue with this if we want to. We can also make this a bit different for a few reasons.
There can be multiple keys pressed at any given time. The original if expression based code doesn't encode this, it only checks one key at a time, and if we come back to this code later we might not remember if that was important or not.
We also handle key directions, but don't translate that to board shift directions and we don't encode what happens if a valid key isn't chosen. That's left to implicitly doing nothing.
To change this up a bit, we can create a new enum
call BoardShift
to hold the various directions the board can shift in.
enum BoardShift {
Left,
Right,
Up,
Down,
}
To turn a KeyCode
into a BoardShift
we can implement the TryFrom
trait which is in the Rust standard library. We need to bring the trait into scope to be able to implement it (or use it).
use std::convert::TryFrom;
Then we can implement TryFrom
for a shared reference to a KeyCode
. We implement the trait on a shared reference because we don't need to mutate it, only read it.
To complete the implementation of the trait we have to provide a type for the error if something goes wrong and a function called try_from
to do the conversion. In this case we use a &'static string
for the Error
because we'll be writing a hardcoded string.
The try_from
function accepts a shared reference to a KeyCode
, which is the same type we're using in the impl type above. The return type is a Result
, because this operation may fail so we want to be able to return Ok()
or Err()
depending on what happens. Self
refers to the type we're implementing the trait for, BoardShift
while Self::Error
refers to the associated type Error
that we defined. An alternate way of writing this type signature would be Result<BoardShift, ^'static str>
.
The KeyCode
enum contains over 100 possible keys that can be pressed, while we only care about four of them. We can match on the ones we care about, while using _
to ignore all of the others.
The conversion is one-to-one in that KeyCode::Left
becomes BoardShift::Left
. We do have to wrap the successful values in Ok
and the error value in Err
to match the type signature.
impl TryFrom<&KeyCode> for BoardShift {
type Error = &'static str;
fn try_from(
value: &KeyCode,
) -> Result<Self, Self::Error> {
match value {
KeyCode::Left => Ok(BoardShift::Left),
KeyCode::Up => Ok(BoardShift::Up),
KeyCode::Right => Ok(BoardShift::Right),
KeyCode::Down => Ok(BoardShift::Down),
_ => Err("not a valid board_shift key"),
}
}
}
With this implementation, whenever we have a KeyCode
we can use BoardShift::try_from
to attempt to convert it to a board shift direction.
BoardShift::try_from(key_code)
In our board_shift
system then, we can access an iterator over all of the keys that are pressed. This makes it clear that multiple keys can be pressed, even though we're about to find the first match anyway.
.find_map
is an interesting function. Because Rust has the Option
type we can combine .find
and .map
into a single function, wrapping the values we want in Some
and replacing the values we don't with None
. More directly, we are going to find the first KeyCode
that can be converted into a BoardShift
and then change that value into the BoardShift
value, skipping all others.
let shift_direction =
keyboard_input.get_just_pressed().find_map(
|key_code| BoardShift::try_from(key_code).ok(),
);
.find_map
accepts a closure that gives us a single item from the iterator at a time. BoardShift::try_from
allows us to try to convert that item into a BoardShift
position. try_from
returns a Result
and find_map
wants an Option
, so we can discard the error (which we don't care about anyway) and convert the Result
into an Option
with .ok()
.
Once we've run find_map
we can match on it. We're getting back an Option<BoardShift>
, so when we match
we can match the entire structure. This allows us to be explicit about what happens if there isn't a valid BoardShift
.
match shift_direction {
Some(BoardShift::Left) => {
dbg!("left");
}
Some(BoardShift::Right) => {
dbg!("right");
}
Some(BoardShift::Up) => {
dbg!("up");
}
Some(BoardShift::Down) => {
dbg!("down");
}
None => (),
}
Running this code with cargo run
shows us the same output as before. The old if-based code works, and new code is more clear about the context of what's happening:
- multiple keys can be pressed
- Of the pressed keys, some may not correspond to board shift actions
- We choose to take the first key that corresponds to a board shift, but there could be more