1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//! Zero-allocation application state machine inspired by `bevy_state`.
//!
//! Embedded titles transition through high-level phases (e.g. `Boot`,
//! `TitleScreen`, `Gameplay`, `Pause`, `GameOver`).
//!
//! This module provides a deterministic, zero-heap state machine wrapper around
//! any `Copy + PartialEq` enum. It exposes change detection (`just_entered`,
//! `just_exited`) so systems can run one-shot enter/exit routines without
//! global boolean flags.
use core::fmt::Debug;
/// Finite application state tracker with frame change detection.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StateMachine<S: Copy + PartialEq> {
current: S,
previous: S,
changed: bool,
}
impl<S: Copy + PartialEq> StateMachine<S> {
/// Create a state machine initialized to `initial`.
///
/// On the very first frame before `update()` is called, `changed` is `true`
/// and `just_entered(initial)` returns `true`.
pub const fn new(initial: S) -> Self {
Self {
current: initial,
previous: initial,
changed: true,
}
}
/// The currently active state.
#[inline]
pub const fn current(&self) -> S {
self.current
}
/// The state that was active on the preceding tick.
#[inline]
pub const fn previous(&self) -> S {
self.previous
}
/// Returns `true` if currently in `state`.
#[inline]
pub fn is(&self, state: S) -> bool {
self.current == state
}
/// Returns `true` if the state transitioned *into* `state` during this frame tick.
#[inline]
pub fn just_entered(&self, state: S) -> bool {
self.changed && self.current == state
}
/// Returns `true` if the state transitioned *out of* `state` during this frame tick.
#[inline]
pub fn just_exited(&self, state: S) -> bool {
self.changed && self.previous == state && self.current != state
}
/// Returns `true` if a state transition occurred on this frame tick.
#[inline]
pub const fn has_changed(&self) -> bool {
self.changed
}
/// Request a transition to `next`.
///
/// If `next` equals the current state, this is a no-op and returns `false`.
/// Otherwise, sets the next state, marks `changed = true`, and returns `true`.
pub fn set(&mut self, next: S) -> bool {
if self.current == next {
false
} else {
self.previous = self.current;
self.current = next;
self.changed = true;
true
}
}
/// Advance to the next frame tick.
///
/// Clears the `changed` flag and synchronizes `previous = current`.
/// Call this once per frame in your main loop.
#[inline]
pub fn update(&mut self) {
self.changed = false;
self.previous = self.current;
}
}
/// Container that holds data only active while the state machine matches `target_state`.
#[derive(Clone, Copy, Debug, Default)]
pub struct StateScoped<T, S: Copy + PartialEq> {
data: Option<T>,
target_state: S,
}
impl<T, S: Copy + PartialEq> StateScoped<T, S> {
/// Create a container scoped to `target_state`.
pub const fn new(target_state: S) -> Self {
Self {
data: None,
target_state,
}
}
/// Put data into the container.
pub fn set(&mut self, value: T) {
self.data = Some(value);
}
/// Access data if the state machine is currently in `target_state`.
pub fn get<'a>(&'a self, sm: &StateMachine<S>) -> Option<&'a T> {
if sm.is(self.target_state) {
self.data.as_ref()
} else {
None
}
}
/// Mutably access data if the state machine is currently in `target_state`.
pub fn get_mut<'a>(&'a mut self, sm: &StateMachine<S>) -> Option<&'a mut T> {
if sm.is(self.target_state) {
self.data.as_mut()
} else {
None
}
}
/// Automatically clears internal data when exiting `target_state`.
pub fn update(&mut self, sm: &StateMachine<S>) {
if sm.just_exited(self.target_state) {
self.data = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GameState {
Title,
Playing,
GameOver,
}
#[test]
fn test_state_machine_lifecycle() {
let mut sm = StateMachine::new(GameState::Title);
assert!(sm.is(GameState::Title));
assert!(sm.just_entered(GameState::Title));
assert!(!sm.just_exited(GameState::Title));
// Tick 1
sm.update();
assert!(sm.is(GameState::Title));
assert!(!sm.just_entered(GameState::Title));
assert!(!sm.has_changed());
// Transition to Playing
assert!(sm.set(GameState::Playing));
assert!(sm.is(GameState::Playing));
assert!(sm.just_entered(GameState::Playing));
assert!(sm.just_exited(GameState::Title));
assert!(sm.has_changed());
// Redundant set
assert!(!sm.set(GameState::Playing));
// Tick 2
sm.update();
assert!(sm.is(GameState::Playing));
assert!(!sm.just_entered(GameState::Playing));
assert!(!sm.just_exited(GameState::Title));
// Transition to GameOver
sm.set(GameState::GameOver);
assert!(sm.just_entered(GameState::GameOver));
assert!(sm.just_exited(GameState::Playing));
}
#[test]
fn test_state_scoped() {
let mut sm = StateMachine::new(GameState::Title);
let mut playing_data = StateScoped::new(GameState::Playing);
playing_data.set(42);
// While in Title, data is not accessible
assert_eq!(playing_data.get(&sm), None);
// Transition to Playing
sm.set(GameState::Playing);
assert_eq!(playing_data.get(&sm), Some(&42));
// Transition to GameOver and update
sm.set(GameState::GameOver);
playing_data.update(&sm);
sm.update();
// Data should have been cleared on exit
assert_eq!(playing_data.get(&sm), None);
}
}