bevy_archie 0.1.6

A comprehensive game controller support module for Bevy
Documentation

Bevy Archie - Rust / Bevy Controller Support Module

License: MIT License: Apache 2.0 Rust Bevy Coverage Following released Bevy versions crates.io docs.rs

Archie Out of Context

A comprehensive game controller support module for the Bevy engine, inspired by the RenPy Controller GUI project.

Controller Support Matrix

Controller Gyroscope Touchpad Adaptive Triggers Rumble Layout
Xbox 360 🔴 🔴 🔴 ✅ Xbox
Xbox One 🔴 🔴 🔴 ✅ Xbox
Xbox Series X|S 🔴 🔴 🔴 ✅ Xbox
PlayStation 3 ✅ 🔴 🔴 ✅ PlayStation
PlayStation 4 ✅ ✅ 🔴 ✅ PlayStation
PlayStation 5 ✅ ✅ ✅ ✅ PlayStation
Switch Pro ✅ 🔴 🔴 ✅ Nintendo
Switch 2 Pro ✅ 🔴 🔴 ✅ Nintendo
Switch Joy-Con ✅ 🔴 🔴 ✅ Nintendo
Steam Controller ✅ ✅ 🔴 ✅ Xbox
Stadia ✅ 🔴 🔴 ✅ Xbox
Amazon Luna 🔴 🔴 🔴 ✅ Xbox
8BitDo M30 🔴 🔴 🔴 ✅ Sega
8BitDo SN30 Pro 🔴 🔴 🔴 ✅ Nintendo
HORI Fighting Cmd 🔴 🔴 🔴 ✅ PlayStation
Generic 🔶 🔶 🔴 ✅ Xbox

Legend: ✅ Supported | 🔴 Hardware limitation | 🔶 Unknown (varies by device)

Note: Gyroscope, touchpad, and adaptive triggers require platform-specific implementations. See Advanced Features for details.

Version Compatibility

bevy bevy_archie
0.18 bevy-0.18 branch
0.17 0.1.x (main)

Features

Core Input System

  • Input Device Detection: Automatically detect and switch between mouse, keyboard, and gamepad input
  • Input Action Mapping: Abstract input actions with customizable bindings for gamepad, keyboard, and mouse
  • Action State Tracking: Query pressed, just_pressed, just_released states and analog values for any action
  • Per-Stick Settings: Independent sensitivity and inversion for left/right analog sticks
  • Deadzone Configuration: Configurable stick deadzones with per-stick customization

Controller Support

  • Controller Icon System: Asset-agnostic icon mapping system that adapts to controller type (Xbox, PlayStation, Nintendo, Steam, Stadia, Generic). Bring your own icon assets or use any compatible pack.
  • Controller Profiles: Automatic detection and profile loading based on vendor/product IDs
  • Multi-controller Support: Handle multiple connected controllers with player assignment
  • Controller Layout Detection: Auto-detect and adapt UI to controller type

Advanced Input Features

  • Actionlike Trait: Define custom action enums with the Actionlike trait for type-safe input handling
  • Haptic Feedback: Rumble and vibration patterns (Constant, Pulse, Explosion, DamageTap, HeavyImpact, Engine, Heartbeat) - fully implemented
  • Input Buffering: Record and analyze input sequences for fighting game-style combo detection
  • Action Modifiers: Detect Tap, Hold, DoubleTap, LongPress, and Released events on actions
  • Button Chords: Detect simultaneous button combinations with configurable clash resolution
  • Virtual Input Composites: Combine buttons into virtual axes (VirtualAxis, VirtualDPad, VirtualDPad3D)
  • Conditional Bindings: Context-aware actions that activate based on game state or custom conditions
  • Input State Machine: Define state machines driven by input actions with automatic transitions
  • Gyroscope Support: Motion controls for PS4/PS5/Switch/Stadia/Steam controllers - complete gesture detection and data structures, needs hardware driver integration (HID/SDL2). See ps5_dualsense_motion.rs and switch_pro_gyro.rs
  • Touchpad Support: PS4/PS5/Steam touchpad input with multi-touch and gesture detection (swipe, pinch, tap) - complete gesture detection and data structures, needs hardware driver integration (HID/SDL2). See ps5_dualsense_motion.rs and steam_touchpad.rs

Multiplayer

  • Player Assignment: Automatic or manual controller-to-player assignment (up to 4 players)
  • Controller Ownership: Track which player owns which controller
  • Hot-swapping: Handle controller disconnection and reassignment

UI & Configuration

  • Controller Remapping: Allow players to remap controller buttons at runtime
  • Virtual Keyboard: On-screen keyboard for controller-friendly text input
  • Virtual Cursor: Gamepad-controlled cursor for mouse-based UI navigation
  • Configuration Persistence: Save and load controller settings to/from JSON files

Developer Tools

  • Input Debugging: Visualize input states, history, and analog values
  • Input Recording: Record input sequences for testing and replay
  • Input Playback: Play back recorded inputs for automated testing
  • Input Mocking: MockInput and MockInputPlugin for unit testing input-dependent systems
  • Build Helpers: Generate icon manifests and organize controller assets at build time

Mobile & Touch

  • Touch Joystick: Virtual on-screen joysticks for mobile platforms with fixed or floating modes

Networking

  • Input Synchronization: ActionDiff and ActionDiffBuffer for efficient network input sync with rollback support

Supported Controllers

  • Xbox - Xbox 360, Xbox One, Xbox Series X|S controllers
  • PlayStation - PS3, PS4, PS5 (DualShock 3, DualShock 4, and DualSense)
  • Nintendo - Switch Pro Controller, Switch 2 Pro, Joy-Cons
  • Steam - Steam Controller, Steam Deck
  • Stadia - Google Stadia Controller (Bluetooth mode)
  • Amazon Luna - Amazon Luna Controller (Xbox-style layout)
  • 8BitDo - M30 (Sega-style), SN30 Pro (Nintendo-style)
  • HORI - Fighting Commander, HORIPAD series
  • Generic - Any other standard gamepad

Note: Stadia controllers must be switched to Bluetooth mode (a permanent one-time operation that was available until Dec 31, 2025). In Bluetooth mode, they function as standard Xbox-style gamepads.

Installation

Add to your Cargo.toml:

[dependencies]
bevy_archie = { path = "path/to/bevy-archie" }
# Or with specific features:
bevy_archie = { path = "path/to/bevy-archie", features = ["full"] }

Quick Start

use bevy::prelude::*;
use bevy_archie::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(ControllerPlugin::default())
        .add_systems(Startup, setup)
        .add_systems(Update, handle_input)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
}

fn handle_input(
    input_state: Res<InputDeviceState>,
    actions: Res<ActionState>,
) {
    // Check which input device is active
    match input_state.active_device {
        InputDevice::Mouse => { /* Mouse logic */ }
        InputDevice::Keyboard => { /* Keyboard logic */ }
        InputDevice::Gamepad(_) => { /* Controller logic */ }
    }

    // Check action states
    if actions.just_pressed(GameAction::Confirm) {
        println!("Confirm pressed!");
    }
}

Action System

Define your game actions and bind them to controller buttons:

use bevy_archie::prelude::*;

// Actions are predefined, but you can extend with custom actions
fn setup_actions(mut action_map: ResMut<ActionMap>) {
    // Rebind an action
    action_map.bind(GameAction::Confirm, GamepadButtonType::South);
    action_map.bind(GameAction::Cancel, GamepadButtonType::East);
    
    // Add keyboard bindings
    action_map.bind_key(GameAction::Confirm, KeyCode::Enter);
    action_map.bind_key(GameAction::Cancel, KeyCode::Escape);
}

Remapping

Enable controller remapping in your settings menu:

fn spawn_remap_ui(mut commands: Commands) {
    commands.spawn((
        RemapButton {
            action: GameAction::Confirm,
        },
        // ... UI components
    ));
}

Configuration

use bevy_archie::prelude::*;

fn configure_controller(mut config: ResMut<ControllerConfig>) {
    // Stick deadzone (0.0 - 1.0)
    config.deadzone = 0.15;
    
    // Per-stick sensitivity multipliers
    config.left_stick_sensitivity = 1.0;
    config.right_stick_sensitivity = 1.5; // Faster cursor movement
    
    // Per-stick X-axis inversion
    config.invert_left_x = false;
    config.invert_right_x = true; // Inverted camera controls
    
    // Auto-detect controller layout
    config.auto_detect_layout = true;
    
    // Force a specific layout
    config.force_layout = Some(ControllerLayout::PlayStation);
}

Virtual Cursor

Enable gamepad-controlled cursor for mouse-based UI:

use bevy::prelude::*;
use bevy_archie::prelude::*;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn virtual cursor (automatically shown when gamepad active)
    bevy_archie::virtual_cursor::spawn_virtual_cursor(
        &mut commands,
        &asset_server,
        None, // Uses default cursor.png
    );
}

fn handle_clicks(mut click_events: EventReader<VirtualCursorClick>) {
    for event in click_events.read() {
        println!("Cursor clicked at: {:?}", event.position);
    }
}

Configuration Persistence

Save and load controller settings:

use bevy_archie::prelude::*;

// Load config on startup
fn load_config(mut config: ResMut<ControllerConfig>) {
    *config = ControllerConfig::load_or_default().unwrap();
}

// Save config
fn save_config(config: Res<ControllerConfig>) {
    config.save_default().unwrap();
}

// Custom path
fn save_to_custom_path(config: Res<ControllerConfig>) {
    config.save_to_file("my_config.json").unwrap();
}

Config files are saved to platform-specific directories:

  • Linux: ~/.config/bevy_archie/controller.json
  • macOS: ~/Library/Application Support/bevy_archie/controller.json
  • Windows: %APPDATA%\bevy_archie\controller.json

Examples

Bevy-archie includes several examples to help you get started:

Basic Examples

Advanced Hardware Integration

These examples show how to integrate real hardware for gyro and touchpad:

  • ps5_dualsense_motion.rs: DualSense gyro + touchpad via hidapi

    • Complete HID report parsing reference
    • Both USB and Bluetooth modes
    • Calibration and data injection patterns
  • switch_pro_gyro.rs: Switch Pro Controller gyro via SDL2

    • Cross-platform gyro support
    • Alternative: Direct HID approach
  • steam_touchpad.rs: Steam Deck/Steam Controller touchpad

    • Steam Input API integration (recommended)
    • Alternative: Direct HID for advanced users

Run examples with:

cargo run --example basic_input
cargo run --example ps5_dualsense_motion --features motion-backends

Supported Controller Layouts

  • Xbox: Xbox 360, Xbox One, Xbox Series controllers
  • PlayStation: DualShock 3/4, DualSense
  • Nintendo: Joy-Con, Pro Controller, GameCube
  • Steam: Steam Controller, Steam Deck
  • Stadia: Google Stadia Controller (Bluetooth mode)
  • Generic: Fallback for unrecognized controllers

Advanced Features

Haptic Feedback

Add rumble and vibration to your game:

use bevy_archie::prelude::*;

fn trigger_rumble(
    mut rumble_events: MessageWriter<RumbleRequest>,
    gamepads: Query<Entity, With<Gamepad>>,
) {
    for gamepad in gamepads.iter() {
        // Simple rumble
        rumble_events.write(RumbleRequest::new(
            gamepad,
            0.8,  // Intensity (0.0-1.0)
            Duration::from_millis(500),
        ));
        
        // Pattern-based rumble
        rumble_events.write(RumbleRequest::with_pattern(
            gamepad,
            RumblePattern::Explosion,  // Strong fade effect
            0.9,
            Duration::from_secs(1),
        ));
    }
}

// Available patterns:
// - Constant: Steady vibration
// - Pulse: Rhythmic pulsing
// - Explosion: Strong start with fade
// - DamageTap: Quick impact feel
// - HeavyImpact: Longer impact
// - Engine: Motor-like hum
// - Heartbeat: Pulse pattern

Input Buffering & Combos

Detect input sequences for fighting game mechanics:

use bevy_archie::prelude::*;

fn setup_combos(mut registry: ResMut<ComboRegistry>) {
    // Define a combo sequence
    registry.register(
        Combo::new("hadouken", vec![
            GameAction::Down,
            GameAction::Right,
            GameAction::Primary,
        ])
        .with_window(Duration::from_millis(500))
    );
}

fn handle_combos(
    mut combo_events: MessageReader<ComboDetected>,
) {
    for event in combo_events.read() {
        println!("Combo detected: {}", event.combo);
        // Trigger special move
    }
}

Multiplayer Controller Management

Assign controllers to players:

use bevy_archie::prelude::*;

fn setup_players(mut commands: Commands) {
    // Spawn player entities
    commands.spawn(Player::new(0)); // Player 1
    commands.spawn(Player::new(1)); // Player 2
}

fn manual_assignment(
    mut assign_events: MessageWriter<AssignControllerRequest>,
    gamepads: Query<Entity, With<Gamepad>>,
) {
    // Manually assign a controller to a player
    if let Some(gamepad) = gamepads.iter().next() {
        assign_events.write(AssignControllerRequest {
            gamepad,
            player: PlayerId::new(0),
        });
    }
}

fn check_ownership(
    ownership: Res<ControllerOwnership>,
    input: Res<ActionState>,
) {
    // Check which player owns a gamepad
    if let Some(player_id) = ownership.get_owner(gamepad_entity) {
        println!("Controller owned by player {}", player_id.id());
    }
    
    // Get gamepad for a specific player
    if let Some(gamepad) = ownership.get_gamepad(PlayerId::new(0)) {
        // Read input for player 1's controller
    }
}

Action Modifiers

Detect advanced input patterns:

use bevy_archie::prelude::*;

fn handle_modifiers(
    mut modifier_events: MessageReader<ModifiedActionEvent>,
) {
    for event in modifier_events.read() {
        match event.modifier {
            ActionModifier::Tap => {
                println!("Quick tap on {:?}", event.action);
            }
            ActionModifier::Hold => {
                println!("Held for {} seconds", event.duration);
            }
            ActionModifier::DoubleTap => {
                println!("Double-tapped!");
            }
            ActionModifier::LongPress => {
                println!("Long press detected");
            }
            ActionModifier::Released => {
                println!("Button released");
            }
        }
    }
}

// Configure modifier timings
fn configure_modifiers(mut state: ResMut<ActionModifierState>) {
    state.config.hold_duration = 0.2;        // 200ms for hold
    state.config.long_press_duration = 0.8;  // 800ms for long press
    state.config.double_tap_window = 0.3;    // 300ms between taps
}

PlayStation Touchpad

Handle touchpad input on DualShock 4 and DualSense:

use bevy_archie::prelude::*;

fn handle_touchpad(
    mut gesture_events: MessageReader<TouchpadGestureEvent>,
    touchpad_query: Query<&TouchpadData>,
) {
    // Handle gestures
    for event in gesture_events.read() {
        match event.gesture {
            TouchpadGesture::Tap => println!("Tapped at {:?}", event.position),
            TouchpadGesture::TwoFingerTap => println!("Two-finger tap"),
            TouchpadGesture::SwipeLeft => println!("Swiped left"),
            TouchpadGesture::SwipeRight => println!("Swiped right"),
            TouchpadGesture::SwipeUp => println!("Swiped up"),
            TouchpadGesture::SwipeDown => println!("Swiped down"),
            TouchpadGesture::PinchIn => println!("Pinch in (zoom out)"),
            TouchpadGesture::PinchOut => println!("Pinch out (zoom in)"),
        }
    }
    
    // Direct touchpad access
    for touchpad in touchpad_query.iter() {
        let finger1_pos = touchpad.finger1.position();
        let finger1_delta = touchpad.finger1_delta();
        
        if touchpad.button_pressed {
            println!("Touchpad button pressed");
        }
        
        println!("Active fingers: {}", touchpad.active_fingers());
    }
}

Controller Profiles

Automatically detect controller models and load profiles:

use bevy_archie::prelude::*;

fn setup_profiles(mut registry: ResMut<ProfileRegistry>) {
    // Register a custom profile for PS5 controllers
    let ps5_profile = ControllerProfile::new("PS5 Default", ControllerModel::PS5)
        .with_action_map(my_custom_action_map())
        .with_layout(ControllerLayout::PlayStation);
    
    registry.register(ps5_profile);
    registry.auto_load = true;  // Auto-apply profiles when controllers connect
}

fn handle_detection(
    mut detected_events: MessageReader<ControllerDetected>,
    detected_query: Query<&DetectedController>,
) {
    for event in detected_events.read() {
        println!("Detected: {:?}", event.model);
        
        // Check controller capabilities
        if event.model.supports_gyro() {
            println!("Controller has gyroscope support");
        }
        if event.model.supports_touchpad() {
            println!("Controller has touchpad");
        }
        if event.model.supports_adaptive_triggers() {
            println!("Controller has adaptive triggers (PS5)");
        }
    }
}

Motion Controls (Gyroscope)

Access gyroscope and accelerometer data:

use bevy_archie::prelude::*;

fn handle_motion(
    mut gesture_events: MessageReader<MotionGestureDetected>,
    gyro_query: Query<&GyroData>,
    accel_query: Query<&AccelData>,
) {
    // Handle detected gestures
    for event in gesture_events.read() {
        match event.gesture {
            MotionGesture::Flick => println!("Quick rotation detected"),
            MotionGesture::Shake => println!("Controller shaken"),
            MotionGesture::Tilt => println!("Controller tilted"),
            MotionGesture::Roll => println!("Controller rolled"),
        }
    }
    
    // Direct gyro access
    for gyro in gyro_query.iter() {
        if gyro.valid {
            let rotation_speed = gyro.magnitude();
            println!("Rotation: pitch={}, yaw={}, roll={}", 
                gyro.pitch, gyro.yaw, gyro.roll);
        }
    }
    
    // Direct accelerometer access
    for accel in accel_query.iter() {
        if accel.valid {
            if accel.is_shaking(3.0) {  // Threshold in m/s²
                println!("Shake detected!");
            }
        }
    }
}

// Configure motion controls
fn configure_motion(mut config: ResMut<MotionConfig>) {
    config.gyro_sensitivity = 1.5;
    config.gyro_deadzone = 0.01;
    config.enabled = true;
}

Debug Tools

Visualize and record input for testing:

use bevy_archie::prelude::*;

fn toggle_debug(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut debug_events: MessageWriter<ToggleInputDebug>,
) {
    if keyboard.just_pressed(KeyCode::F12) {
        debug_events.write(ToggleInputDebug { enable: true });
    }
}

fn configure_debugger(mut debugger: ResMut<InputDebugger>) {
    debugger.show_history = true;   // Show input history
    debugger.show_sticks = true;    // Show analog stick positions
    debugger.show_buttons = true;   // Show button states
    debugger.show_gyro = true;      // Show gyro data
    debugger.history_size = 50;     // Keep last 50 inputs
}

fn start_recording(
    mut record_events: MessageWriter<RecordingCommand>,
) {
    record_events.write(RecordingCommand { start: true });
}

fn playback_recording(
    recorder: Res<InputRecorder>,
    mut playback_events: MessageWriter<PlaybackCommand>,
) {
    if !recorder.recording {
        playback_events.write(PlaybackCommand {
            inputs: recorder.recorded.clone(),
        });
    }
}

Virtual Input Composites

Combine multiple buttons into unified axes:

use bevy_archie::prelude::*;

fn setup_virtual_inputs(mut commands: Commands) {
    // Combine W/S keys into a vertical axis (-1.0 to 1.0)
    let vertical = VirtualAxis::new(KeyCode::KeyW, KeyCode::KeyS);
    
    // Combine WASD into a 2D movement vector
    let movement = VirtualDPad::new(
        KeyCode::KeyW,  // up
        KeyCode::KeyS,  // down
        KeyCode::KeyA,  // left
        KeyCode::KeyD,  // right
    );
    
    // Combine multiple buttons with OR logic
    let any_jump = VirtualButton::any(vec![
        KeyCode::Space,
        KeyCode::KeyW,
    ]);
}

Button Chords

Detect simultaneous button presses:

use bevy_archie::prelude::*;

fn setup_chords(mut chord_registry: ResMut<ChordRegistry>) {
    // Register Ctrl+S chord for saving
    let save_chord = ButtonChord::from_buttons([KeyCode::ControlLeft, KeyCode::KeyS]);
    chord_registry.register("save", save_chord);
    
    // Register gamepad chord (LB + RB for special move)
    let special_chord = ButtonChord::from_buttons([
        GamepadButton::LeftTrigger,
        GamepadButton::RightTrigger,
    ]);
    chord_registry.register("special_move", special_chord)
        .with_clash_strategy(ClashStrategy::PrioritizeLongest);
}

fn handle_chords(mut chord_events: MessageReader<ChordTriggered>) {
    for event in chord_events.read() {
        match event.chord_name.as_str() {
            "save" => println!("Save triggered!"),
            "special_move" => println!("Special move!"),
            _ => {}
        }
    }
}

Conditional Bindings

Make actions context-aware:

use bevy_archie::prelude::*;

#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
enum GameState {
    #[default]
    Menu,
    Playing,
    Paused,
}

fn setup_conditional_bindings(mut bindings: ResMut<ConditionalBindings>) {
    // "Confirm" only works in menus
    bindings.add(
        GameAction::Confirm,
        KeyCode::Enter,
        InputCondition::in_state(GameState::Menu),
    );
    
    // "Attack" only works while playing
    bindings.add(
        GameAction::Primary,
        KeyCode::Space,
        InputCondition::in_state(GameState::Playing),
    );
    
    // Chain conditions: works in Playing OR Paused
    bindings.add(
        GameAction::Pause,
        KeyCode::Escape,
        InputCondition::in_state(GameState::Playing)
            .or(InputCondition::in_state(GameState::Paused)),
    );
}

Input State Machine

Define states driven by input:

use bevy_archie::prelude::*;

fn setup_state_machine(mut commands: Commands) {
    let state_machine = StateMachineBuilder::new()
        .add_state("idle")
        .add_state("walking")
        .add_state("running")
        .add_state("jumping")
        .initial_state("idle")
        // Transitions based on actions
        .add_transition("idle", "walking", GameAction::Up)
        .add_transition("idle", "walking", GameAction::Down)
        .add_transition("walking", "running", GameAction::Primary) // Sprint
        .add_transition("idle", "jumping", GameAction::Confirm)    // Jump
        .add_transition("walking", "jumping", GameAction::Confirm)
        // Return to idle when no input
        .add_transition("walking", "idle", GameAction::Released)
        .build();
    
    commands.insert_resource(state_machine);
}

fn handle_state_changes(mut state_events: MessageReader<StateMachineTransition>) {
    for event in state_events.read() {
        println!("State changed: {} -> {}", event.from_state, event.to_state);
    }
}

Input Mocking for Tests

Test input-dependent systems:

use bevy_archie::prelude::*;
use bevy_archie::testing::*;

#[test]
fn test_jump_mechanic() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugins(MockInputPlugin);  // Use mock input instead of real
    
    // Simulate pressing jump
    let mock = MockInput::new()
        .press(GameAction::Confirm)
        .with_duration(Duration::from_millis(100));
    
    app.world.insert_resource(mock);
    app.update();
    
    // Assert jump was triggered
    let actions = app.world.resource::<ActionState>();
    assert!(actions.just_pressed(GameAction::Confirm));
}

// Script a sequence of inputs
fn test_combo_detection() {
    let sequence = MockInputSequence::new()
        .then_press(GameAction::Down, Duration::from_millis(50))
        .then_press(GameAction::Right, Duration::from_millis(50))
        .then_press(GameAction::Primary, Duration::from_millis(50));
    
    // Inject and verify combo detection
}

Touch Joystick (Mobile)

Add virtual joysticks for touch screens:

use bevy_archie::prelude::*;
use bevy_archie::touch_joystick::*;

fn setup_touch_controls(mut commands: Commands) {
    // Left stick for movement (fixed position)
    commands.spawn(TouchJoystick {
        position: Vec2::new(150.0, 150.0),
        radius: 100.0,
        dead_zone: 0.15,
        mode: JoystickMode::Fixed,
        output_action: Some(GameAction::Up), // Maps to movement
    });
    
    // Right stick for camera (floating - appears where you touch)
    commands.spawn(TouchJoystick {
        position: Vec2::ZERO,
        radius: 80.0,
        dead_zone: 0.1,
        mode: JoystickMode::Floating,
        output_action: Some(GameAction::LookUp),
    });
}

fn read_joystick(joysticks: Query<&TouchJoystick>) {
    for joystick in joysticks.iter() {
        let direction = joystick.direction(); // Vec2 from -1 to 1
        let magnitude = joystick.magnitude(); // 0.0 to 1.0
    }
}

Network Input Sync

Synchronize input state across network:

use bevy_archie::prelude::*;
use bevy_archie::networking::*;

fn send_input_to_server(
    action_state: Res<ActionState>,
    mut diff_buffer: ResMut<ActionDiffBuffer>,
    mut network: ResMut<NetworkClient>,
) {
    // Generate diff from last sent state
    if let Some(diff) = diff_buffer.generate_diff(&action_state) {
        // Serialize and send
        let bytes = diff.serialize().expect("serialization failed");
        network.send_reliable(bytes);
        
        // Store for potential rollback
        diff_buffer.push(diff);
    }
}

fn receive_input_from_client(
    mut network_events: MessageReader<NetworkPacket>,
    mut remote_actions: ResMut<RemoteActionStates>,
) {
    for packet in network_events.read() {
        let diff = ActionDiff::deserialize(&packet.data)
            .expect("deserialization failed");
        remote_actions.apply_diff(packet.player_id, diff);
    }
}

Examples

Run the examples to see features in action:

# Basic input handling
cargo run --example basic_input

# Controller icon display
cargo run --example controller_icons

# Button remapping UI
cargo run --example remapping

# Virtual cursor
cargo run --example virtual_cursor

# Config persistence
cargo run --example config_persistence

API Reference

SystemSets

bevy_archie provides the following system sets for ordering your systems:

pub enum ControllerSystemSet {
    /// Device detection runs first.
    Detection,
    /// Action state updates.
    Actions,
    /// UI updates based on input state.
    UI,
}

Execution order: Detection → Actions → UI

Use these to order your systems relative to controller input processing:

app.add_systems(Update, my_input_system.after(ControllerSystemSet::Actions));

Core Types

InputDeviceState

Tracks the currently active input device.

pub struct InputDeviceState {
    pub active_device: InputDevice,
    pub last_gamepad: Option<Entity>,
    // ...
}

// Methods
fn using_gamepad(&self) -> bool;
fn using_keyboard(&self) -> bool;
fn using_mouse(&self) -> bool;
fn active_gamepad(&self) -> Option<Entity>;

ActionState

Query the state of game actions.

pub struct ActionState {
    // ...
}

// Methods
fn pressed(&self, action: GameAction) -> bool;
fn just_pressed(&self, action: GameAction) -> bool;
fn just_released(&self, action: GameAction) -> bool;
fn value(&self, action: GameAction) -> f32;  // 0.0-1.0 for analog

ActionMap

Map actions to input sources.

pub struct ActionMap {
    // ...
}

// Methods
fn bind_gamepad(&mut self, action: GameAction, button: GamepadButton);
fn bind_axis(&mut self, action: GameAction, axis: GamepadAxis, direction: AxisDirection, threshold: f32);
fn bind_key(&mut self, action: GameAction, key: KeyCode);
fn bind_mouse(&mut self, action: GameAction, button: MouseButton);
fn clear_bindings(&mut self, action: GameAction);
fn primary_gamepad_button(&self, action: GameAction) -> Option<GamepadButton>;

GameAction

Predefined actions that can be customized.

pub enum GameAction {
    // Navigation
    Confirm, Cancel, Pause, Select,
    
    // Movement
    Up, Down, Left, Right,
    
    // Camera
    LookUp, LookDown, LookLeft, LookRight,
    
    // Actions
    Primary, Secondary,
    LeftShoulder, RightShoulder,
    LeftTrigger, RightTrigger,
    
    // UI
    PageLeft, PageRight,
    
    // Custom slots
    Custom1, Custom2, Custom3, Custom4,
}

// Methods
fn all() -> &'static [GameAction];
fn display_name(&self) -> &'static str;
fn is_remappable(&self) -> bool;
fn is_required(&self) -> bool;

Configuration

ControllerConfig

Main configuration resource.

pub struct ControllerConfig {
    pub deadzone: f32,
    pub left_stick_sensitivity: f32,
    pub right_stick_sensitivity: f32,
    pub invert_left_x: bool,
    pub invert_left_y: bool,
    pub invert_right_x: bool,
    pub invert_right_y: bool,
    pub auto_detect_layout: bool,
    pub force_layout: Option<ControllerLayout>,
}

// Methods (for persistence)
fn save_default(&self) -> std::io::Result<()>;
fn save_to_file(&self, path: impl AsRef<Path>) -> std::io::Result<()>;
fn load_or_default() -> std::io::Result<Self>;
fn load_from_file(path: impl AsRef<Path>) -> std::io::Result<Self>;

Events (now Messages in Bevy 0.17)

All events are now Message types. Use MessageReader and MessageWriter:

  • InputDeviceChanged - Input device switched
  • GamepadConnected / GamepadDisconnected - Controller connection
  • VirtualCursorClick - Virtual cursor clicked
  • RumbleRequest - Request haptic feedback
  • ComboDetected - Input combo detected
  • ModifiedActionEvent - Action modifier detected
  • TouchpadGestureEvent - Touchpad gesture
  • MotionGestureDetected - Motion gesture
  • ControllerAssigned / ControllerUnassigned - Player assignment
  • ControllerDetected - Controller model detected
  • StartRemapEvent / RemapEvent - Remapping events
  • ToggleInputDebug / RecordingCommand / PlaybackCommand - Debug commands

Platform Support

Haptic Feedback

Fully implemented using Bevy's native GamepadRumbleRequest. Works out of the box on all platforms that support rumble through gilrs.

Motion Controls

What's implemented: Complete gesture detection (shake, tilt, flick, roll), data structures (GyroData, AccelData), and event system.

What's needed: Hardware drivers to read sensor data from controllers. See the Hardware Integration Guide for detailed instructions.

Quick example (see ps5_dualsense_motion.rs for full code):

fn inject_gyro_data(mut gamepads: Query<&mut GyroData>) {
    // Use hidapi, SDL2, or platform-specific drivers
    let (pitch, yaw, roll) = read_controller_sensors();
    for mut gyro in &mut gamepads {
        gyro.set_raw(pitch, yaw, roll);
    }
}

Touchpad

What's implemented: Complete gesture detection (swipe, pinch, tap, multi-touch), data structures (TouchpadData), and event system.

What's needed: Hardware drivers to read touchpad data from controllers. See the Hardware Integration Guide for detailed instructions.

Quick example (see ps5_dualsense_motion.rs for full code):

fn inject_touchpad_data(mut gamepads: Query<&mut TouchpadData>) {
    // Use hidapi, SDL2, or platform-specific drivers
    let (x, y, pressed) = read_touchpad();
    for mut touchpad in &mut gamepads {
        touchpad.set_finger(0, x, y, pressed);
        touchpad.update_frame();
    }
}

Hardware integration resources:

Migration from Bevy 0.16

Bevy 0.17 introduced a major change: Events are now Messages.

Key Changes

// Bevy 0.16
app.add_event::<MyEvent>();
fn system(mut events: EventWriter<MyEvent>) { }
fn reader(mut events: EventReader<MyEvent>) { }

// Bevy 0.17
app.add_message::<MyEvent>();
fn system(mut events: MessageWriter<MyEvent>) { }
fn reader(mut events: MessageReader<MyEvent>) { }

All events in bevy_archie have been migrated to Messages.

Testing

Running Tests

# Run all unit tests
cargo test --lib

# Run all tests including integration tests
cargo test

# Run specific test module
cargo test --lib config::tests

# Run with all features enabled
cargo test --all-features

Test Coverage

The project includes comprehensive unit and integration tests covering:

  • Core Modules (actions, config, detection): Input device detection, action mapping, configuration management
  • Icon System (icons): Icon filename generation, platform-specific labels, asset loading
  • Integration Tests: Plugin initialization, resource management, end-to-end workflows

Coverage Goal: 80% code coverage across all modules. See docs/TEST_COVERAGE.md for coverage tools and analysis.

Test Structure

  • src/*/tests: Unit tests for each module
  • tests/integration_tests.rs: Integration tests for full plugin functionality
  • .cargo/config.toml: Test configuration and aliases

For detailed coverage analysis instructions, see docs/TEST_COVERAGE.md.

Controller Icons

This library's icon system is asset-agnostic - it provides the infrastructure for loading and displaying controller-appropriate icons, but does not bundle icon assets in the crate to keep download size minimal.

Recommended Icon Pack

We recommend Mr. Breakfast's Free Prompts - a comprehensive CC0-licensed icon pack with 400+ PNG/SVG icons for Xbox, PlayStation, Nintendo Switch, Steam Deck, keyboard, and mouse.

Adding Icon Assets

  1. Download the icon pack from itch.io

  2. Extract to your project's assets folder:

    your_game/
    └── assets/
        └── icons/
            └── mrbreakfast/
                ├── LICENSE
                └── png/
                    ├── xbox_a.png
                    ├── ps_cross.png
                    └── ...
    
  3. Configure the icon system:

    fn setup_icons(mut commands: Commands) {
        commands.insert_resource(
            ControllerIconAssets::new("icons/mrbreakfast/png")
        );
    }
    

Alternative Icon Packs

You can use any icon pack that follows standard naming conventions:

Icon Naming Conventions

The system expects icons named according to platform conventions:

  • Xbox: xbox_a.png, xbox_b.png, xbox_lb.png, xbox_lt.png
  • PlayStation: ps_cross.png, ps_circle.png, ps_l1.png, ps_l2.png
  • Nintendo: switch_b.png, switch_a.png, switch_l.png, switch_zl.png
  • Generic: left_stick.png, right_stick.png, dpad.png

Icon sizes are supported via suffixes: xbox_a_small.png (32x32), xbox_a.png (48x48), xbox_a_large.png (64x64).

If your icon pack uses different naming, create a thin wrapper or use symbolic links.

Credits

Inspired by the RenPy Controller GUI by Feniks.

Included Assets

License

Licensed under either of:

at your option.