bevy_tnua/basis_action_traits.rs
1use bevy::prelude::*;
2use bevy::time::Stopwatch;
3use bevy_tnua_physics_integration_layer::math::{Float, Vector3};
4
5use std::{any::Any, time::Duration};
6
7use crate::{TnuaMotor, TnuaProximitySensor, TnuaRigidBodyTracker};
8
9/// Various data passed to [`TnuaBasis::apply`].
10pub struct TnuaBasisContext<'a> {
11 /// The duration of the current frame.
12 pub frame_duration: Float,
13
14 /// A sensor that collects data about the rigid body from the physics backend.
15 pub tracker: &'a TnuaRigidBodyTracker,
16
17 /// A sensor that tracks the distance of the character's center from the ground.
18 pub proximity_sensor: &'a TnuaProximitySensor,
19
20 /// The direction considered as "up".
21 pub up_direction: Dir3,
22}
23
24/// The main movement command of a character.
25///
26/// A basis handles the character's motion when the user is not feeding it any input, or when it
27/// just moves around without doing anything special. A simple game would only need once basis -
28/// [`TnuaBuiltinWalk`](crate::builtins::TnuaBuiltinWalk) - but more complex games can have bases
29/// for things like swimming or driving.
30///
31/// The type that implements this trait is called the basis _input_, and is expected to be
32/// overwritten each frame by the controller system of the game code. Configuration is considered
33/// as part of the input. If the basis needs to persist data between frames it must keep it in its
34/// [state](Self::State).
35pub trait TnuaBasis: 'static + Send + Sync {
36 /// The default name of the basis.
37 ///
38 /// [Once `type_name` becomes `const`](https://github.com/rust-lang/rust/issues/63084), this
39 /// will default to it. For now, just set it to the name of the type.
40 const NAME: &'static str;
41
42 /// Data that the basis can persist between frames.
43 ///
44 /// The basis will typically update this in its [`apply`](Self::apply). It has three purposes:
45 ///
46 /// 1. Store data that cannot be calculated on the spot. For example - a timer for tracking
47 /// coyote time.
48 ///
49 /// 2. Pass data from the basis to the action (or to Tnua's internal mechanisms)
50 ///
51 /// 3. Inspect the basis from game code systems, like an animation controlling system that
52 /// needs to know which animation to play based on the basis' current state.
53 type State: Default + Send + Sync;
54
55 /// This is where the basis affects the character's motion.
56 ///
57 /// This method gets called each frame to let the basis control the [`TnuaMotor`] that will
58 /// later move the character.
59 ///
60 /// Note that after the motor is set in this method, if there is an action going on, the
61 /// action's [`apply`](TnuaAction::apply) will also run and typically change some of the things
62 /// the basis did to the motor.
63 ///
64 /// It can also update the state.
65 fn apply(&self, state: &mut Self::State, ctx: TnuaBasisContext, motor: &mut TnuaMotor);
66
67 /// A value to configure the range of the ground proximity sensor according to the basis'
68 /// needs.
69 fn proximity_sensor_cast_range(&self, state: &Self::State) -> Float;
70
71 /// The displacement of the character from where the basis wants it to be.
72 ///
73 /// This is a query method, used by the action to determine what the basis thinks.
74 fn displacement(&self, state: &Self::State) -> Option<Vector3>;
75
76 /// The velocity of the character, relative the what the basis considers its frame of
77 /// reference.
78 ///
79 /// This is a query method, used by the action to determine what the basis thinks.
80 fn effective_velocity(&self, state: &Self::State) -> Vector3;
81
82 /// The vertical velocity the character requires to stay the same height if it wants to move in
83 /// [`effective_velocity`](Self::effective_velocity).
84 fn vertical_velocity(&self, state: &Self::State) -> Float;
85
86 /// Nullify the fields of the basis that represent user input.
87 fn neutralize(&mut self);
88
89 /// Can be queried by an action to determine if the character should be considered "in the air".
90 ///
91 /// This is a query method, used by the action to determine what the basis thinks.
92 fn is_airborne(&self, state: &Self::State) -> bool;
93
94 /// If the basis is at coyote time - finish the coyote time.
95 ///
96 /// This will be called automatically by Tnua, if the controller runs an action that [violated
97 /// coyote time](TnuaAction::VIOLATES_COYOTE_TIME), so that a long coyote time will not allow,
98 /// for example, unaccounted air jumps.
99 ///
100 /// If the character is fully grounded, this method must not change that.
101 fn violate_coyote_time(&self, state: &mut Self::State);
102}
103
104/// Helper trait for accessing a basis and its trait with dynamic dispatch.
105pub trait DynamicBasis: Send + Sync + Any + 'static {
106 #[doc(hidden)]
107 fn as_any(&self) -> &dyn Any;
108
109 #[doc(hidden)]
110 fn as_mut_any(&mut self) -> &mut dyn Any;
111
112 #[doc(hidden)]
113 fn apply(&mut self, ctx: TnuaBasisContext, motor: &mut TnuaMotor);
114
115 /// Dynamically invokes [`TnuaBasis::proximity_sensor_cast_range`].
116 fn proximity_sensor_cast_range(&self) -> Float;
117
118 /// Dynamically invokes [`TnuaBasis::displacement`].
119 fn displacement(&self) -> Option<Vector3>;
120
121 /// Dynamically invokes [`TnuaBasis::effective_velocity`].
122 fn effective_velocity(&self) -> Vector3;
123
124 /// Dynamically invokes [`TnuaBasis::vertical_velocity`].
125 fn vertical_velocity(&self) -> Float;
126
127 /// Dynamically invokes [`TnuaBasis::neutralize`].
128 fn neutralize(&mut self);
129
130 /// Dynamically invokes [`TnuaBasis::is_airborne`].
131 fn is_airborne(&self) -> bool;
132
133 #[doc(hidden)]
134 fn violate_coyote_time(&mut self);
135}
136
137pub(crate) struct BoxableBasis<B: TnuaBasis> {
138 pub(crate) input: B,
139 pub(crate) state: B::State,
140}
141
142impl<B: TnuaBasis> BoxableBasis<B> {
143 pub(crate) fn new(basis: B) -> Self {
144 Self {
145 input: basis,
146 state: Default::default(),
147 }
148 }
149}
150
151impl<B: TnuaBasis> DynamicBasis for BoxableBasis<B> {
152 fn as_any(&self) -> &dyn Any {
153 self
154 }
155
156 fn as_mut_any(&mut self) -> &mut dyn Any {
157 self
158 }
159
160 fn apply(&mut self, ctx: TnuaBasisContext, motor: &mut TnuaMotor) {
161 self.input.apply(&mut self.state, ctx, motor);
162 }
163
164 fn proximity_sensor_cast_range(&self) -> Float {
165 self.input.proximity_sensor_cast_range(&self.state)
166 }
167
168 fn displacement(&self) -> Option<Vector3> {
169 self.input.displacement(&self.state)
170 }
171
172 fn effective_velocity(&self) -> Vector3 {
173 self.input.effective_velocity(&self.state)
174 }
175
176 fn vertical_velocity(&self) -> Float {
177 self.input.vertical_velocity(&self.state)
178 }
179
180 fn neutralize(&mut self) {
181 self.input.neutralize();
182 }
183
184 fn is_airborne(&self) -> bool {
185 self.input.is_airborne(&self.state)
186 }
187
188 fn violate_coyote_time(&mut self) {
189 self.input.violate_coyote_time(&mut self.state)
190 }
191}
192
193/// Various data passed to [`TnuaAction::apply`].
194#[derive(Clone)]
195pub struct TnuaActionContext<'a> {
196 /// The duration of the current frame.
197 pub frame_duration: Float,
198
199 /// A sensor that collects data about the rigid body from the physics backend.
200 pub tracker: &'a TnuaRigidBodyTracker,
201
202 /// A sensor that tracks the distance of the character's center from the ground.
203 pub proximity_sensor: &'a TnuaProximitySensor,
204
205 /// The direction considered as "up".
206 pub up_direction: Dir3,
207
208 /// An accessor to the currently active basis.
209 pub basis: &'a dyn DynamicBasis,
210}
211
212impl<'a> TnuaActionContext<'a> {
213 /// Can be used to get the concrete basis.
214 ///
215 /// Use with care - actions that use it will only be usable with one basis.
216 pub fn concrete_basis<B: TnuaBasis>(&self) -> Option<(&B, &B::State)> {
217 let boxable_basis: &BoxableBasis<B> = self.basis.as_any().downcast_ref()?;
218 Some((&boxable_basis.input, &boxable_basis.state))
219 }
220
221 /// "Downgrade" to a basis context.
222 ///
223 /// This is useful for some helper methods of [the concrete basis and its
224 /// state](Self::concrete_basis) that require a basis context.
225 pub fn as_basis_context(&self) -> TnuaBasisContext<'a> {
226 TnuaBasisContext {
227 frame_duration: self.frame_duration,
228 tracker: self.tracker,
229 proximity_sensor: self.proximity_sensor,
230 up_direction: self.up_direction,
231 }
232 }
233
234 pub fn frame_duration_as_duration(&self) -> Duration {
235 Duration::from_secs_f64(self.frame_duration.into())
236 }
237}
238
239/// Input for [`TnuaAction::apply`] that informs it about the long-term feeding of the input.
240#[derive(PartialEq, Eq, Debug, Clone, Copy)]
241pub enum TnuaActionLifecycleStatus {
242 /// There was no action in the previous frame
243 Initiated,
244 /// There was a different action in the previous frame
245 CancelledFrom,
246 /// This action was already active in the previous frame, and it keeps getting fed
247 StillFed,
248 /// This action was fed up until the previous frame, and now no action is fed
249 NoLongerFed,
250 /// This action was fed up until the previous frame, and now a different action tries to override it
251 CancelledInto,
252}
253
254impl TnuaActionLifecycleStatus {
255 /// Continue if the action is still fed, finish if its not fed or if some other action gets
256 /// fed.
257 pub fn directive_simple(&self) -> TnuaActionLifecycleDirective {
258 match self {
259 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
260 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
261 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
262 TnuaActionLifecycleStatus::NoLongerFed => TnuaActionLifecycleDirective::Finished,
263 TnuaActionLifecycleStatus::CancelledInto => TnuaActionLifecycleDirective::Finished,
264 }
265 }
266
267 /// Similar to [`directive_simple`](Self::directive_simple), but if some other action gets fed
268 /// and this action is still being fed, reschedule this action once the other action finishes,
269 /// as long as more time than `after_seconds` has passed.
270 pub fn directive_simple_reschedule(
271 &self,
272 after_seconds: Float,
273 ) -> TnuaActionLifecycleDirective {
274 match self {
275 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
276 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
277 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
278 TnuaActionLifecycleStatus::NoLongerFed => {
279 // The rescheduling will probably go away, but in case things happen too fast and
280 // it doesn't - pass it anyway.
281 TnuaActionLifecycleDirective::Reschedule { after_seconds }
282 }
283 TnuaActionLifecycleStatus::CancelledInto => {
284 TnuaActionLifecycleDirective::Reschedule { after_seconds }
285 }
286 }
287 }
288
289 /// Continue - unless the action is cancelled into another action.
290 pub fn directive_linger(&self) -> TnuaActionLifecycleDirective {
291 match self {
292 TnuaActionLifecycleStatus::Initiated => TnuaActionLifecycleDirective::StillActive,
293 TnuaActionLifecycleStatus::CancelledFrom => TnuaActionLifecycleDirective::StillActive,
294 TnuaActionLifecycleStatus::StillFed => TnuaActionLifecycleDirective::StillActive,
295 TnuaActionLifecycleStatus::NoLongerFed => TnuaActionLifecycleDirective::StillActive,
296 TnuaActionLifecycleStatus::CancelledInto => TnuaActionLifecycleDirective::Finished,
297 }
298 }
299
300 /// Determine if the action just started, whether from no action or to replace another action.
301 pub fn just_started(&self) -> bool {
302 match self {
303 TnuaActionLifecycleStatus::Initiated => true,
304 TnuaActionLifecycleStatus::CancelledFrom => true,
305 TnuaActionLifecycleStatus::StillFed => false,
306 TnuaActionLifecycleStatus::NoLongerFed => false,
307 TnuaActionLifecycleStatus::CancelledInto => false,
308 }
309 }
310
311 /// Determine if the action is currently active - still fed and not replaced by another.
312 pub fn is_active(&self) -> bool {
313 match self {
314 TnuaActionLifecycleStatus::Initiated => true,
315 TnuaActionLifecycleStatus::CancelledFrom => true,
316 TnuaActionLifecycleStatus::StillFed => true,
317 TnuaActionLifecycleStatus::NoLongerFed => false,
318 TnuaActionLifecycleStatus::CancelledInto => false,
319 }
320 }
321}
322
323/// A decision by [`TnuaAction::apply`] that determines if the action should be continued or not.
324///
325/// Note that an action may continue (probably with different state) after no longer being fed, or
326/// stopped while still being fed. It's up to the action, and it should be responsible with it.
327#[derive(PartialEq, Debug, Clone, Copy)]
328pub enum TnuaActionLifecycleDirective {
329 /// The action should continue in the next frame.
330 StillActive,
331
332 /// The action should not continue in the next frame.
333 ///
334 /// If another action is pending, it will run in this frame. This means that two actions can
335 /// run in the same frame, as long as the first is finished (or
336 /// [rescheduled](Self::Reschedule))
337 ///
338 /// If [`TnuaAction::apply`] returns this but the action is still being fed, it will not run
339 /// again unless it stops being fed for one frame and then gets fed again. If this is not the
340 /// desired behavior, [`TnuaActionLifecycleDirective::Reschedule`] should be used instead.
341 Finished,
342
343 /// The action should not continue in the next frame, but if its still being fed it run again
344 /// later. The rescheduled action will be considered a new action.
345 ///
346 /// If another action is pending, it will run in this frame. This means that two actions can
347 /// run in the same frame, as long as the first is rescheduled (or [finished](Self::Finished))
348 Reschedule {
349 /// Only reschedule the action after this much time has passed.
350 after_seconds: Float,
351 },
352}
353
354/// A decision by [`TnuaAction::initiation_decision`] that determines if the action can start.
355#[derive(PartialEq, Eq, Debug, Clone, Copy)]
356pub enum TnuaActionInitiationDirective {
357 /// The action will not start as long as the input is still fed. In order to start it, the
358 /// input must be released for at least one frame and then start being fed again.
359 Reject,
360
361 /// The action will not start this frame, but if the input is still fed next frame
362 /// [`TnuaAction::initiation_decision`] will be checked again.
363 Delay,
364
365 /// The action can start this frame.
366 Allow,
367}
368
369/// A character movement command for performing special actions.
370///
371/// "Special" does not necessarily mean **that** special - even
372/// [jumping](crate::builtins::TnuaBuiltinJump) or [crouching](crate::builtins::TnuaBuiltinCrouch)
373/// are considered [`TnuaAction`]s. Unlike basis - which is something constant - an action is
374/// usually something more momentarily that has a flow.
375///
376/// The type that implements this trait is called the action _input_, and is expected to be
377/// overwritten each frame by the controller system of the game code - although unlike basis the
378/// input will probably be the exact same. Configuration is considered as part of the input. If the
379/// action needs to persist data between frames it must keep it in its [state](Self::State).
380pub trait TnuaAction: 'static + Send + Sync {
381 /// The default name of the action.
382 ///
383 /// [Once `type_name` becomes `const`](https://github.com/rust-lang/rust/issues/63084), this
384 /// will default to it. For now, just set it to the name of the type.
385 const NAME: &'static str;
386
387 /// Data that the action can persist between frames.
388 ///
389 /// The action will typically update this in its [`apply`](Self::apply). It has three purposes:
390 ///
391 /// 1. Store data that cannot be calculated on the spot. For example - the part of the jump
392 /// the character is currently at.
393 ///
394 /// 2. Pass data from the action to Tnua's internal mechanisms.
395 ///
396 /// 3. Inspect the action from game code systems, like an animation controlling system that
397 /// needs to know which animation to play based on the action's current state.
398 type State: Default + Send + Sync;
399
400 /// Set this to true for actions that may launch the character into the air.
401 const VIOLATES_COYOTE_TIME: bool;
402
403 /// This is where the action affects the character's motion.
404 ///
405 /// This method gets called each frame to let the action control the [`TnuaMotor`] that will
406 /// later move the character. Note that this happens the motor was set by the basis'
407 /// [`apply`](TnuaBasis::apply). Here the action can modify some aspects of or even completely
408 /// overwrite what the basis did.
409 ///
410 /// It can also update the state.
411 ///
412 /// The returned value of this action determines whether or not the action will continue in the
413 /// next frame.
414 fn apply(
415 &self,
416 state: &mut Self::State,
417 ctx: TnuaActionContext,
418 lifecycle_status: TnuaActionLifecycleStatus,
419 motor: &mut TnuaMotor,
420 ) -> TnuaActionLifecycleDirective;
421
422 /// A value to configure the range of the ground proximity sensor according to the action's
423 /// needs.
424 fn proximity_sensor_cast_range(&self) -> Float {
425 0.0
426 }
427
428 /// Decides whether the action can start.
429 ///
430 /// The difference between rejecting the action here with
431 /// [`TnuaActionInitiationDirective::Reject`] or [`TnuaActionInitiationDirective::Delay`] and
432 /// approving it with [`TnuaActionInitiationDirective::Allow`] only to do nothing in it and
433 /// terminate with [`TnuaActionLifecycleDirective::Finished`] on the first frame, is that if
434 /// some other action is currently running, in the former that action will continue to be
435 /// active, while in the latter it'll be cancelled into this new action - which, having being
436 /// immediately finished, will leave the controller with no active action, or with some third
437 /// action if there is one.
438 fn initiation_decision(
439 &self,
440 ctx: TnuaActionContext,
441 being_fed_for: &Stopwatch,
442 ) -> TnuaActionInitiationDirective;
443
444 /// If the action targets an entity, return that entity
445 fn target_entity(&self, _state: &Self::State) -> Option<Entity> {
446 None
447 }
448}
449
450pub trait DynamicAction: Send + Sync + Any + 'static {
451 fn as_any(&self) -> &dyn Any;
452 fn as_mut_any(&mut self) -> &mut dyn Any;
453 fn apply(
454 &mut self,
455 ctx: TnuaActionContext,
456 lifecycle_status: TnuaActionLifecycleStatus,
457 motor: &mut TnuaMotor,
458 ) -> TnuaActionLifecycleDirective;
459 fn proximity_sensor_cast_range(&self) -> Float;
460 fn initiation_decision(
461 &self,
462 ctx: TnuaActionContext,
463 being_fed_for: &Stopwatch,
464 ) -> TnuaActionInitiationDirective;
465 fn violates_coyote_time(&self) -> bool;
466 fn target_entity(&self) -> Option<Entity>;
467}
468
469pub(crate) struct BoxableAction<A: TnuaAction> {
470 pub(crate) input: A,
471 pub(crate) state: A::State,
472}
473
474impl<A: TnuaAction> BoxableAction<A> {
475 pub(crate) fn new(basis: A) -> Self {
476 Self {
477 input: basis,
478 state: Default::default(),
479 }
480 }
481}
482
483impl<A: TnuaAction> DynamicAction for BoxableAction<A> {
484 fn as_any(&self) -> &dyn Any {
485 self
486 }
487
488 fn as_mut_any(&mut self) -> &mut dyn Any {
489 self
490 }
491
492 fn apply(
493 &mut self,
494 ctx: TnuaActionContext,
495 lifecycle_status: TnuaActionLifecycleStatus,
496 motor: &mut TnuaMotor,
497 ) -> TnuaActionLifecycleDirective {
498 self.input
499 .apply(&mut self.state, ctx, lifecycle_status, motor)
500 }
501
502 fn proximity_sensor_cast_range(&self) -> Float {
503 self.input.proximity_sensor_cast_range()
504 }
505
506 fn initiation_decision(
507 &self,
508 ctx: TnuaActionContext,
509 being_fed_for: &Stopwatch,
510 ) -> TnuaActionInitiationDirective {
511 self.input.initiation_decision(ctx, being_fed_for)
512 }
513
514 fn violates_coyote_time(&self) -> bool {
515 A::VIOLATES_COYOTE_TIME
516 }
517
518 fn target_entity(&self) -> Option<Entity> {
519 self.input.target_entity(&self.state)
520 }
521}