Skip to main content

mittens_engine/engine/ecs/system/
input_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::{
4    ComponentRef, ForwardAxis, InputComponent, InputTransformModeComponent, QueryRootMode,
5    RollAxis, TransformComponent, resolve_component_ref,
6};
7use crate::engine::ecs::system::System;
8use crate::engine::graphics::VisualWorld;
9use crate::engine::user_input::InputState;
10use crate::utils::math;
11use std::collections::HashMap;
12use winit::event::MouseButton;
13use winit::keyboard::{Key, NamedKey};
14
15/// System that processes input components and updates transforms based on WASD input.
16///
17/// Intended topology (simple one-way data flow):
18/// InputComponent -> TransformComponent -> (Camera2DComponent, RenderableComponent, ...)
19#[derive(Debug, Default)]
20pub struct InputSystem {
21    inputs: Vec<ComponentId>,
22
23    // FPS mode needs stable yaw/pitch/bank without per-frame extraction.
24    // Keyed by the controlled TransformComponent id.
25    fps_yaw_pitch_roll: HashMap<ComponentId, (f32, f32, f32)>,
26}
27
28impl InputSystem {
29    pub fn new() -> Self {
30        Self {
31            inputs: Vec::new(),
32            fps_yaw_pitch_roll: HashMap::new(),
33        }
34    }
35
36    /// Register an InputComponent.
37    pub fn register_input(&mut self, component: ComponentId) {
38        if !self.inputs.iter().any(|c| *c == component) {
39            self.inputs.push(component);
40        }
41    }
42
43    fn compute_rotation(
44        &self,
45        roll_axis: RollAxis,
46        input: &InputState,
47        dt_sec: f32,
48        rotation: &mut [f32; 4],
49    ) {
50        // Roll keys.
51        let q = input.key_down(&Key::Character("q".into()));
52        let e = input.key_down(&Key::Character("e".into()));
53
54        // Right-button drag rotates the rig (yaw + pitch).
55        let (drag_dx, drag_dy) = input.mouse_drag_delta_button(MouseButton::Right);
56
57        // Sensitivity is radians per pixel.
58        const MOUSE_SENS_RAD_PER_PX: f32 = 0.003;
59        let yaw_delta = drag_dx * MOUSE_SENS_RAD_PER_PX;
60        let pitch_delta = drag_dy * MOUSE_SENS_RAD_PER_PX;
61
62        // Relative/flight-style semantics: apply local incremental rotations.
63        if yaw_delta != 0.0 {
64            let q_yaw = math::quat_from_axis_angle([0.0, 1.0, 0.0], yaw_delta);
65            *rotation = math::quat_mul(*rotation, q_yaw);
66        }
67        if pitch_delta != 0.0 {
68            let q_pitch = math::quat_from_axis_angle([1.0, 0.0, 0.0], pitch_delta);
69            *rotation = math::quat_mul(*rotation, q_pitch);
70        }
71
72        if q || e {
73            const ROT_SPEED_RAD_PER_SEC: f32 = 1.5;
74            let dir = (q as i32) as f32 - (e as i32) as f32;
75            let dtheta = dir * ROT_SPEED_RAD_PER_SEC * dt_sec;
76            let axis = match roll_axis {
77                RollAxis::X => [1.0, 0.0, 0.0],
78                RollAxis::Y => [0.0, 1.0, 0.0],
79                RollAxis::Z => [0.0, 0.0, 1.0],
80            };
81            let q_roll = math::quat_from_axis_angle(axis, dtheta);
82            *rotation = math::quat_mul(*rotation, q_roll);
83        }
84    }
85
86    fn compute_rotation_fps(
87        &mut self,
88        transform_cid: ComponentId,
89        roll_axis: RollAxis,
90        input: &InputState,
91        dt_sec: f32,
92        rotation: &mut [f32; 4],
93    ) {
94        // Roll keys.
95        let q = input.key_down(&Key::Character("q".into()));
96        let e = input.key_down(&Key::Character("e".into()));
97
98        // Right-button drag rotates the rig (yaw + pitch).
99        let (drag_dx, drag_dy) = input.mouse_drag_delta_button(MouseButton::Right);
100
101        // Sensitivity is radians per pixel.
102        const MOUSE_SENS_RAD_PER_PX: f32 = 0.003;
103        let yaw_delta = drag_dx * MOUSE_SENS_RAD_PER_PX;
104        let pitch_delta = drag_dy * MOUSE_SENS_RAD_PER_PX;
105
106        // Allow Q/E rotation even without mouse drag.
107        let qe_delta = if q || e {
108            const ROT_SPEED_RAD_PER_SEC: f32 = 1.5;
109            let dir = (q as i32) as f32 - (e as i32) as f32;
110            dir * ROT_SPEED_RAD_PER_SEC * dt_sec
111        } else {
112            0.0
113        };
114
115        if yaw_delta == 0.0 && pitch_delta == 0.0 && qe_delta == 0.0 {
116            return;
117        }
118
119        // Initialize once from current rotation.
120        let (mut yaw, mut pitch, mut roll) = self
121            .fps_yaw_pitch_roll
122            .get(&transform_cid)
123            .copied()
124            .unwrap_or_else(|| {
125                let right =
126                    math::vec3_normalize(math::quat_rotate_vec3(*rotation, [1.0, 0.0, 0.0]));
127                let fwd = math::vec3_normalize(math::quat_rotate_vec3(*rotation, [0.0, 0.0, -1.0]));
128
129                // Yaw is global (world up): angle around +Y.
130                let yaw = right[2].atan2(right[0]);
131                // Pitch comes from forward Y.
132                let pitch = fwd[1].clamp(-1.0, 1.0).asin();
133                // Note: roll isn't extracted currently; it starts at 0 and is preserved
134                // once the user rolls with Q/E.
135                (yaw, pitch, 0.0)
136            });
137
138        // Apply deltas.
139        yaw += yaw_delta;
140        pitch += pitch_delta;
141
142        // Q/E rotates around the configured axis.
143        match roll_axis {
144            RollAxis::Y => yaw += qe_delta,
145            RollAxis::X => pitch += qe_delta,
146            RollAxis::Z => roll += qe_delta,
147        }
148
149        const MAX_PITCH: f32 = 1.55; // ~88.8deg
150        pitch = pitch.clamp(-MAX_PITCH, MAX_PITCH);
151
152        // Persist state.
153        self.fps_yaw_pitch_roll
154            .insert(transform_cid, (yaw, pitch, roll));
155
156        // Rebuild rotation from yaw/pitch/bank.
157        // Yaw: global axis. Pitch: relative to yaw (around yaw-rotated right).
158        // Bank (roll): around camera-forward axis.
159        let q_yaw = math::quat_from_axis_angle([0.0, 1.0, 0.0], yaw);
160        let right = math::quat_rotate_vec3(q_yaw, [1.0, 0.0, 0.0]);
161        let q_pitch = math::quat_from_axis_angle(right, pitch);
162
163        let q_base = math::quat_mul(q_pitch, q_yaw);
164        let fwd_world = math::vec3_normalize(math::quat_rotate_vec3(q_base, [0.0, 0.0, -1.0]));
165        let q_bank = math::quat_from_axis_angle(fwd_world, roll);
166
167        *rotation = math::quat_mul(q_bank, q_base);
168    }
169
170    fn compute_translation(
171        &self,
172        forward_axis: ForwardAxis,
173        fps_rotation: bool,
174        fps_yaw: Option<f32>,
175        speed_units_per_sec: f32,
176        input: &InputState,
177        dt_sec: f32,
178        rotation: [f32; 4],
179        translation: &mut [f32; 3],
180    ) {
181        // Read movement keys.
182        let w = input.key_down(&Key::Character("w".into()));
183        let a = input.key_down(&Key::Character("a".into()));
184        let s = input.key_down(&Key::Character("s".into()));
185        let d = input.key_down(&Key::Character("d".into()));
186        let r: bool = input.key_down(&Key::Character("r".into()));
187        let f: bool = input.key_down(&Key::Character("f".into()));
188
189        // Holding Shift increases movement speed.
190        let speed_multiplier = if input.key_down(&Key::Named(NamedKey::Shift)) {
191            3.0
192        } else {
193            1.0
194        };
195
196        let speed = speed_units_per_sec * speed_multiplier * dt_sec;
197
198        match forward_axis {
199            ForwardAxis::Y => {
200                // Legacy 2D-style translation delta (x/y).
201                let mut dx = 0.0f32;
202                let mut dy = 0.0f32;
203
204                if w {
205                    dy += 1.0;
206                }
207                if s {
208                    dy -= 1.0;
209                }
210                if a {
211                    dx -= 1.0;
212                }
213                if d {
214                    dx += 1.0;
215                }
216
217                // Normalize diagonal movement.
218                let len = (dx * dx + dy * dy).sqrt();
219                if len > 0.0 {
220                    dx /= len;
221                    dy /= len;
222                }
223
224                // Translate in the transform's local (rotated) axes.
225                let v = math::quat_rotate_vec3(rotation, [dx, dy, 0.0]);
226                translation[0] += v[0] * speed;
227                translation[1] += v[1] * speed;
228            }
229
230            ForwardAxis::Z => {
231                let mut dx = 0.0f32;
232                let mut dy: f32 = 0.0f32;
233                let mut dz = 0.0f32;
234
235                if a {
236                    dx -= 1.0;
237                }
238                if d {
239                    dx += 1.0;
240                }
241                if r {
242                    dy += 1.0;
243                }
244                if f {
245                    dy -= 1.0;
246                }
247                if w {
248                    dz -= 1.0;
249                }
250                if s {
251                    dz += 1.0;
252                }
253
254                // Normalize diagonal movement.
255                let len = (dx * dx + dy * dy + dz * dz).sqrt();
256                if len > 0.0 {
257                    dx /= len;
258                    dy /= len;
259                    dz /= len;
260                }
261
262                if fps_rotation {
263                    // FPS: yaw drives horizontal movement; pitch doesn't.
264                    let yaw = fps_yaw.unwrap_or_else(|| {
265                        let right = math::quat_rotate_vec3(rotation, [1.0, 0.0, 0.0]);
266                        right[2].atan2(right[0])
267                    });
268                    let q_yaw = math::quat_from_axis_angle([0.0, 1.0, 0.0], yaw);
269                    let v = math::quat_rotate_vec3(q_yaw, [dx, 0.0, dz]);
270                    translation[0] += v[0] * speed;
271                    translation[1] += dy * speed;
272                    translation[2] += v[2] * speed;
273                } else {
274                    // Flight/relative: full rotation drives movement.
275                    let v = math::quat_rotate_vec3(rotation, [dx, dy, dz]);
276                    translation[0] += v[0] * speed;
277                    translation[1] += v[1] * speed;
278                    translation[2] += v[2] * speed;
279                }
280            }
281        }
282    }
283
284    fn resolve_translation_basis_rotation(
285        &self,
286        world: &World,
287        mode_component: Option<ComponentId>,
288        source: Option<&ComponentRef>,
289        fallback_rotation: [f32; 4],
290    ) -> [f32; 4] {
291        let Some(source) = source else {
292            return fallback_rotation;
293        };
294        let Some(target) =
295            resolve_component_ref(world, source, mode_component, QueryRootMode::SelfSubtree)
296        else {
297            return fallback_rotation;
298        };
299        self.nearest_transform_world_rotation(world, target)
300            .unwrap_or(fallback_rotation)
301    }
302
303    fn nearest_transform_world_rotation(
304        &self,
305        world: &World,
306        start: ComponentId,
307    ) -> Option<[f32; 4]> {
308        let mut current = Some(start);
309        while let Some(component) = current {
310            if let Some(transform) = world.get_component_by_id_as::<TransformComponent>(component) {
311                return Some(math::mat_to_quat(transform.transform.matrix_world));
312            }
313            current = world.parent_of(component);
314        }
315        None
316    }
317
318    /// Process input and queue at most one transform update per InputComponent.
319    ///
320    /// This only supports the intended topology:
321    /// InputComponent -> TransformComponent (child)
322    pub fn process_input(
323        &mut self,
324        world: &mut World,
325        input: &InputState,
326        emit: &mut dyn crate::engine::ecs::SignalEmitter,
327        dt_sec: f32,
328    ) {
329        // We gate early to avoid scanning inputs if nothing relevant is pressed.
330        let any_move = input.key_down(&Key::Character("w".into()))
331            || input.key_down(&Key::Character("a".into()))
332            || input.key_down(&Key::Character("s".into()))
333            || input.key_down(&Key::Character("d".into()))
334            || input.key_down(&Key::Character("r".into()))
335            || input.key_down(&Key::Character("f".into()))
336            || input.key_down(&Key::Character("q".into()))
337            || input.key_down(&Key::Character("e".into()));
338
339        let any_drag = input.mouse_dragging_button(MouseButton::Right);
340
341        if !any_move && !any_drag {
342            return;
343        }
344
345        let inputs = self.inputs.clone();
346        for input_cid in inputs {
347            let speed_units_per_sec =
348                match world.get_component_by_id_as::<InputComponent>(input_cid) {
349                    Some(input_comp) => input_comp.speed,
350                    None => continue,
351                };
352
353            // Find TransformComponent child. If absent, we don't compute.
354            let transform_child = world.children_of(input_cid).iter().copied().find(|&cid| {
355                world
356                    .get_component_by_id_as::<TransformComponent>(cid)
357                    .is_some()
358            });
359
360            // Optional mode child.
361            let (
362                mode_component,
363                forward_axis,
364                roll_axis,
365                rotation_enabled,
366                fps_rotation,
367                translation_basis_source,
368            ) = world
369                .children_of(input_cid)
370                .iter()
371                .copied()
372                .find_map(|cid| {
373                    world
374                        .get_component_by_id_as::<InputTransformModeComponent>(cid)
375                        .map(|m| {
376                            (
377                                Some(cid),
378                                m.forward_axis,
379                                m.roll_axis,
380                                m.rotation_enabled,
381                                m.fps_rotation,
382                                m.translation_basis_source.clone(),
383                            )
384                        })
385                })
386                .unwrap_or((None, ForwardAxis::Y, RollAxis::Z, true, false, None));
387
388            let Some(transform_cid) = transform_child else {
389                continue;
390            };
391
392            let external_basis_rotation = translation_basis_source.as_ref().map(|source| {
393                self.resolve_translation_basis_rotation(
394                    world,
395                    mode_component,
396                    Some(source),
397                    [0.0, 0.0, 0.0, 1.0],
398                )
399            });
400
401            if let Some(transform_comp_mut) =
402                world.get_component_by_id_as_mut::<TransformComponent>(transform_cid)
403            {
404                if rotation_enabled && fps_rotation {
405                    self.compute_rotation_fps(
406                        transform_cid,
407                        roll_axis,
408                        input,
409                        dt_sec,
410                        &mut transform_comp_mut.transform.rotation,
411                    );
412                } else if rotation_enabled {
413                    self.compute_rotation(
414                        roll_axis,
415                        input,
416                        dt_sec,
417                        &mut transform_comp_mut.transform.rotation,
418                    );
419                }
420                let fps_yaw = if fps_rotation {
421                    self.fps_yaw_pitch_roll
422                        .get(&transform_cid)
423                        .map(|(y, _, _)| *y)
424                } else {
425                    None
426                };
427                let translation_basis_rotation =
428                    external_basis_rotation.unwrap_or(transform_comp_mut.transform.rotation);
429                self.compute_translation(
430                    forward_axis,
431                    fps_rotation,
432                    fps_yaw,
433                    speed_units_per_sec,
434                    input,
435                    dt_sec,
436                    translation_basis_rotation,
437                    &mut transform_comp_mut.transform.translation,
438                );
439
440                transform_comp_mut.transform.recompute_model();
441                emit.push_intent_now(
442                    transform_cid,
443                    crate::engine::ecs::IntentValue::UpdateTransform {
444                        component_ids: vec![transform_cid],
445                        translation: transform_comp_mut.transform.translation,
446                        rotation_quat_xyzw: transform_comp_mut.transform.rotation,
447                        scale: transform_comp_mut.transform.scale,
448                    },
449                );
450            }
451        }
452    }
453}
454
455impl System for InputSystem {
456    fn tick(
457        &mut self,
458        _world: &mut World,
459        _visuals: &mut VisualWorld,
460        _input: &InputState,
461        _dt_sec: f32,
462    ) {
463        // InputSystem is driven by SystemWorld::tick calling process_input with a CommandQueue.
464    }
465}