Log in to access Rust Adventure videos!

Lesson Details

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. This new system will be 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(https://docs.rs/bevy/0.10.0/bevy/input/prelude/struct.Input.html#method.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. Since this system can run at the same time as render_tile_points, we can change our add_system to add_systems and use a tuple

.add_systems((render_tile_points, board_shift))

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.

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 =
    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