bevy_mod_observable_timer/lib.rs
1#![doc = include_str!("../README.md")]
2
3use core::{
4 ops::{Deref, DerefMut},
5 time::Duration,
6};
7
8use bevy::{
9 ecs::{
10 lifecycle::HookContext,
11 schedule::{InternedScheduleLabel, ScheduleLabel},
12 world::DeferredWorld,
13 },
14 prelude::*,
15};
16
17/// The [`SystemSet`] during which [`ObservableTimer`]s are updated.
18///
19/// Runs in [`Update`] by default, but this is configurable. See [`ObservableTimerPlugin::in_schedule()`].
20#[derive(SystemSet, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ObservableTimerSystems;
22
23/// This plugin provides functionality for the [`ObservableTimer`] component.
24///
25/// See the crate-level documentation for more information.
26pub struct ObservableTimerPlugin {
27 schedule: InternedScheduleLabel,
28}
29
30impl ObservableTimerPlugin {
31 /// Creates an `ObservableTimerPlugin` whose timers update in the given schedule.
32 ///
33 /// The default plugin updates in [`Update`].
34 ///
35 /// # Example
36 ///
37 /// ```
38 /// # use bevy::prelude::*;
39 /// # use bevy_mod_observable_timer::*;
40 /// # let mut app = App::new();
41 /// // Timers will be updated in `Last`
42 /// app.add_plugins(ObservableTimerPlugin::in_schedule(Last));
43 /// ```
44 pub fn in_schedule(schedule: impl ScheduleLabel) -> Self {
45 Self {
46 schedule: schedule.intern(),
47 }
48 }
49}
50
51impl Default for ObservableTimerPlugin {
52 fn default() -> Self {
53 Self::in_schedule(Update)
54 }
55}
56
57impl Plugin for ObservableTimerPlugin {
58 fn build(&self, app: &mut App) {
59 app.add_systems(
60 self.schedule,
61 update_observable_timers.in_set(ObservableTimerSystems),
62 );
63 }
64}
65
66/// Describes the behavior that should be taken by an [`ObservableTimer`] upon finishing.
67///
68/// # See also
69/// - [`ObservableTimer::with_finish_behavior()`]
70/// - [`ObservableTimer::finish_behavior`]
71#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
72pub enum TimerFinishBehavior {
73 /// Do nothing.
74 ///
75 /// Note that this will leave the `ObservableTimer` component in place, which means it will still be looped through
76 /// when updating timers.
77 None,
78 /// Remove only the `ObservableTimer` component.
79 RemoveComponent,
80 /// Despawn the entity that the `ObservableTimer` is attached to.
81 ///
82 /// This is the default behavior.
83 #[default]
84 DespawnEntity,
85}
86
87/// A timer component that triggers observable lifecycle events on its [`Entity`].
88///
89/// When an `ObservableTimer` is first added to an `Entity` (either by adding a new one, or replacing the current one)
90/// a [`TimerStarted`] event will be triggered. Then, each time an interval completes, a [`TimerFinished`] event will
91/// be triggered. Finally, when the timer component is removed, a [`TimerStopped`] event will be triggered.
92///
93/// By default a [`TimerMode::Once`] timer will despawn its `Entity` when it finishes. This behavior can be changed to
94/// removing only the `ObservableTimer` component, or doing nothing. See [`Self::with_finish_behavior`] for setting
95/// behavior at creation, or [`Self::finish_behavior`] for changing it after creation. Note that this behavior will
96/// not be run if the timer is removed manually before finishing.
97///
98/// To cancel a currently running timer simply remove the component. This will cause a [`TimerStopped`] event to be
99/// triggered.
100#[derive(Component, Debug, Clone)]
101#[component(on_remove = on_timer_removed)]
102pub struct ObservableTimer {
103 /// The internal [`Timer`].
104 pub timer: Timer,
105 /// The timer's [finish behavior](TimerFinishBehavior).
106 pub finish_behavior: TimerFinishBehavior,
107}
108
109impl ObservableTimer {
110 /// Create a new timer.
111 pub fn new(duration: Duration, mode: TimerMode) -> Self {
112 Self {
113 timer: Timer::new(duration, mode),
114 finish_behavior: TimerFinishBehavior::default(),
115 }
116 }
117
118 /// Create a new timer from a duration in seconds.
119 pub fn from_seconds(duration: f32, mode: TimerMode) -> Self {
120 Self {
121 timer: Timer::from_seconds(duration, mode),
122 finish_behavior: TimerFinishBehavior::default(),
123 }
124 }
125
126 /// Set the [`TimerFinishBehavior`] for this timer.
127 pub fn with_finish_behavior(self, finish_behavior: TimerFinishBehavior) -> Self {
128 Self {
129 finish_behavior,
130 ..self
131 }
132 }
133}
134
135impl Deref for ObservableTimer {
136 type Target = Timer;
137 fn deref(&self) -> &Self::Target {
138 &self.timer
139 }
140}
141
142impl DerefMut for ObservableTimer {
143 fn deref_mut(&mut self) -> &mut Self::Target {
144 &mut self.timer
145 }
146}
147
148/// A timer [`EntityEvent`] that is triggered when an [`ObservableTimer`] is inserted or spawned.
149#[derive(EntityEvent, Debug)]
150pub struct TimerStarted {
151 /// The timer [`Entity`].
152 #[entity_event]
153 pub entity: Entity,
154}
155
156/// A timer [`EntityEvent`] that is triggered when an [`ObservableTimer`] is removed or despawned.
157#[derive(EntityEvent, Debug)]
158pub struct TimerStopped {
159 /// The timer [`Entity`].
160 #[entity_event]
161 pub entity: Entity,
162 /// This is `true` for [`TimerMode::Once`] timers that finished normally, and removed or
163 /// despawned themselves.
164 pub finished: bool,
165}
166
167/// A timer [`EntityEvent`] that is triggered when an [`ObservableTimer`] finishes.
168#[derive(EntityEvent, Debug)]
169pub struct TimerFinished {
170 /// The timer [`Entity`].
171 #[entity_event]
172 pub entity: Entity,
173}
174
175fn on_timer_removed(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
176 let timer = world.get::<ObservableTimer>(entity).unwrap();
177 let finished = timer.mode() == TimerMode::Once && timer.is_finished();
178 world.commands().trigger(TimerStopped { entity, finished });
179}
180
181fn update_observable_timers(
182 time: Res<Time>,
183 mut timers: Query<(Entity, &mut ObservableTimer)>,
184 mut commands: Commands,
185) {
186 let delta = time.delta();
187 for (entity, mut timer) in timers.iter_mut() {
188 if timer.is_added() {
189 commands.trigger(TimerStarted { entity })
190 }
191
192 if timer.tick(delta).just_finished() {
193 for _ in 0..timer.times_finished_this_tick() {
194 commands.trigger(TimerFinished { entity });
195 }
196
197 if timer.mode() == TimerMode::Once {
198 match timer.finish_behavior {
199 TimerFinishBehavior::None => {}
200 TimerFinishBehavior::RemoveComponent => {
201 commands.entity(entity).remove::<ObservableTimer>();
202 }
203 TimerFinishBehavior::DespawnEntity => {
204 commands.entity(entity).despawn();
205 }
206 }
207 }
208 }
209 }
210}