diff --git a/examples/twin_stick_controller.rs b/examples/twin_stick_controller.rs new file mode 100644 index 00000000..f5aa53fe --- /dev/null +++ b/examples/twin_stick_controller.rs @@ -0,0 +1,217 @@ +//! This is a fairly complete example that implements a twin stick controller. +//! +//! The controller supports both gamepad/MKB input and switches between them depending on +//! the most recent input. +//! +//! This example builds on top of several of the concepts introduced in other examples. In particular, +//! the `default_controls`. `mouse_position`, and `action_state_resource` examples. + +use bevy::{ + input::gamepad::GamepadEvent, input::keyboard::KeyboardInput, prelude::*, window::PrimaryWindow, +}; +use leafwing_input_manager::{axislike::DualAxisData, prelude::*, user_input::InputKind}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(InputManagerPlugin::::default()) + // Defined below, detects whether MKB or gamepad are active + .add_plugins(InputModeManagerPlugin) + .init_resource::>() + .insert_resource(PlayerAction::default_input_map()) + // Set up the scene + .add_systems(Startup, setup_scene) + // Set up the input processing + .add_systems( + Update, + player_mouse_look.run_if(in_state(ActiveInput::MouseKeyboard)), + ) + .add_systems(Update, control_player.after(player_mouse_look)) + .run(); +} + +// ----------------------------- Player Action Input Handling ----------------------------- +#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)] +pub enum PlayerAction { + Move, + Look, + Shoot, +} + +// Exhaustively match `PlayerAction` and define the default binding to the input +impl PlayerAction { + fn default_gamepad_binding(&self) -> UserInput { + // Match against the provided action to get the correct default gamepad input + match self { + Self::Move => UserInput::Single(InputKind::DualAxis(DualAxis::left_stick())), + Self::Look => UserInput::Single(InputKind::DualAxis(DualAxis::right_stick())), + Self::Shoot => { + UserInput::Single(InputKind::GamepadButton(GamepadButtonType::RightTrigger)) + } + } + } + + fn default_mkb_binding(&self) -> UserInput { + // Match against the provided action to get the correct default gamepad input + match self { + Self::Move => UserInput::VirtualDPad(VirtualDPad::wasd()), + Self::Look => UserInput::VirtualDPad(VirtualDPad::arrow_keys()), + Self::Shoot => UserInput::Single(InputKind::Mouse(MouseButton::Left)), + } + } + + fn default_input_map() -> InputMap { + let mut input_map = InputMap::default(); + + for variant in PlayerAction::variants() { + input_map.insert(variant.default_mkb_binding(), variant.clone()); + input_map.insert(variant.default_gamepad_binding(), variant); + } + input_map + } +} + +// ----------------------------- Input mode handling ----------------------------- +pub struct InputModeManagerPlugin; + +impl Plugin for InputModeManagerPlugin { + fn build(&self, app: &mut App) { + // Add a state to record the current active input + app.add_state::() + // System to switch to gamepad as active input + .add_systems( + Update, + activate_gamepad.run_if(in_state(ActiveInput::MouseKeyboard)), + ) + // System to switch to MKB as active input + .add_systems(Update, activate_mkb.run_if(in_state(ActiveInput::Gamepad))); + } +} + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] +enum ActiveInput { + #[default] + MouseKeyboard, + Gamepad, +} + +/// Switch the gamepad when any button is pressed or any axis input used +fn activate_gamepad( + mut next_state: ResMut>, + mut gamepad_evr: EventReader, +) { + for ev in gamepad_evr.read() { + match ev { + GamepadEvent::Button(_) | GamepadEvent::Axis(_) => { + info!("Switching to gamepad input"); + next_state.set(ActiveInput::Gamepad); + return; + } + _ => (), + } + } +} + +/// Switch to mouse and keyboard input when any keyboard button is pressed +fn activate_mkb( + mut next_state: ResMut>, + mut kb_evr: EventReader, +) { + for _ev in kb_evr.read() { + info!("Switching to mouse and keyboard input"); + next_state.set(ActiveInput::MouseKeyboard); + } +} + +// ----------------------------- Mouse input handling----------------------------- + +/// Note that we handle the action_state mutation differently here than in the `mouse_position` +/// example. Here we don't use an `ActionDriver`, but change the action data directly. +fn player_mouse_look( + camera_query: Query<(&GlobalTransform, &Camera)>, + player_query: Query<&Transform, With>, + window_query: Query<&Window, With>, + mut action_state: ResMut>, +) { + // Update each actionstate with the mouse position from the window + // by using the referenced entities in ActionStateDriver and the stored action as + // a key into the action data + let (camera_transform, camera) = camera_query.get_single().expect("Need a single camera"); + let player_transform = player_query.get_single().expect("Need a single player"); + let window = window_query + .get_single() + .expect("Need a single primary window"); + + // Many steps can fail here, so we'll wrap in an option pipeline + // First check if cursor is in window + // Then check if the ray intersects the plane defined by the player + // Then finally compute the point along the ray to look at + if let Some(p) = window + .cursor_position() + .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor)) + .and_then(|ray| Some(ray).zip(ray.intersect_plane(player_transform.translation, Vec3::Y))) + .map(|(ray, p)| ray.get_point(p)) + { + let diff = (p - player_transform.translation).xz(); + if diff.length_squared() > 1e-3f32 { + // Press the look action, so we can check that it is active + action_state.press(PlayerAction::Look); + // Modify the action data to set the axis + let action_data = action_state.action_data_mut(PlayerAction::Look); + // Flipping y sign here to be consistent with gamepad input. We could also invert the gamepad y axis + action_data.axis_pair = Some(DualAxisData::from_xy(Vec2::new(diff.x, -diff.y))); + } + } +} + +// ----------------------------- Movement ----------------------------- +fn control_player( + time: Res