Keyboard Input
In the previous example, the keyboard input acts as though the player
holds down any key he presses. This is because HTML/Javascript doesn't
give us a way to query if a key is currently held down - it only gives
is keydown
and keyup
events. Back in
binding_events we just smashed all
the key events into one. It's time to break those into separate function
calls and to maintain state for the keys we are interested in.
Assing the extra binding is a case of modifying the Core
struct to
separate the bindings:
{
// keyboard events
self.canvas.set_tab_index(1); // Canvas elements ignore key events unless they have a tab index
let anim_app1 = self.app.clone();
let anim_app2 = self.app.clone();
let keydown_callback = Closure::wrap(Box::new(move |event: KeyboardEvent| {
let e: Event = event.clone().dyn_into().unwrap();
e.stop_propagation();
e.prevent_default();
anim_app1.borrow_mut().keydown_event(event);
}) as Box<dyn FnMut(_)>);
let keyup_callback = Closure::wrap(Box::new(move |event: KeyboardEvent| {
let e: Event = event.clone().dyn_into().unwrap();
e.stop_propagation();
e.prevent_default();
anim_app2.borrow_mut().keyup_event(event);
}) as Box<dyn FnMut(_)>);
self.canvas
.add_event_listener_with_callback("keydown", keydown_callback.as_ref().unchecked_ref())
.unwrap();
self.canvas
.add_event_listener_with_callback("keyup", keyup_callback.as_ref().unchecked_ref())
.unwrap();
keydown_callback.forget();
keyup_callback.forget();
}
And creating the extra function in our App struct:
pub fn keydown_event(&mut self, event: KeyboardEvent) {
// Do something
}
pub fn keyup_event(&mut self, event: KeyboardEvent) {
// Do something else
}
Now we need to mantain the state. Let's create an enum to represent the state of the keys and how it transitions between states.
impl KeyState {
fn update(&self) -> KeyState {
match self {
KeyState::JustPressed => KeyState::Down,
KeyState::Down => KeyState::Down,
KeyState::JustReleased => KeyState::Up,
KeyState::Up => KeyState::Up,
}
}
fn active(&self) -> bool {
match self {
KeyState::JustPressed => true,
KeyState::Down => true,
KeyState::JustReleased => false,
KeyState::Up => false,
}
}
}
So the idea is that the JS events set the KeyState
into
JustPressed
or JustReleased
, and then on the subsequent frames
it is in the state Down
or Up
. Code can either query the edge event
by looking at the value of the KeyState directly, or can use the "active"
function to determine if the key is in a "downy" state.
And now create a struct to store the state for each key we're interested in:
struct KeyMap {
forwards: KeyState,
backwards: KeyState,
turn_left: KeyState,
turn_right: KeyState,
}
impl KeyMap {
fn new() -> Self {
Self {
forwards: KeyState::Up,
backwards: KeyState::Up,
turn_left: KeyState::Up,
turn_right: KeyState::Up,
}
}
fn update(&mut self) {
self.forwards = self.forwards.update();
self.backwards = self.backwards.update();
self.turn_left = self.turn_left.update();
self.turn_right = self.turn_right.update();
}
fn set_state_from_str(&mut self, code: &str, new_state: KeyState) {
match code {
"KeyW" => {self.forwards = new_state},
"KeyS" => {self.backwards = new_state},
"KeyA" => {self.turn_left = new_state},
"KeyD" => {self.turn_right = new_state},
_ => ()
};
}
}
On the keyup
and keydown
events the function set_state_from_str
will
be called, and on every action frame, update
will be called.
One final thing and that is that the keydown
event continues to fire when
held down, so the contents of our keydown and keyup functions should be:
pub fn keydown_event(&mut self, event: KeyboardEvent) {
if !event.repeat() {
self.key_map.set_state_from_str(&event.code(), KeyState::JustPressed);
}
}
pub fn keyup_event(&mut self, event: KeyboardEvent) {
self.key_map.set_state_from_str(&event.code(), KeyState::JustReleased);
}
Now we can map the state of the key_map
to the player in our animation frame
callback:
let player_ship = &mut self.ship_entities[0];
player_ship.linear_thrust = 0.0;
player_ship.angular_thrust = 0.0;
if self.key_map.forwards.active() {
player_ship.linear_thrust += 1.0
}
if self.key_map.backwards.active() {
player_ship.linear_thrust -= 1.0
}
if self.key_map.turn_left.active() {
player_ship.angular_thrust += 1.0
}
if self.key_map.turn_right.active() {
player_ship.angular_thrust -= 1.0
}
self.key_map.update();
And the result is:
Why implement it all this way? Why not convert the key string to an enum then use a hashmap to store key state, and make the KeyMap more generic? The same reason I didn't factor out generic "sprite drawing code" - I'm not trying to make a game engine here, and this is the simplest way to get the job done.