Tracking Input

Using the events we receive from Winit, we have a way of responding to events as they happen, but we don't necessarily know the state of a given key, the mouse, the window, etc. What we want is for that information to be tracked and made available to gamestates, so they can make informed decisions using up-to-date information.

Necessary Dependencies

First, let's add a math library to simplify some of the calculations.

This will be heavily used when we design our world crate!

cargo add anyhow -p nalgebra_glm

Then, export it in crates/phantom_dependencies/src/lib.rs:

pub use nalgebra_glm as glm;

Adding an Input Resource

Let's add a file at crates/phantom_app/src/resources/input.rs.

use phantom_dependencies::{
    glm,
    winit::{
        dpi::PhysicalPosition,
        event::{
            ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, VirtualKeyCode,
            WindowEvent,
        },
    },
};
use std::collections::HashMap;

pub type KeyMap = HashMap<VirtualKeyCode, ElementState>;

pub struct Input {
    pub keystates: KeyMap,
    pub mouse: Mouse,
    pub allowed: bool,
}

impl Default for Input {
    fn default() -> Self {
        Self {
            keystates: KeyMap::default(),
            mouse: Mouse::default(),
            allowed: true,
        }
    }
}

impl Input {
    pub fn is_key_pressed(&self, keycode: VirtualKeyCode) -> bool {
        self.keystates.contains_key(&keycode) && self.keystates[&keycode] == ElementState::Pressed
    }

    pub fn handle_event<T>(&mut self, event: &Event<T>, window_center: glm::Vec2) {
        if !self.allowed {
            return;
        }

        if let Event::WindowEvent {
            event:
                WindowEvent::KeyboardInput {
                    input:
                        KeyboardInput {
                            virtual_keycode: Some(keycode),
                            state,
                            ..
                        },
                    ..
                },
            ..
        } = *event
        {
            *self.keystates.entry(keycode).or_insert(state) = state;
        }

        self.mouse.handle_event(event, window_center);
    }
}

#[derive(Default)]
pub struct Mouse {
    pub is_left_clicked: bool,
    pub is_right_clicked: bool,
    pub position: glm::Vec2,
    pub position_delta: glm::Vec2,
    pub offset_from_center: glm::Vec2,
    pub wheel_delta: glm::Vec2,
    pub moved: bool,
    pub scrolled: bool,
}

impl Mouse {
    pub fn handle_event<T>(&mut self, event: &Event<T>, window_center: glm::Vec2) {
        match event {
            Event::NewEvents { .. } => self.new_events(),
            Event::WindowEvent { event, .. } => match *event {
                WindowEvent::MouseInput { button, state, .. } => self.mouse_input(button, state),
                WindowEvent::CursorMoved { position, .. } => {
                    self.cursor_moved(position, window_center)
                }
                WindowEvent::MouseWheel {
                    delta: MouseScrollDelta::LineDelta(h_lines, v_lines),
                    ..
                } => self.mouse_wheel(h_lines, v_lines),
                _ => {}
            },
            _ => {}
        }
    }

    fn new_events(&mut self) {
        if !self.scrolled {
            self.wheel_delta = glm::vec2(0.0, 0.0);
        }
        self.scrolled = false;

        if !self.moved {
            self.position_delta = glm::vec2(0.0, 0.0);
        }
        self.moved = false;
    }

    fn cursor_moved(&mut self, position: PhysicalPosition<f64>, window_center: glm::Vec2) {
        let last_position = self.position;
        let current_position = glm::vec2(position.x as _, position.y as _);
        self.position = current_position;
        self.position_delta = current_position - last_position;
        self.offset_from_center = window_center - glm::vec2(position.x as _, position.y as _);
        self.moved = true;
    }

    fn mouse_wheel(&mut self, h_lines: f32, v_lines: f32) {
        self.wheel_delta = glm::vec2(h_lines, v_lines);
        self.scrolled = true;
    }

    fn mouse_input(&mut self, button: MouseButton, state: ElementState) {
        let clicked = state == ElementState::Pressed;
        match button {
            MouseButton::Left => self.is_left_clicked = clicked,
            MouseButton::Right => self.is_right_clicked = clicked,
            _ => {}
        }
    }
}

This lets us easily track keystates and mouse information!

Adding a System Resource

Let's add a file at crates/phantom_app/src/resources/system.rs.

use phantom_dependencies::{
    glm,
    winit::{
        dpi::PhysicalSize,
        event::{Event, WindowEvent},
    },
};
use std::{cmp, time::Instant};

pub struct System {
    pub window_dimensions: [u32; 2],
    pub delta_time: f64,
    pub last_frame: Instant,
    pub exit_requested: bool,
}

impl System {
    pub fn new(window_dimensions: [u32; 2]) -> Self {
        Self {
            last_frame: Instant::now(),
            window_dimensions,
            delta_time: 0.01,
            exit_requested: false,
        }
    }

    pub fn aspect_ratio(&self) -> f32 {
        let width = self.window_dimensions[0];
        let height = cmp::max(self.window_dimensions[1], 0);
        width as f32 / height as f32
    }

    pub fn window_center(&self) -> glm::Vec2 {
        glm::vec2(
            self.window_dimensions[0] as f32 / 2.0,
            self.window_dimensions[1] as f32 / 2.0,
        )
    }

    pub fn handle_event<T>(&mut self, event: &Event<T>) {
        match event {
            Event::NewEvents { .. } => {
                self.delta_time = (Instant::now().duration_since(self.last_frame).as_micros()
                    as f64)
                    / 1_000_000_f64;
                self.last_frame = Instant::now();
            }
            Event::WindowEvent { event, .. } => match *event {
                WindowEvent::CloseRequested => self.exit_requested = true,
                WindowEvent::Resized(PhysicalSize { width, height }) => {
                    self.window_dimensions = [width, height];
                }
                _ => {}
            },
            _ => {}
        }
    }
}

Now, we won't have to pollute our main game loop just to keep track of window dimensions and frame latency.

Adding Resources

Now, we can modify our crates/phantom_app/src/resources.rs to include our new resources.

mod input;
mod system;

pub use self::{input::*, system::*};

use phantom_dependencies::winit::window::Window;

pub struct Resources<'a> {
    pub window: &'a mut Window,
    pub input: &'a mut Input,
    pub system: &'a mut System,
}

...

Instantiating Resources

Finally, we can instantiate our resources in our crates/phantom_app/src/app.rs.

use crate::{Input, Resources, State, StateMachine, System};

pub fn run(...) {
    ...

    let physical_size = window.inner_size();
    let window_dimensions = [physical_size.width, physical_size.height];

    let mut input = Input::default();
    let mut system = System::new(window_dimensions);

    ...

    event_loop.run(move |event, _, control_flow| {
       let resources = Resources {
            ...
            input: &mut input,
            system: &mut system,
        };  
        ...
    })
}
...