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}