bevy_tnua/basis_action_traits.rs
1use std::fs::File;
2use std::io::ErrorKind;
3use std::path::Path;
4use std::time::Duration;
5
6use crate::TnuaMotor;
7use crate::action_state::TnuaActionStateInterface;
8use crate::ghost_overrides::TnuaGhostOverwrite;
9use crate::sensor_sets::TnuaSensors;
10use bevy::prelude::*;
11use bevy::time::Stopwatch;
12use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGhostSensor;
13use bevy_tnua_physics_integration_layer::data_for_backends::TnuaProximitySensor;
14use bevy_tnua_physics_integration_layer::data_for_backends::TnuaRigidBodyTracker;
15use serde::Deserialize;
16use serde::Serialize;
17
18use crate::math::*;
19
20/// See [the derive macro](bevy_tnua_macros::TnuaScheme).
21pub trait TnuaScheme: 'static + Send + Sync + Sized {
22 /// The base motion controller of the character. Typically
23 /// [`TnuaBuiltinWalk`](crate::builtins::TnuaBuiltinWalk)
24 type Basis: TnuaBasis;
25 /// A big configuration struct, containing the configuration of the basis and of all the
26 /// actions.
27 type Config: TnuaSchemeConfig<Scheme = Self> + Asset;
28 /// An enum with a unit variant for each variant of the control scheme.
29 type ActionDiscriminant: TnuaActionDiscriminant;
30 /// An enum mirroring the control scheme, except instead of just having the input of each
31 /// action each variant has a [`TnuaActionState`] which contains the action's input,
32 /// configuration and memoery. If the variant in the control scheme has payload, they are
33 /// copied to the variant in action state as is.
34 type ActionState: TnuaActionState<Basis = Self::Basis, Discriminant = Self::ActionDiscriminant>;
35
36 /// The action without the input and payloads.
37 fn discriminant(&self) -> Self::ActionDiscriminant;
38
39 #[doc(hidden)]
40 fn into_action_state_variant(self, config: &Self::Config) -> Self::ActionState;
41
42 #[doc(hidden)]
43 fn update_in_action_state(
44 self,
45 action_state_enum: &mut Self::ActionState,
46 ) -> TnuaUpdateInActionStateResult<Self>;
47
48 #[doc(hidden)]
49 fn initiation_decision(
50 &self,
51 config: &Self::Config,
52 sensors: &<Self::Basis as TnuaBasis>::Sensors<'_>,
53 ctx: TnuaActionContext<Self::Basis>,
54 being_fed_for: &Stopwatch,
55 ) -> TnuaActionInitiationDirective;
56}
57
58#[doc(hidden)]
59pub enum TnuaUpdateInActionStateResult<S: TnuaScheme> {
60 Success,
61 WrongVariant(S),
62}
63
64/// A big configuration struct, containing the configuration of the basis and of all the actions of
65/// a control scheme.
66pub trait TnuaSchemeConfig: Serialize + for<'de> Deserialize<'de> {
67 /// The control scheme this configuration belongs to.
68 type Scheme: TnuaScheme<Config = Self>;
69
70 #[doc(hidden)]
71 fn basis_config(&self) -> &<<Self::Scheme as TnuaScheme>::Basis as TnuaBasis>::Config;
72
73 /// Use to create a template of the configuration in the assets directory.
74 ///
75 /// The call to this function should be removed after the file is created, to avoid checking it
76 /// on every run and to avoid breaking WASM (which cannot access the assets directory using the
77 /// filesystem)
78 fn write_if_not_exist(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
79 let serialized = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::new())
80 .expect("Should be able to serialize all configs to RON");
81 let file = File::options().write(true).create_new(true).open(path);
82 if let Err(err) = &file
83 && err.kind() == ErrorKind::AlreadyExists
84 {
85 return Ok(());
86 }
87 use std::io::Write;
88 write!(file?, "{}", serialized)
89 }
90}
91
92/// Various data passed to [`TnuaBasis::apply`].
93#[derive(Debug)]
94pub struct TnuaBasisContext<'a> {
95 /// The duration of the current frame.
96 pub frame_duration: Float,
97
98 /// A sensor that collects data about the rigid body from the physics backend.
99 pub tracker: &'a TnuaRigidBodyTracker,
100
101 /// The direction considered as "up".
102 pub up_direction: Dir3,
103}
104
105/// The main movement command of a character.
106///
107/// A basis handles the character's motion when the user is not feeding it any input, or when it
108/// just moves around without doing anything special. A simple game would only need one basis -
109/// [`TnuaBuiltinWalk`](crate::builtins::TnuaBuiltinWalk) - but more complex games can have bases
110/// for things like swimming or driving.
111///
112/// The type that implements this trait is called the basis _input_, and is expected to be
113/// overwritten each frame by the controller system of the game code. Configuration is considered
114/// as part of the input. Configuration is stored in an asset, as part of a struct implementing
115/// [`TnuaSchemeConfig`] which also holds the configuration for all the actions. If the basis needs
116/// to persist data between frames it must keep it in its [memory](TnuaBasis::Memory).
117pub trait TnuaBasis: Default + 'static + Send + Sync {
118 type Config: Send + Sync + Clone + Serialize + for<'de> Deserialize<'de>;
119 type Memory: Send + Sync + Default;
120 type Sensors<'a>: TnuaSensors<'a>;
121
122 /// This is where the basis affects the character's motion.
123 ///
124 /// This method gets called each frame to let the basis control the [`TnuaMotor`] that will
125 /// later move the character.
126 ///
127 /// Note that after the motor is set in this method, if there is an action going on, the
128 /// action's [`apply`](TnuaAction::apply) will also run and typically change some of the things
129 /// the basis did to the motor.
130 ///
131 /// It can also update the memory.
132 fn apply(
133 &self,
134 config: &Self::Config,
135 memory: &mut Self::Memory,
136 sensors: &Self::Sensors<'_>,
137 ctx: TnuaBasisContext,
138 motor: &mut TnuaMotor,
139 );
140
141 /// This is where the basis initiates its sensors.
142 ///
143 /// * Use the helper
144 /// [`ProximitySensorPreparationHelper`](crate::sensor_sets::ProximitySensorPreparationHelper)
145 /// to create the sensors.
146 /// * Return `None` if - and only if - some _essential_ sensors are missing, because it'll
147 /// cause the controller to skip frames until it returns `Some`.
148 ///
149 /// An example of non-essential sensor is the headroom sensor in
150 /// [`TnuaBuiltinWalkSensors`](crate::builtins::TnuaBuiltinWalkSensors) - the controller can
151 /// function without it (it just won't be able to do crouch enforcement) so if it's absence
152 /// does not cause prevent the controller from running (unlike the `ground` sensor, which is
153 /// essential)
154 /// * Even if some essential sensors are missing, make sure to prepare _all_ the sensors that
155 /// need preparation before returning `None`. No point preparing one sensor per frame.
156 /// * If a sensor exists but wrongly configured - launch the command to reconfigure it and
157 /// return the existing sensor. Unless the configuration is totally off (e.g. - sensor points
158 /// in a very wrong direction) it's probably better to use the misconfigured one than to skip
159 /// a frame.
160 #[allow(clippy::too_many_arguments)]
161 fn get_or_create_sensors<'a: 'b, 'b>(
162 up_direction: Dir3,
163 config: &'a Self::Config,
164 memory: &Self::Memory,
165 entities: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
166 proximity_sensors_query: &'b Query<(&TnuaProximitySensor, Has<TnuaGhostSensor>)>,
167 controller_entity: Entity,
168 commands: &mut Commands,
169 has_ghost_overwrites: bool,
170 ) -> Option<Self::Sensors<'b>>;
171
172 /// Iterate over each ghost overwrite and the sensor entity of the relevant sensor.
173 ///
174 /// Note that not all sensors need to have ghost overwrites.
175 fn ghost_sensor_overwrites<'a>(
176 ghost_overwrites: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::GhostOverwrites,
177 entities: &<Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
178 ) -> impl Iterator<Item = (&'a mut TnuaGhostOverwrite, Entity)>;
179}
180
181/// Input for [`TnuaAction::apply`] that informs it about the long-term feeding of the input.
182#[derive(PartialEq, Eq, Debug, Clone, Copy)]
183pub enum TnuaActionLifecycleStatus {
184 /// There was no action in the previous frame
185 Initiated,
186 /// There was a different action in the previous frame
187 CancelledFrom,
188 /// This action was already active in the previous frame, and it keeps getting fed
189 StillFed,
190 /// This action was fed up until the previous frame, and now no action is fed
191 NoLongerFed,
192 /// This action was fed up until the previous frame, and now a different action tries to override it
193 CancelledInto,
194}
195
196impl TnuaActionLifecycleStatus {
197 /// Continue if the action is still fed, finish if its not fed or if some other action gets
198 /// fed.
199 pub fn directive_simple(&self) -> TnuaActionLifecycleDirective {
200 match self {
201 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
202 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
203 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
204 TnuaActionLifecycleStatus::NoLongerFed => TnuaActionLifecycleDirective::Finished,
205 TnuaActionLifecycleStatus::CancelledInto => TnuaActionLifecycleDirective::Finished,
206 }
207 }
208
209 /// Similar to [`directive_simple`](Self::directive_simple), but if some other action gets fed
210 /// and this action is still being fed, reschedule this action once the other action finishes,
211 /// as long as more time than `after_seconds` has passed.
212 pub fn directive_simple_reschedule(
213 &self,
214 after_seconds: Float,
215 ) -> TnuaActionLifecycleDirective {
216 match self {
217 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
218 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
219 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
220 TnuaActionLifecycleStatus::NoLongerFed => {
221 // The rescheduling will probably go away, but in case things happen too fast and
222 // it doesn't - pass it anyway.
223 TnuaActionLifecycleDirective::Reschedule { after_seconds }
224 }
225 TnuaActionLifecycleStatus::CancelledInto => {
226 TnuaActionLifecycleDirective::Reschedule { after_seconds }
227 }
228 }
229 }
230
231 /// Continue - unless the action is cancelled into another action.
232 pub fn directive_linger(&self) -> TnuaActionLifecycleDirective {
233 match self {
234 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
235 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
236 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
237 TnuaActionLifecycleStatus::NoLongerFed => TnuaActionLifecycleDirective::StillActive,
238 TnuaActionLifecycleStatus::CancelledInto => TnuaActionLifecycleDirective::Finished,
239 }
240 }
241
242 /// Determine if the action just started, whether from no action or to replace another action.
243 pub fn just_started(&self) -> bool {
244 match self {
245 TnuaActionLifecycleStatus::Initiated => true,
246 TnuaActionLifecycleStatus::CancelledFrom => true,
247 TnuaActionLifecycleStatus::StillFed => false,
248 TnuaActionLifecycleStatus::NoLongerFed => false,
249 TnuaActionLifecycleStatus::CancelledInto => false,
250 }
251 }
252
253 /// Determine if the action is currently active - still fed and not replaced by another.
254 pub fn is_active(&self) -> bool {
255 match self {
256 TnuaActionLifecycleStatus::Initiated => true,
257 TnuaActionLifecycleStatus::CancelledFrom => true,
258 TnuaActionLifecycleStatus::StillFed => true,
259 TnuaActionLifecycleStatus::NoLongerFed => false,
260 TnuaActionLifecycleStatus::CancelledInto => false,
261 }
262 }
263}
264
265/// A decision by [`TnuaAction::apply`] that determines if the action should be continued or not.
266///
267/// Note that an action may continue (probably with different state) after no longer being fed, or
268/// stopped while still being fed. It's up to the action, and it should be responsible with it.
269#[derive(PartialEq, Debug, Clone, Copy)]
270pub enum TnuaActionLifecycleDirective {
271 /// The action should continue in the next frame.
272 StillActive,
273
274 /// The action should not continue in the next frame.
275 ///
276 /// If another action is pending, it will run in this frame. This means that two actions can
277 /// run in the same frame, as long as the first is finished (or
278 /// [rescheduled](Self::Reschedule))
279 ///
280 /// If [`TnuaAction::apply`] returns this but the action is still being fed, it will not run
281 /// again unless it stops being fed for one frame and then gets fed again. If this is not the
282 /// desired behavior, [`TnuaActionLifecycleDirective::Reschedule`] should be used instead.
283 Finished,
284
285 /// The action should not continue in the next frame, but if its still being fed it run again
286 /// later. The rescheduled action will be considered a new action.
287 ///
288 /// If another action is pending, it will run in this frame. This means that two actions can
289 /// run in the same frame, as long as the first is rescheduled (or [finished](Self::Finished))
290 Reschedule {
291 /// Only reschedule the action after this much time has passed.
292 after_seconds: Float,
293 },
294}
295
296/// A decision by [`TnuaAction::initiation_decision`] that determines if the action can start.
297#[derive(PartialEq, Eq, Debug, Clone, Copy)]
298pub enum TnuaActionInitiationDirective {
299 /// The action will not start as long as the input is still fed. In order to start it, the
300 /// input must be released for at least one frame and then start being fed again.
301 Reject,
302
303 /// The action will not start this frame, but if the input is still fed next frame
304 /// [`TnuaAction::initiation_decision`] will be checked again.
305 Delay,
306
307 /// The action can start this frame.
308 Allow,
309}
310
311/// A character movement command for performing special actions.
312///
313/// "Special" does not necessarily mean **that** special - even
314/// [jumping](crate::builtins::TnuaBuiltinJump) or [crouching](crate::builtins::TnuaBuiltinCrouch)
315/// are considered [`TnuaAction`]s. Unlike basis - which is something constant - an action is
316/// usually something more momentarily that has a flow.
317///
318/// The type that implements this trait is called the action _input_, and is expected to be
319/// overwritten each frame by the controller system of the game code - although unlike basis the
320/// input will probably be the exact same. Configuration is stored in an asset, as part of a struct
321/// implementing [`TnuaSchemeConfig`] which holds the configuration for the basis and all the
322/// actions. If the action needs to persist data between frames it must keep it in its
323/// [memory](TnuaAction::Memory).
324pub trait TnuaAction<B: TnuaBasis>: 'static + Send + Sync {
325 type Config: Send + Sync + Clone + Serialize + for<'de> Deserialize<'de>;
326
327 /// Data that the action can persist between frames.
328 ///
329 /// The action will typically update this in its [`apply`](Self::apply). It has three purposes:
330 ///
331 /// 1. Store data that cannot be calculated on the spot. For example - the part of the jump
332 /// the character is currently at.
333 ///
334 /// 2. Pass data from the action to Tnua's internal mechanisms.
335 ///
336 /// 3. Inspect the action from game code systems, like an animation controlling system that
337 /// needs to know which animation to play based on the action's current state.
338 type Memory: Send + Sync + Default;
339
340 /// Decides whether the action can start.
341 ///
342 /// The difference between rejecting the action here with
343 /// [`TnuaActionInitiationDirective::Reject`] or [`TnuaActionInitiationDirective::Delay`] and
344 /// approving it with [`TnuaActionInitiationDirective::Allow`] only to do nothing in it and
345 /// terminate with [`TnuaActionLifecycleDirective::Finished`] on the first frame, is that if
346 /// some other action is currently running, in the former that action will continue to be
347 /// active, while in the latter it'll be cancelled into this new action - which, having being
348 /// immediately finished, will leave the controller with no active action, or with some third
349 /// action if there is one.
350 fn initiation_decision(
351 &self,
352 config: &Self::Config,
353 sensors: &B::Sensors<'_>,
354 ctx: TnuaActionContext<B>,
355 being_fed_for: &Stopwatch,
356 ) -> TnuaActionInitiationDirective;
357
358 /// This is where the action affects the character's motion.
359 ///
360 /// This method gets called each frame to let the action control the [`TnuaMotor`] that will
361 /// later move the character. Note that this happens the motor was set by the basis'
362 /// [`apply`](TnuaBasis::apply). Here the action can modify some aspects of or even completely
363 /// overwrite what the basis did.
364 ///
365 /// It can also update the memory.
366 ///
367 /// The returned value of this action determines whether or not the action will continue in the
368 /// next frame.
369 fn apply(
370 &self,
371 config: &Self::Config,
372 memory: &mut Self::Memory,
373 sensors: &B::Sensors<'_>,
374 ctx: TnuaActionContext<B>,
375 lifecycle_status: TnuaActionLifecycleStatus,
376 motor: &mut TnuaMotor,
377 ) -> TnuaActionLifecycleDirective;
378
379 /// An action can use this method to send information back to the basis' memory.
380 ///
381 /// For example - a jump action can use that to violate the basis' coyote time.
382 #[allow(unused_variables)]
383 fn influence_basis(
384 &self,
385 config: &Self::Config,
386 memory: &Self::Memory,
387 ctx: TnuaBasisContext,
388 basis_input: &B,
389 basis_config: &B::Config,
390 basis_memory: &mut B::Memory,
391 ) {
392 }
393}
394
395/// An enum with a unit variant for each variant of the control scheme.
396pub trait TnuaActionDiscriminant:
397 'static + Send + Sync + Copy + Clone + PartialEq + Eq + core::fmt::Debug
398{
399 #[doc(hidden)]
400 const NUM_FEED_STATUS_SLOTS: usize;
401
402 #[doc(hidden)]
403 fn feed_status_slot(&self) -> usize;
404}
405
406/// An enum mirroring the control scheme, except instead of just having the input of each action
407/// each variant has a [`TnuaActionState`] which contains the action's input, configuration and
408/// memoery. If the variant in the control scheme has payload, they are copied to the variant in
409/// action state as is.
410pub trait TnuaActionState: 'static + Send + Sync {
411 /// The basis of the control scheme this action state enum represents.
412 type Basis: TnuaBasis;
413 /// An enum with a unit variant for each variant of the control scheme.
414 type Discriminant: TnuaActionDiscriminant;
415
416 /// The action without the input and payloads.
417 fn discriminant(&self) -> Self::Discriminant;
418
419 #[doc(hidden)]
420 fn interface(&self) -> &dyn TnuaActionStateInterface<Self::Basis>;
421 #[doc(hidden)]
422 fn interface_mut(&mut self) -> &mut dyn TnuaActionStateInterface<Self::Basis>;
423
424 #[doc(hidden)]
425 fn modify_basis_config(&self, basis_config: &mut <Self::Basis as TnuaBasis>::Config);
426}
427
428/// References to the full state of the basis in the controller.
429#[derive(Clone)]
430pub struct TnuaBasisAccess<'a, B: TnuaBasis> {
431 /// The data that the user control system sets in [the `basis` field of the
432 /// `TnuaController`](crate::TnuaController::basis).
433 pub input: &'a B,
434 /// The basis configuration, retrieved from an asset and can be modified by payloads that
435 /// implement [`TnuaConfigModifier`].
436 pub config: &'a B::Config,
437 /// Initiated when the controller is created, and gets updated by the basis itself or by an
438 /// action (using [`influence_basis`](TnuaAction::influence_basis))
439 pub memory: &'a B::Memory,
440}
441
442/// Various data passed to [`TnuaAction::apply`].
443pub struct TnuaActionContext<'a, B: TnuaBasis> {
444 /// The duration of the current frame.
445 pub frame_duration: Float,
446
447 /// A sensor that collects data about the rigid body from the physics backend.
448 pub tracker: &'a TnuaRigidBodyTracker,
449
450 /// The direction considered as "up".
451 pub up_direction: Dir3,
452
453 /// An accessor to the basis.
454 pub basis: &'a TnuaBasisAccess<'a, B>,
455}
456
457impl<'a, B: TnuaBasis> TnuaActionContext<'a, B> {
458 /// "Downgrade" to a basis context.
459 ///
460 /// This is useful for some helper methods of that require a basis context.
461 pub fn as_basis_context(&self) -> TnuaBasisContext<'a> {
462 TnuaBasisContext {
463 frame_duration: self.frame_duration,
464 tracker: self.tracker,
465 up_direction: self.up_direction,
466 }
467 }
468
469 /// The duration of the current frame where the action is being applied.
470 pub fn frame_duration_as_duration(&self) -> Duration {
471 #[allow(clippy::useless_conversion)]
472 Duration::from_secs_f64(self.frame_duration.into())
473 }
474}
475
476/// Implement on payloads that can modify the configuration while the relevant action is running.
477///
478/// Note that the payload needs to have `#[scheme(modify_basis_config)]` on it in the control
479/// scheme in order for this to take effect.
480pub trait TnuaConfigModifier<C> {
481 /// Modify the configuration.
482 ///
483 /// This does not touch the asset itself - it modifies a copy of the configuration stored in
484 /// the controller. Each time this method is called it works on a fresh copy of the
485 /// configuration - one that was just cloned from the asset and was not already modified by
486 /// this payload (though it could have been modified by previous payloads in the action
487 /// variant definition). This means that it's okay to do things like `config.field *= 2.0;` -
488 /// it won't cause the number in the field to explode.
489 fn modify_config(&self, config: &mut C);
490}