Files
storyteller-bevy/studio/src/input.rs
T
Danny McGee 699c1baa4b Add WASD camera controls (#1)
This PR adds FPS-style controls to the camera in addition to the existing DCC-style pan/orbit/dolly controls. Additionally, some architectural changes have been made to facilitate this and pave the way toward making all of the app controls configurable in the future. Namely, a new `input` module has been added, loosely inspired by Unreal Engine's "Enhanced Input" system (and as I understand it, conceptually similar to Unity's "new" input system as well).

The basic idea is that raw device inputs (coming from `bevy::input`) are mapped to named, app-specific "input actions" before being consumed by other parts of the application. For example, in the camera motion system, instead of checking for mouse motion while the middle mouse button is pressed, we now check for a `CameraMotion::Pan` input action event.

Input actions carry information about their lifecycle phase (`Start`, `Ongoing`, and `Stop` — a stand-in for Bevy's `just_pressed`, `pressed`, and `just_released` methods) and some type of magnitude (`f32`, `Vec2`, or `Vec3` depending on the action).

At present, we've simply moved the hard-coded device input checks from the `orbit_cam` module to the new `input` module, but theoretically, this could eventually be refactored to read the mappings between device inputs and input actions from some kind of user configuration.
2023-10-05 22:29:01 -04:00

185 lines
5.9 KiB
Rust

use bevy::{input::mouse::MouseMotion, prelude::*, utils::HashSet};
pub struct StudioInputPlugin;
impl Plugin for StudioInputPlugin {
fn build(&self, app: &mut App) {
app.add_event::<InputAction>();
app.add_systems(
PreUpdate,
translate_bevy_events.after(bevy::input::InputSystem),
);
}
}
#[derive(Event, Clone, Copy)]
pub struct InputAction(pub Lifecycle, pub Action);
#[derive(Clone, Copy)]
pub enum Lifecycle {
Start,
Ongoing,
Stop,
}
#[derive(Clone, Copy)]
pub enum Action {
Camera(CameraMotion),
}
#[derive(Clone, Copy)]
pub enum CameraMotion {
Translate(Vec3),
Pan(Vec2),
Zoom(f32),
Orbit(Vec2),
Rotate(Vec2),
}
impl From<CameraMotion> for Action {
fn from(value: CameraMotion) -> Self {
Action::Camera(value)
}
}
// TODO: Make the bindings of raw device inputs to InputActions configurable
fn translate_bevy_events(
mouse_motion: EventReader<MouseMotion>,
mouse_buttons: Res<Input<MouseButton>>,
keyboard: Res<Input<KeyCode>>,
mut input: EventWriter<InputAction>,
) {
// Camera Pan
if mouse_buttons.pressed(MouseButton::Middle) {
let delta = read_mouse_delta(mouse_motion);
let lifecycle = if mouse_buttons.just_pressed(MouseButton::Middle) {
Lifecycle::Start
} else {
Lifecycle::Ongoing
};
input.send(InputAction(lifecycle, CameraMotion::Pan(delta).into()));
}
// Stop Camera Panning
else if mouse_buttons.just_released(MouseButton::Middle) {
input.send(InputAction(
Lifecycle::Stop,
CameraMotion::Pan(Vec2::ZERO).into(),
));
}
// Camera Orbit/Zoom
else if keyboard.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]) {
if mouse_buttons.any_pressed([MouseButton::Left, MouseButton::Right]) {
let delta = read_mouse_delta(mouse_motion);
let lifecycle =
if mouse_buttons.any_just_pressed([MouseButton::Left, MouseButton::Right]) {
Lifecycle::Start
} else {
Lifecycle::Ongoing
};
let motion = if mouse_buttons.pressed(MouseButton::Left) {
CameraMotion::Orbit(delta)
} else {
CameraMotion::Zoom(delta.y)
};
input.send(InputAction(lifecycle, motion.into()));
}
// Stop Camera Orbit
else if mouse_buttons.just_released(MouseButton::Left) {
input.send(InputAction(
Lifecycle::Stop,
CameraMotion::Orbit(Vec2::ZERO).into(),
));
}
// Stop Camera Zoom
else if mouse_buttons.just_released(MouseButton::Right) {
input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into()));
}
}
// Stop Camera Orbit & Zoom
else if (keyboard.just_released(KeyCode::AltLeft) && !keyboard.pressed(KeyCode::AltRight))
|| (keyboard.just_released(KeyCode::AltRight) && !keyboard.pressed(KeyCode::AltLeft))
{
input.send(InputAction(
Lifecycle::Stop,
CameraMotion::Orbit(Vec2::ZERO).into(),
));
input.send(InputAction(Lifecycle::Stop, CameraMotion::Zoom(0.).into()));
}
// Camera Rotation
else if mouse_buttons.pressed(MouseButton::Right) {
let lifecycle = if mouse_buttons.just_pressed(MouseButton::Right) {
Lifecycle::Start
} else {
Lifecycle::Ongoing
};
let delta = read_mouse_delta(mouse_motion);
input.send(InputAction(lifecycle, CameraMotion::Rotate(delta).into()));
}
// Stop Camera Rotation
else if mouse_buttons.just_released(MouseButton::Right) {
input.send(InputAction(
Lifecycle::Stop,
CameraMotion::Rotate(Vec2::ZERO).into(),
))
}
// Camera Translation (FPS-like)
use KeyCode::*;
let wasd_keys = [W, A, S, D, E, Q];
let wasd_keys_set = wasd_keys.into_iter().collect::<HashSet<_>>();
// A little unfortunate that `get_pressed()` doesn't just return a reference
// to the original HashSet >_< Maybe the compiler will be nice to us and
// optimize away the redundant clone?
let keys_pressed = keyboard.get_pressed().cloned().collect::<HashSet<_>>();
let wasd_keys_pressed = wasd_keys_set
.intersection(&keys_pressed)
.collect::<HashSet<_>>();
if !wasd_keys_pressed.is_empty() {
let mut mvmt = Vec3::ZERO;
#[rustfmt::skip] {
if wasd_keys_pressed.contains(&W) { mvmt.z -= 1.; }
if wasd_keys_pressed.contains(&A) { mvmt.x -= 1.; }
if wasd_keys_pressed.contains(&S) { mvmt.z += 1.; }
if wasd_keys_pressed.contains(&D) { mvmt.x += 1.; }
if wasd_keys_pressed.contains(&E) { mvmt.y += 1.; }
if wasd_keys_pressed.contains(&Q) { mvmt.y -= 1.; }
};
let mvmt = mvmt.normalize_or_zero();
// Compare WASD keys pressed with WASD keys _just_ pressed to determine lifecycle
let keys_just_pressed = keyboard.get_just_pressed().cloned().collect::<HashSet<_>>();
let wasd_keys_just_pressed = keys_just_pressed
.intersection(&wasd_keys_set)
.collect::<HashSet<_>>();
let lifecycle = if wasd_keys_pressed.len() == wasd_keys_just_pressed.len() {
Lifecycle::Start
} else {
Lifecycle::Ongoing
};
input.send(InputAction(lifecycle, CameraMotion::Translate(mvmt).into()));
}
// Stop Translation
else if keyboard.any_just_released(wasd_keys) {
input.send(InputAction(
Lifecycle::Stop,
CameraMotion::Translate(Vec3::ZERO).into(),
));
}
}
#[inline]
fn read_mouse_delta(mut mouse_motion: EventReader<MouseMotion>) -> Vec2 {
mouse_motion
.into_iter()
.fold(Vec2::ZERO, |accum, current| accum + current.delta)
}