engine/
input.rs

1// SPDX-FileCopyrightText: 2024 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5use core::time::Duration;
6
7use arrayvec::ArrayVec;
8use platform::{Button, Event, InputDevice, Instant};
9
10/// The amount of time [`QueuedEvent`]s are held in the [`EventQueue`] without
11/// being handled.
12pub const EVENT_QUEUE_TIMEOUT: Duration = Duration::from_millis(200);
13
14/// A queue of input events to be processed by [`InputDeviceState::update`].
15pub type EventQueue = ArrayVec<QueuedEvent, 1000>;
16
17/// Input event that happened at some point in the past, waiting to be used as a
18/// trigger for an [`ActionState`], or to be timed out.
19pub struct QueuedEvent {
20    /// The event itself.
21    pub event: Event,
22    /// Timestamp of when the event happened.
23    pub timestamp: Instant,
24}
25
26impl QueuedEvent {
27    /// Returns true if the time between this event and the given timestamp is
28    /// greater than [`EVENT_QUEUE_TIMEOUT`].
29    pub fn timed_out(&self, timestamp: Instant) -> bool {
30        if let Some(time_since_event) = timestamp.duration_since(self.timestamp) {
31            time_since_event >= EVENT_QUEUE_TIMEOUT
32        } else {
33            false
34        }
35    }
36}
37
38/// The main input interface for the game, created, and maintained in game code.
39///
40/// `N` should be the amount of actions that can be triggered by the player to
41/// be detected by the game. Every frame, [`InputDeviceState::update`] should be
42/// called, and then the [`ActionState::pressed`] status of the values in
43/// [`InputDeviceState::actions`] should be used to trigger any relevant events
44/// in the game based on the actions' index.
45///
46/// ### Example
47/// ```
48/// # let mut event_queue = engine::input::EventQueue::new();
49/// # let a_device_from_platform = platform::InputDevice::new(0);
50/// # let button_from_platform = platform::Button::new(0);
51/// # let another_button_from_platform = platform::Button::new(0);
52/// use engine::input::{InputDeviceState, ActionState, ActionKind};
53///
54/// #[repr(usize)]
55/// enum PlayerAction {
56///     Jump,
57///     Run,
58///     Select,
59///     _Count,
60/// }
61///
62/// let mut input_device_state = InputDeviceState {
63///     device: a_device_from_platform,
64///     actions: [ActionState::default(); PlayerAction::_Count as usize],
65/// };
66///
67/// // Maybe bind the actions:
68/// let jump_action = &mut input_device_state.actions[PlayerAction::Jump as usize];
69/// jump_action.kind = ActionKind::Instant;
70/// jump_action.mapping = Some(button_from_platform);
71///
72/// let run_action = &mut input_device_state.actions[PlayerAction::Run as usize];
73/// run_action.kind = ActionKind::Held;
74/// run_action.mapping = Some(another_button_from_platform);
75///
76/// // Somewhere early in a frame:
77/// input_device_state.update(&mut event_queue);
78/// if input_device_state.actions[PlayerAction::Jump as usize].pressed {
79///     // Jump!
80/// }
81/// ```
82pub struct InputDeviceState<const N: usize> {
83    /// The device this [`InputDeviceState`] tracks.
84    pub device: InputDevice,
85    /// Each action's current state, updated based on events in
86    /// [`InputDeviceState::update`].
87    pub actions: [ActionState; N],
88}
89
90impl<const N: usize> InputDeviceState<N> {
91    /// Checks the event queue for any events that could be consumed by this
92    /// [`InputDeviceState`], and consumes any such events to trigger actions.
93    ///
94    /// Also resets the [`ActionState::pressed`] status of
95    /// [`ActionKind::Instant`] actions.
96    pub fn update(&mut self, event_queue: &mut EventQueue) {
97        // Reset any instant actions to "not pressed"
98        for action in &mut self.actions {
99            if matches!(action.kind, ActionKind::Instant) {
100                action.pressed = false;
101            }
102        }
103
104        // Handle events, removing events from the queue if they triggered an action
105        event_queue.retain(|event| {
106            match event.event {
107                Event::DigitalInputPressed(device, button) if device == self.device => {
108                    for action in &mut self.actions {
109                        if action.mapping == Some(button) && !action.disabled {
110                            match action.kind {
111                                ActionKind::Instant if action.pressed => return true, // handle this event on the next frame, there's many presses queued up
112                                ActionKind::Instant => action.pressed = true,
113                                ActionKind::Held => action.pressed = true,
114                                ActionKind::Toggle => action.pressed = !action.pressed,
115                            }
116                            return false;
117                        }
118                    }
119                }
120
121                Event::DigitalInputReleased(device, button) if device == self.device => {
122                    for action in &mut self.actions {
123                        if action.mapping == Some(button) && !action.disabled {
124                            if matches!(action.kind, ActionKind::Held) {
125                                action.pressed = false;
126                            }
127                            return false;
128                        }
129                    }
130                }
131
132                _ => return true,
133            }
134            true
135        });
136    }
137}
138
139/// A rebindable action and its current state.
140#[derive(Clone, Copy, Default)]
141pub struct ActionState {
142    /// How events are used to change the status of [`ActionState::pressed`].
143    pub kind: ActionKind,
144    /// Button which triggers this action.
145    pub mapping: Option<Button>,
146    /// If true, events are ignored, but unless the events time out, they will
147    /// trigger the action once this is set to false again.
148    ///
149    /// Can be used to e.g. disable jumping while in-air, but still cause a jump
150    /// trigger if the player pressed the button right before landing.
151    pub disabled: bool,
152    /// True if the action should be triggered based on input events, parsed
153    /// according to the action's [`ActionKind`].
154    pub pressed: bool,
155}
156
157/// The button press pattern to be used to trigger a specific action.
158#[derive(Clone, Copy, Default)]
159pub enum ActionKind {
160    /// Actions that happen right away when the button is pressed, and stop
161    /// happening until the next press.
162    #[default]
163    Instant,
164    /// Actions that start happening when the button is pressed, and stop
165    /// happening when it's released.
166    Held,
167    /// (Accessible alternative for [`ActionKind::Held`], gameplay logic shouldn't
168    /// really change between these two.) Actions that start happening when the
169    /// button is pressed one time, and stop happening when it's pressed again.
170    Toggle,
171}