Note: This branch (
main) now targets Bevy 0.18. If you need Bevy 0.17 support, see thebevy-0.17branch.
A comprehensive game controller support module for the Bevy engine, inspired by the RenPy Controller GUI project.
| 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.
| bevy | bevy_archie |
|---|---|
| 0.18 | 0.2.x (main) |
| 0.17 | 0.1.x (bevy-0.17) |
- 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 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
- Actionlike Trait: Define custom action enums with the
Actionliketrait 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
- 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
- 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
- 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:
MockInputandMockInputPluginfor unit testing input-dependent systems - Build Helpers: Generate icon manifests and organize controller assets at build time
- Touch Joystick: Virtual on-screen joysticks for mobile platforms with fixed or floating modes
- Input Synchronization:
ActionDiffandActionDiffBufferfor efficient network input sync with rollback support
- 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.
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"] }| Configuration | Pre-link (.rlib) | Final Binary Impact |
|---|---|---|
| Default features | ~8.7 MB | ~200-400 KB |
| All features | ~9.7 MB | ~300-500 KB |
The .rlib size includes Rust metadata and monomorphization templates. After LTO and dead code elimination, bevy_archie adds roughly 0.5-1% overhead on top of a typical Bevy application (~50-80 MB).
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!");
}
}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);
}Note: bevy-archie is asset-agnostic. You must provide your own icon assets or use a compatible icon pack like:
- Mr. Breakfast's Free Prompts (400+ icons, Xbox/PS/Switch/Steam Deck)
- Kenney Input Prompts
- Custom artwork
The icon system provides platform-aware filename generation and asset loading infrastructure. Point it to your icon directory:
fn setup_icons(mut commands: Commands) {
commands.insert_resource(
ControllerIconAssets::new("assets/icons") // Your icon directory
);
}Display controller-appropriate button icons in your UI:
fn spawn_button_prompt(
mut commands: Commands,
icon_assets: Res<ControllerIconAssets>,
input_state: Res<InputDeviceState>,
) {
let icon = icon_assets.get_icon(
GamepadButtonType::South,
input_state.controller_layout,
);
commands.spawn(ImageNode {
image: icon,
..default()
});
}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 to match the expected names.
Enable controller remapping in your settings menu:
fn spawn_remap_ui(mut commands: Commands) {
commands.spawn((
RemapButton {
action: GameAction::Confirm,
},
// ... UI components
));
}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);
}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);
}
}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
Bevy-archie includes several examples to help you get started:
- basic_input.rs: Simple input handling
- controller_icons.rs: Display controller-specific icons
- remapping.rs: Runtime button remapping
- virtual_cursor.rs: Gamepad-controlled cursor
- config_persistence.rs: Save/load settings
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- 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)
- Amazon Luna: Amazon Luna Controller (Xbox-style layout)
- Generic: Fallback for unrecognized controllers
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 patternDetect 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
}
}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
}
}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
}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());
}
}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)");
}
}
}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;
}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(),
});
}
}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,
]);
}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!"),
_ => {}
}
}
}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)),
);
}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);
}
}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
}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
}
}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);
}
}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_persistencebevy_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));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>;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 analogMap 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>;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;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>;All events are now Message types. Use MessageReader and MessageWriter:
InputDeviceChanged- Input device switchedGamepadConnected/GamepadDisconnected- Controller connectionVirtualCursorClick- Virtual cursor clickedRumbleRequest- Request haptic feedbackComboDetected- Input combo detectedModifiedActionEvent- Action modifier detectedTouchpadGestureEvent- Touchpad gestureMotionGestureDetected- Motion gestureControllerAssigned/ControllerUnassigned- Player assignmentControllerDetected- Controller model detectedStartRemapEvent/RemapEvent- Remapping eventsToggleInputDebug/RecordingCommand/PlaybackCommand- Debug commands
Fully implemented using Bevy's native GamepadRumbleRequest. Works out of the box on all platforms that support rumble through gilrs.
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);
}
}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:
- ๐ Hardware Integration Guide - Complete guide with controller-specific details
- ๐ฎ PS5 DualSense Example - Both gyro and touchpad
- ๐ฎ Switch Pro Example - Gyro via SDL2
- ๐ฎ Steam Deck Example - Touchpad via Steam Input API
Bevy 0.17 introduced a major change: Events are now Messages.
// 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.
# 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-featuresThe 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.
src/*/tests: Unit tests for each moduletests/integration_tests.rs: Integration tests for full plugin functionality.cargo/config.toml: Test configuration and aliases
Inspired by the RenPy Controller GUI by Feniks.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
