Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Gamepad Support

Gamepad input is provided by the gilrs crate. gilrs handles the platform differences across Windows, macOS, Linux, and the web, and Nightshade wraps the live state on world.resources.input.gamepad.

Enabling Gamepad

Gamepad is gated behind the gamepad cargo feature. Add it to the engine dependency.

[dependencies]
nightshade = { git = "...", features = ["engine", "gamepad"] }

Gamepad Resource

The resource wraps the gilrs library and tracks which gamepad is currently active.

#![allow(unused)]
fn main() {
pub struct Gamepad {
    pub gilrs: Option<gilrs::Gilrs>,
    pub gamepad: Option<gilrs::GamepadId>,
    pub events: Vec<gilrs::Event>,
}
}

The engine initializes gilrs at startup and updates the active gamepad as controllers are connected or disconnected.

Polling the Active Gamepad

query_active_gamepad returns a gilrs::Gamepad handle for the currently active controller. The handle exposes value for axes and triggers and is_pressed for buttons.

#![allow(unused)]
fn main() {
use nightshade::ecs::input::queries::query_active_gamepad;

fn run_systems(&mut self, world: &mut World) {
    if let Some(gamepad) = query_active_gamepad(world) {
        let left_x = gamepad.value(gilrs::Axis::LeftStickX);
        let left_y = gamepad.value(gilrs::Axis::LeftStickY);

        if gamepad.is_pressed(gilrs::Button::South) {
            self.jump();
        }
    }
}
}

Buttons

#![allow(unused)]
fn main() {
use nightshade::ecs::input::queries::query_active_gamepad;

fn run_systems(&mut self, world: &mut World) {
    if let Some(gamepad) = query_active_gamepad(world) {
        if gamepad.is_pressed(gilrs::Button::South) {
            jump();
        }

        if gamepad.is_pressed(gilrs::Button::West) {
            attack();
        }
    }
}
}

Button Mapping

gilrs uses position names (South, East, West, North) so the same code works across controller layouts. The face buttons map to different labels on each platform's controller, summarized below.

gilrs::ButtonXboxPlayStationNintendo
SouthACrossB
EastBCircleA
WestXSquareY
NorthYTriangleX
LeftTriggerLBL1L
RightTriggerRBR1R
SelectViewShare-
StartMenuOptions+
DPadUp/Down/Left/RightD-PadD-PadD-Pad

Analog Sticks

Axis values are f32 in the range [-1.0, 1.0]. Center is zero. LeftStickX is positive right, LeftStickY is positive up.

#![allow(unused)]
fn main() {
if let Some(gamepad) = query_active_gamepad(world) {
    let move_x = gamepad.value(gilrs::Axis::LeftStickX);
    let move_y = gamepad.value(gilrs::Axis::LeftStickY);

    let look_x = gamepad.value(gilrs::Axis::RightStickX);
    let look_y = gamepad.value(gilrs::Axis::RightStickY);
}
}

A deadzone of 0.15 to 0.20 is standard. Below that, treat the stick as centered. Cheap controllers drift, and reading raw values means the camera slowly rotates while the player is not touching the stick.

Triggers

Triggers are axes, not buttons. They report [0.0, 1.0] for how far pulled. LeftZ and RightZ are the analog values. The LeftTrigger and RightTrigger buttons in the table above are the digital shoulder buttons on most controllers, not the analog triggers.

#![allow(unused)]
fn main() {
if let Some(gamepad) = query_active_gamepad(world) {
    let left = gamepad.value(gilrs::Axis::LeftZ);
    let right = gamepad.value(gilrs::Axis::RightZ);

    let acceleration = right * max_acceleration;
}
}

Event Handling

Gamepad events arrive as gilrs::Event values in the AppEvent queue. Match on the event type for transition-based logic.

#![allow(unused)]
fn main() {
fn on_gamepad_event(&mut self, world: &mut World, event: gilrs::Event) {
    if let gilrs::EventType::ButtonPressed(button, _) = event.event {
        match button {
            gilrs::Button::Start => self.paused = !self.paused,
            gilrs::Button::South => self.player_jump(),
            _ => {}
        }
    }
}
}

Combining Keyboard and Gamepad

Player input often comes from whichever device is being held. The pattern is to read both, prefer the gamepad when its sticks are out of the deadzone, and fall back to the keyboard otherwise. Action buttons OR across devices so either input fires the action.

#![allow(unused)]
fn main() {
use nightshade::ecs::input::queries::query_active_gamepad;

struct PlayerInput {
    movement: Vec2,
    jump: bool,
    attack: bool,
}

fn gather_input(world: &mut World) -> PlayerInput {
    let mut input = PlayerInput {
        movement: Vec2::zeros(),
        jump: false,
        attack: false,
    };

    let keyboard = &world.resources.input.keyboard;
    if keyboard.is_key_pressed(KeyCode::KeyW) { input.movement.y -= 1.0; }
    if keyboard.is_key_pressed(KeyCode::KeyS) { input.movement.y += 1.0; }
    if keyboard.is_key_pressed(KeyCode::KeyA) { input.movement.x -= 1.0; }
    if keyboard.is_key_pressed(KeyCode::KeyD) { input.movement.x += 1.0; }
    input.jump |= keyboard.is_key_pressed(KeyCode::Space);

    if let Some(gamepad) = query_active_gamepad(world) {
        let stick_x = gamepad.value(gilrs::Axis::LeftStickX);
        let stick_y = gamepad.value(gilrs::Axis::LeftStickY);

        if stick_x.abs() > 0.15 || stick_y.abs() > 0.15 {
            input.movement = Vec2::new(stick_x, stick_y);
        }

        input.jump |= gamepad.is_pressed(gilrs::Button::South);
        input.attack |= gamepad.is_pressed(gilrs::Button::West);
    }

    input
}
}