ruststructidioms

Efficient yet ergonomic way to represent a gamepad state struct in Rust


I’m working on a Rust project involving gamepad input, using the gilrs library (game input library). Currently, I represent the state of my controller with the following struct:

#[derive(Default, Clone)]
pub struct GamepadState {
    axes: HashMap<gilrs::Axis, f32>,
    buttons: HashMap<gilrs::Button, bool>
}

This design allows me to easily retrieve the state of axes and buttons using methods like these:

impl GamepadState {
    pub fn axis(&self, axis: gilrs::Axis) -> f32 {
        self.axes.get(&axis).copied().unwrap_or(0_f32)
    }
    pub fn is_pressed(&self, button: gilrs::Button) -> bool {
        self.buttons.get(&button).copied().unwrap_or(false)
    }
}

Which would allow for code like this when handling events from gilrs:

match event {
    gilrs::EventType::AxisChanged(c_axis, value, _) => {
        gamepad_state.axes.insert(c_axis, value);
    }
    gilrs::EventType::ButtonPressed(c_button, _) => {
        gamepad_state.buttons.insert(c_button, true);
    }
    gilrs::EventType::ButtonReleased(c_button, _) => {
        gamepad_state.buttons.insert(c_button, false);
    }
    _ => {}
}

I chose HashMap to make the implementation flexible and concise, but it’s not a zero-cost abstraction. I want to optimize this design for better performance while maintaining a clean and ergonomic API for accessing the state.

If I were to replace HashMap, it would look something like this:

match event {
    gilrs::EventType::AxisChanged(c_axis, value, _) => {
    use gilrs::Axis;        
        match c_axis {
            Axis::LeftStickX => joystick_state.left_x = value,
            Axis::LeftStickY => joystick_state.left_y = value,
            Axis::RightStickX => joystick_state.right_x = value,
            Axis::RightStickY => joystick_state.right_y = value,
        }
    },
    // similar handling for buttons, which is worse because triggers and d-pad actions are also considered buttons in `gilrs`
}

But this approach feels clunky and less scalable.

My Question:

  1. How can I redesign this GamepadState struct to avoid HashMap while maintaining or improving performance?
  2. Is there a better way to manage gamepad states with fixed axes and buttons in Rust that doesn’t sacrifice ergonomics?

Any advice on idiomatic, efficient, and scalable solutions would be greatly appreciated!


Solution

  • The fastest possible way will be to use an array - which you can index directly with the value of the enums Axis and Button. However note that while these enums do have successive values, they are not guaranteed to have that. Luckily, the worst that can happen is a panic if someone bundles your app with an incorrect version of the library.

    const AXES_COUNT: usize = 9;
    const BUTTONS_COUNT: usize = 20;
    
    #[derive(Default, Clone)]
    pub struct GamepadState {
        axes: [f32; AXES_COUNT],
        buttons: [bool; BUTTONS_COUNT],
    }
    
    impl GamepadState {
        pub fn new() -> Self {
            Self {
                axes: [0.0; AXES_COUNT],
                buttons: [false; BUTTONS_COUNT],
            }
        }
    
        pub fn axis(&self, axis: gilrs::Axis) -> f32 {
            self.axes[axis as usize]
        }
    
        pub fn is_pressed(&self, button: gilrs::Button) -> bool {
            self.buttons[button as usize]
        }
    }
    

    If you don't miss any variant, this should compile to the most performant possible machine code (which is an offset by a number and read/write).

    For the buttons, you might consider a bitmap. This might be faster due to better cache access if you have many instances of GamepadState (which you likely not).