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