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}