Skip to main content

fyrox_animation/machine/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Animation blending state machine.
22//!
23//! Machine is used to blend multiple animation as well as perform automatic "smooth transition
24//! between states. See [`Machine`] docs for more info and examples.
25
26#![warn(missing_docs)]
27
28use crate::{
29    core::{
30        reflect::prelude::*,
31        visitor::{Visit, VisitResult, Visitor},
32    },
33    Animation, AnimationContainer, AnimationPose, EntityId,
34};
35use fxhash::FxHashSet;
36
37pub use event::Event;
38use fyrox_core::pool::Handle;
39use fyrox_core::{find_by_name_mut, find_by_name_ref};
40pub use layer::MachineLayer;
41pub use mask::LayerMask;
42pub use node::{
43    blend::{BlendAnimations, BlendAnimationsByIndex, BlendPose, IndexedBlendInput},
44    play::PlayAnimation,
45    AnimationPoseSource, PoseNode,
46};
47pub use parameter::{Parameter, ParameterContainer, PoseWeight};
48pub use state::State;
49pub use transition::Transition;
50
51pub mod event;
52pub mod layer;
53pub mod mask;
54pub mod node;
55pub mod parameter;
56pub mod state;
57pub mod transition;
58
59/// Animation blending state machine is used to blend multiple animation as well as perform automatic smooth transitions
60/// between states.
61///
62/// # Terminology
63///
64/// `Node` - is a part of sub-graph that backs _states_ with animations. Typical nodes are `PlayAnimation`, `BlendAnimations`,
65/// `BlendAnimationsByIndex`, etc. Nodes can be connected forming a tree, some node could be marked as output - its animation
66/// will be used in parent state.
67/// `State` - is a final source of animation for blending. There could be any number of states, for example typical
68/// states are: `run`, `idle`, `jump` etc. A state could be marked as _entry_ state - it will be active at the first frame
69/// when using the machine. There is always one state active.
70/// `Transition` - is a connection between states that has transition time, a link to a parameter that defines whether the
71/// transition should be performed or not. Transition is directional; there could be any number of transitions between any
72/// number of states (loops are allowed).
73/// `Parameter` - is a named variable of a fixed type (see `Parameters` section for more info).
74/// `Layer` - is a separate state graph, there could be any number of layers - each with its own mask.
75/// `Mask` - a set of handles to nodes which will be excluded from animation on a layer.
76/// `Pose` - a final result of blending multiple animation into one.
77///
78/// Summarizing everything of this, we can describe animation blending state machine as a state graph, where each state has its
79/// own sub-graph (tree) that provides animation for blending. States can be connected via transitions.
80///
81/// # Parameters
82///
83/// Parameter is a named variable of a fixed type. Parameters are used as a data source in various places in the animation
84/// blending state machines. There are three main types of parameters:
85///
86/// `Rule` - boolean value that used as a trigger for transitions. When transition is using some rule, it checks the value
87/// of the parameter and if it is `true` transition starts.
88/// `Weight` - real number (`f32`) that is used a weight when you blending multiple animations into one.
89/// `Index` - natural number (`i32`) that is used as an animation selector.
90///
91/// Each parameter has a name, it could be pretty much any string.
92///
93/// # Layers
94///
95/// Layer is a separate state graph. Layers mainly used to animate different parts of humanoid (but not only) characters. For
96/// example there could a layer for upper body and a layer for lower body. Upper body layer could contain animations for aiming,
97/// melee attacks while lower body layer could contain animations for standing, running, crouching, etc. This gives you an
98/// ability to have running character that could aim or melee attack, or crouching and aiming, and so on with any combination.
99/// Both layers use the same set of parameters, so a change in a parameter will affect all layers that use it.
100///
101/// # Examples
102///
103/// Let have a quick look at simple state machine graph with a single layer:
104///
105/// ```text
106///                                                  +-------------+
107///                                                  |  Idle Anim  |
108///                                                  +------+------+
109///                                                         |
110///           Walk Weight                                   |
111/// +-----------+      +-------+           Walk->Idle Rule  |
112/// | Walk Anim +------+       |                            |
113/// +-----------+      |       |      +-------+         +---+---+
114///                    | Blend |      |       +-------->+       |
115///                    |       +------+ Walk  |         |  Idle |
116/// +-----------+      |       |      |       +<--------+       |
117/// | Aim Anim  +------+       |      +--+----+         +---+---+
118/// +-----------+      +-------+         |                  ^
119///           Aim Weight                 | Idle->Walk Rule  |
120///                                      |                  |
121///                       Walk->Run Rule |    +---------+   | Run->Idle Rule
122///                                      |    |         |   |
123///                                      +--->+   Run   +---+
124///                                           |         |
125///                                           +----+----+
126///                                                |
127///                                                |
128///                                         +------+------+
129///                                         |  Run Anim   |
130///                                         +-------------+
131/// ```
132///
133/// Here we have `Walk`, `Idle`, `Run` _states_ which uses different sources of poses:
134///
135/// - `Run` and `Idle` both directly uses respective animations as a pose source.
136/// - `Walk` - is the most complex here - it uses result of blending between `Aim` and `Walk` animations with different
137/// weights. This is useful if your character can only walk or can walk *and* aim at the same time. Desired pose
138/// determined by `Walk Weight` and `Aim Weight` parameters combination (see `Parameters` section for more info).
139/// **Note:** Such blending is almost never used on practice, instead you should use multiple animation layers. This
140/// serves only as an example that the machine can blend animations.
141///
142/// There are four transitions between three states each with its own _rule_. Rule is just Rule parameter which can
143/// have boolean value that indicates that transition should be activated. The machine on the image above can be created
144/// using code like so:
145///
146/// ```no_run
147/// use fyrox_animation::{
148///     machine::{
149///         Machine, State, Transition, PoseNode,
150///         Parameter, PlayAnimation, PoseWeight, BlendAnimations, BlendPose
151///     },
152///     core::pool::Handle
153/// };
154/// use fyrox_core::pool::ErasedHandle;
155///
156/// // Assume that these are correct handles.
157/// let idle_animation = Handle::default();
158/// let walk_animation = Handle::default();
159/// let aim_animation = Handle::default();
160///
161/// let mut machine = Machine::<ErasedHandle>::new();
162///
163/// let root_layer = &mut machine.layers_mut()[0];
164///
165/// let aim = root_layer.add_node(PoseNode::PlayAnimation(PlayAnimation::new(aim_animation)));
166/// let walk = root_layer.add_node(PoseNode::PlayAnimation(PlayAnimation::new(walk_animation)));
167///
168/// // Blend two animations together
169/// let blend_aim_walk = root_layer.add_node(PoseNode::BlendAnimations(
170///     BlendAnimations::new(vec![
171///         BlendPose::new(PoseWeight::Constant(0.75), aim),
172///         BlendPose::new(PoseWeight::Constant(0.25), walk)
173///     ])
174/// ));
175///
176/// let walk_state = root_layer.add_state(State::new("Walk", blend_aim_walk));
177///
178/// let idle = root_layer.add_node(PoseNode::PlayAnimation(PlayAnimation::new(idle_animation)));
179/// let idle_state = root_layer.add_state(State::new("Idle", idle));
180///
181/// root_layer.add_transition(Transition::new("Walk->Idle", walk_state, idle_state, 1.0, "WalkToIdle"));
182/// root_layer.add_transition(Transition::new("Idle->Walk", idle_state, walk_state, 1.0, "IdleToWalk"));
183///
184/// ```
185///
186/// This creates a machine with a single animation layer, fills it with some states that are backed by animation
187/// sources (either simple animation playback or animation blending). You can use multiple layers to animate a single
188/// model - for example one layer could be used for upper body of a character and other is lower body. This means that
189/// locomotion machine will take control over lower body and combat machine will control upper body.
190///
191/// Complex state machines quite hard to create from code, you should use ABSM editor instead whenever possible.
192#[derive(Default, Debug, Visit, Reflect, Clone, PartialEq)]
193pub struct Machine<T: EntityId> {
194    parameters: ParameterContainer,
195
196    #[visit(optional)]
197    layers: Vec<MachineLayer<T>>,
198
199    #[visit(skip)]
200    #[reflect(hidden)]
201    final_pose: AnimationPose<T>,
202
203    #[visit(skip)]
204    #[reflect(hidden)]
205    animations_cache: FxHashSet<Handle<Animation<T>>>,
206}
207
208impl<T: EntityId> Machine<T> {
209    /// Creates a new animation blending state machine with a single animation layer.
210    #[inline]
211    pub fn new() -> Self {
212        Self {
213            parameters: Default::default(),
214            layers: vec![MachineLayer::new()],
215            final_pose: Default::default(),
216            animations_cache: Default::default(),
217        }
218    }
219
220    /// Sets a value for existing parameter with given id or registers new parameter with given id and provided value.
221    /// The method returns a reference to the machine, so the calls could be chained:
222    ///
223    /// ```rust
224    /// use fyrox_animation::machine::{Machine, Parameter};
225    /// use fyrox_core::pool::ErasedHandle;
226    ///
227    /// let mut machine = Machine::<ErasedHandle>::new();
228    ///
229    /// machine
230    ///     .set_parameter("Run", Parameter::Rule(true))
231    ///     .set_parameter("Jump", Parameter::Rule(false));
232    /// ```
233    #[inline]
234    pub fn set_parameter(&mut self, id: &str, new_value: Parameter) -> &mut Self {
235        match self.parameters.get_mut(id) {
236            Some(parameter) => {
237                *parameter = new_value;
238            }
239            None => {
240                self.parameters.add(id, new_value);
241            }
242        }
243
244        self
245    }
246
247    /// Returns a shared reference to the container with all parameters used by the animation blending state machine.
248    #[inline]
249    pub fn parameters(&self) -> &ParameterContainer {
250        &self.parameters
251    }
252
253    /// Returns a mutable reference to the container with all parameters used by the animation blending state machine.
254    #[inline]
255    pub fn parameters_mut(&mut self) -> &mut ParameterContainer {
256        &mut self.parameters
257    }
258
259    /// Adds a new layer to the animation blending state machine.
260    #[inline]
261    pub fn add_layer(&mut self, layer: MachineLayer<T>) {
262        self.layers.push(layer)
263    }
264
265    /// Removes a layer at given index. Panics if index is out-of-bounds.
266    #[inline]
267    pub fn remove_layer(&mut self, index: usize) -> MachineLayer<T> {
268        self.layers.remove(index)
269    }
270
271    /// Inserts a layer at given position, panics in index is out-of-bounds.
272    #[inline]
273    pub fn insert_layer(&mut self, index: usize, layer: MachineLayer<T>) {
274        self.layers.insert(index, layer)
275    }
276
277    /// Removes last layer from the list.
278    #[inline]
279    pub fn pop_layer(&mut self) -> Option<MachineLayer<T>> {
280        self.layers.pop()
281    }
282
283    /// Returns a shared reference to the list of layers.
284    #[inline]
285    pub fn layers(&self) -> &[MachineLayer<T>] {
286        &self.layers
287    }
288
289    /// Returns a mutable reference to the list of layers.
290    #[inline]
291    pub fn layers_mut(&mut self) -> &mut [MachineLayer<T>] {
292        &mut self.layers
293    }
294
295    /// Tries to find a layer by its name. Returns index of the layer and its reference.
296    #[inline]
297    pub fn find_layer_by_name_ref<S: AsRef<str>>(
298        &self,
299        name: S,
300    ) -> Option<(usize, &MachineLayer<T>)> {
301        find_by_name_ref(self.layers.iter().enumerate(), name)
302    }
303
304    /// Tries to find a layer by its name. Returns index of the layer and its reference.
305    #[inline]
306    pub fn find_by_name_mut<S: AsRef<str>>(
307        &mut self,
308        name: S,
309    ) -> Option<(usize, &mut MachineLayer<T>)> {
310        find_by_name_mut(self.layers.iter_mut().enumerate(), name)
311    }
312
313    /// Returns final pose of the machine.
314    #[inline]
315    pub fn pose(&self) -> &AnimationPose<T> {
316        &self.final_pose
317    }
318
319    /// Computes final animation pose that could be then applied to a set of entities graph. This
320    /// method will update all the animations used by the machine automatically. Make sure to **not**
321    /// update the animations in the container before using this method. Otherwise your animations
322    /// will be updated more than once, and they'll play at higher speed and performance will also
323    /// be decreased.
324    #[inline]
325    pub fn evaluate_pose(
326        &mut self,
327        animations: &mut AnimationContainer<T>,
328        dt: f32,
329    ) -> &AnimationPose<T> {
330        self.final_pose.reset();
331
332        self.animations_cache.clear();
333        for layer in self.layers.iter_mut() {
334            let mut states_to_check = [Some(layer.active_state()), None, None];
335            if let Ok(active_transition) = layer.transitions().try_borrow(layer.active_transition())
336            {
337                states_to_check[1] = Some(active_transition.source);
338                states_to_check[2] = Some(active_transition.dest);
339            }
340            for state_to_check in states_to_check.iter().flatten() {
341                if let Ok(state) = layer.states().try_borrow(*state_to_check) {
342                    state.collect_animations(layer.nodes(), &mut self.animations_cache);
343                }
344            }
345        }
346
347        for animation_handle in self.animations_cache.iter() {
348            if let Ok(animation) = animations.try_get_mut(*animation_handle) {
349                if animation.is_enabled() {
350                    animation.tick(dt);
351                }
352            }
353        }
354
355        for layer in self.layers.iter_mut() {
356            let weight = layer.weight();
357            let pose = layer.evaluate_pose(animations, &self.parameters, dt);
358
359            self.final_pose.blend_with(pose, weight);
360        }
361
362        &self.final_pose
363    }
364}