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}