| Crates.io | bevy_archie |
| lib.rs | bevy_archie |
| version | 0.1.6 |
| created_at | 2026-01-17 07:17:49.173092+00 |
| updated_at | 2026-01-23 14:08:27.676371+00 |
| description | A comprehensive game controller support module for Bevy |
| homepage | https://github.com/greysquirr3l/bevy-archie |
| repository | https://github.com/greysquirr3l/bevy-archie |
| max_upload_size | |
| id | 2050081 |
| size | 729,007 |

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 | bevy-0.18 branch |
| 0.17 | 0.1.x (main) |
Actionlike trait for type-safe input handlingVirtualAxis, VirtualDPad, VirtualDPad3D)MockInput and MockInputPlugin for unit testing input-dependent systemsActionDiff and ActionDiffBuffer for efficient network input sync with rollback supportNote: 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"] }
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);
}
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:
~/.config/bevy_archie/controller.json~/Library/Application Support/bevy_archie/controller.json%APPDATA%\bevy_archie\controller.jsonBevy-archie includes several examples to help you get started:
These examples show how to integrate real hardware for gyro and touchpad:
ps5_dualsense_motion.rs: DualSense gyro + touchpad via hidapi
switch_pro_gyro.rs: Switch Pro Controller gyro via SDL2
steam_touchpad.rs: Steam Deck/Steam Controller touchpad
Run examples with:
cargo run --example basic_input
cargo run --example ps5_dualsense_motion --features motion-backends
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
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
}
}
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_persistence
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));
InputDeviceStateTracks 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>;
ActionStateQuery 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
ActionMapMap 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>;
GameActionPredefined 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;
ControllerConfigMain 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 commandsFully 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:
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-features
The project includes comprehensive unit and integration tests covering:
actions, config, detection): Input device detection, action mapping, configuration managementicons): Icon filename generation, platform-specific labels, asset loadingCoverage 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 aliasesFor detailed coverage analysis instructions, see docs/TEST_COVERAGE.md.
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.
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.
Download the icon pack from itch.io
Extract to your project's assets folder:
your_game/
āāā assets/
āāā icons/
āāā mrbreakfast/
āāā LICENSE
āāā png/
āāā xbox_a.png
āāā ps_cross.png
āāā ...
Configure the icon system:
fn setup_icons(mut commands: Commands) {
commands.insert_resource(
ControllerIconAssets::new("icons/mrbreakfast/png")
);
}
You can use any icon pack that follows standard naming conventions:
The system expects icons named according to platform conventions:
xbox_a.png, xbox_b.png, xbox_lb.png, xbox_lt.pngps_cross.png, ps_circle.png, ps_l1.png, ps_l2.pngswitch_b.png, switch_a.png, switch_l.png, switch_zl.pngleft_stick.png, right_stick.png, dpad.pngIcon 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.
Inspired by the RenPy Controller GUI by Feniks.
Licensed under either of:
at your option.