Animation State Machine
While baking animations inside .model files might be enough for simple, continuous animations*, when it comes to actually having complex movement occur (such as a randomized escort path, or a helicopter that simultaneously hovers and shoots missiles, like in Hotline Miami day 2), you will want to make use of the Animation State Machine (will be referred to as ASM).
For any unit to make use of the ASM, it will require four additional files with the following extensions:
animation_state_machineanimation_statesanimation_defanimation_subset
* There are some exceptions to this, as in, for instance, car door opening/closing animations, which are actually a single animation where certain ranges of keyframes correspond to the animation. The keyframes that play can be specified in animation_group sequences; see Sequence Manager XML.
But what exactly is a "state"?
An animation state is a way to control what action an unit is doing, what happens during it, and what it does when it finishes. Take, for instance, a fps/melee_hit state; the animation is determined through weights, sounds are played accordingly, and, when it finishes, it exits to the fps/idle state, that is, the "weapon idle" FPS animation. Compare this to a baked animation, where you can only start it, pause it, and be forced to time your sound design around the finished animation instead of using animation markers.
Initial .unit setup
The animation_state_machine and animation_def files need to be directly referenced by the .unit file:
<unit type="str" slot="1">
... network syncs ...
<anim_state_machine name="anims/units/helicopter/helicopter" />
<dependencies>
<depends_on animation_def="anims/units/helicopter/helicopter"/>
<depends_on animation_state_machine="anims/units/helicopter/helicopter"/>
</dependencies>
... the rest of the unit definition ...
</unit>
Files
animation_state_machine
This file needs to be referenced inside the unit's .unit file. Let's break down a sample animation_state_machine file. Note that the purpose of some attributes is currently unknown.
<state_machine name="a_cool_name_for_the_asm" timebase="frames">
<segment name="base"/>
<segment name="secondary_actions"/>
<global name="a_variable_name" value="0"/>
<default state="std/empty"/>
<states file="path/to/the/states/file"/>
</state_machine>
- Segments are used to allow for multiple states to be active at once; say, a medic can be crouching while playing the upper body "heal" animation. Every state will need to be assigned to a segment, and only one state can play at once per segment. HOWEVER, you can play multiple animations together from separate states in a single segment by using blend sets; see the section on
animation_def. - Globals allow for weights: a single state can have multiple animations (if it is of the "mix" type), however, it will only play the animation with the highest weight value. These can be controlled through scripting (see the corresponding section).
- A default empty state, with no animations, is set when the unit is loaded. This is standard on any
animation_state_machinefile unless you want the unit to start doing stuff as soon as it loads. If a invalid state is specified, no other states will work. - An
animation_statesfile is defined. There exists the possibility of defining the states directly in this file; however, it is recommended to still create individual files.
animation_states
This file needs to be referenced inside the animation_state_machine file. It contains all of the playable states for the ASM.
A valid animation state file resembles the following:
<xml>
<state name="std" type="template_state" segment="a_segment_for_the_state" mute="an_optional_segment_to_mute">
<to name="a_generic_name_for_this_animation" redirect="name/of/the/corresponding/state"/>
</state>
<state name="std/empty" type="emptyloop" segment="a_segment_for_the_state">
</state>
... ALL OTHER STATES ...
</xml>
- A state of the type
template_state: here, you define the names of the animation redirects to call from the sequence manager, that is, any name of your choice, and a reference to the state that will be played, for each state. You can't trigger states from the sequence manager if you use theirname/formatted/like/thisdirectly. - The default empty state we specified in the
animation_state_machinefile.
State types
template_state: a state that serves as a property template for other states under the hierarchy. For it to apply to children states, it needs to be defined before them.emptyloop: a state that does not play an animation until it is overriden by another state.- A special blending effect occurs when entering a state of this type when using multiple segments; see the
from_groupaction description below.
- A special blending effect occurs when entering a state of this type when using multiple segments; see the
once: a state that plays a single, pre-determined animation, once. If given multiple animations, it will always pick the same one, regardless of weights. For the correct behaviour in that case, usemix.loop: a state that plays a single, pre-determined animation, repeatedly, until another state is triggered.mix: a state that plays the animation with the most weight from a list of possible animations, once.mixloop: a state that plays the animation with the most weight from a list of possible animations, repeatedly, until another state is triggered.timeblend: a state that "plays" the position of the bones defined in its animation at a timet, which is an implicit parameter of the state, settable from Lua. The bones won't move from that position until another state is played or this one is re-entered with anothertvalue. When using this state, animations will need to define atime=parameter alongside them for reference purposes. This state also allows weights. E.g.<anim name="falcogini_wheel_turn_left" time="1" weight="falcogini"/>:- When
t = 0, the state will place the bones at their starting position in the animation. - When
t = 1(the value specified in the XML), the state will place the bones at their ending position in the animation. - When
0 < t < 1, the state will place the bone at that position in the animation given the referencetimeparameter.
- When
snapshot: unknown. Used once incore/units/locator/locator.animation_state_machine.
The following states are referenced in engine decompilations, but never used in the game:
sequence: untested, but should play its animations in a sequence as specified.empty: unknown.offset_once: unknown.offset_loop: unknown.
State actions
to: define a redirect to a state. E.g.<to name="reload_exit" redirect="fps/reload/r870_shotgun/exit"/>anim: play an animation with the specified name, which should have been defined in theanimation_subsetfiles. If the state is of the typemixormixloop, you also need to include aweight=parameter. E.g.<anim name="throw_concussion" weight="projectile_concussion"/>exit: when the animation that the state played finishes, enter the named state. This will never trigger onloopandmixloopstates. E.g.<exit name="fps/idle"/>. If exiting a state in a particular segment to a state in another segment, the second state will break.block: Block the specified state from triggering during this state. E.g.<block name="upper_body/recoil/crouch/auto_exit"/>from_group: blend the specified "amount" between the animation that the specified state was playing when exited and the animation that will now be played by this state. E.g.<from_group name="fps/idle" blend="1"/>.from_grouponly works for same-segment state switching; to blend finished animations on a segment with playing animations on another segment, exit on the finished segment to an emptyloop state on that same segment which usesfrom_group. See the video resource on blending for more info.
keys: a table ofkeyelements. More information below.param: similar toanimation_state_machineglobals, except these are defined and stored on a per-state basis. They can also be used to set weights, just like globals, and controlled through Lua. E.g.<param name="generic_stance" value="0"/>default: set default properties for states under the hierarchy. E.g.<default blend="1.5" blend_out="1.5"/>.blend: amount of blend-in; see the video resource on blending for more info.blend_out: unknown... pretty sure that it doesn't do what it's supposed to. If you want to actually blend out, usefrom_group.
Key types
General format:
<key at="key_type" callback="the_method_of_that_extension_class_to_call" class_name="the_name_of_the_extension_class" param1="first_param" param2="second_param" param3="moar_params" param4="you_get_the_point"/>
The former will result in the callback function being called as callback_function(param1, param2, param3, param4, ...) when the condition specified in at is met. Parameters are optional. Now for the key types:
enter: when the state is entered.exit: when the state is exited.loop: when the state loops. This will only trigger onloopandmixloop.trigger: when the animation reaches a certain marker. E.g.<key at="trigger" trigger="wp_g3_grab_end" callback="play_sound" class_name="base" param1="wp_g3_grab_end"/>full_blend: when a blend-in finishes. Does not work on emptyloop states.- Any number: when the animation is at that keyframe.
Naming states
State names should aim to follow the next structure, each "group" divided from each other by a forward slash (e.g. std/helicopter_attack_1):
- An identifier for the collection of states that follow the template state of the same identifier. E.g.
std,fps, etc. The initial template state should include all the redirect declarations. - For any states that require other template states inside the domain of the initial template state, repeat the former naming process.
Some examples:
std/animation_1will play while applying possible key actions in thestdtemplate state.std/death/animation_1will play while applying possible key actions in thestd/deathtemplate state. The latter will then inherently apply any key actions in thestdtemplate state.
Example
A base-game example of a state, with comments indicating what its doing. This particular one is the state for reloading the Reinfeld 880:
<state name="fps/reload/r870_shotgun/loop" type="loop" segment="base" speed="1.0"> <!-- Note the possibility of changing the speed of animations here -->
<!-- Set redirects for stopping the reload shell insert loop. These would be called from Lua in this case. -->
<to name="reload_exit" redirect="fps/reload/r870_shotgun/exit"/>
<to name="reload_not_empty_exit" redirect="fps/reload/r870_shotgun/not_empty_exit"/>
<!-- Play the animation -->
<anim name="r870_shotgun_reload_loop"/>
<!-- Do stuff at certain markers/events -->
<keys>
<key at="enter" callback="anim_clbk_spawn_shotgun_shell" class_name="base"/>
<key at="loop" callback="anim_clbk_spawn_shotgun_shell" class_name="base"/>
<key at="enter" callback="enter_shotgun_reload_loop" class_name="base" param1="fps/reload/r870_shotgun/loop"/>
<key at="trigger" trigger="wp_reinbeck_reload_cock" callback="play_sound" class_name="base" param1="wp_reinbeck_reload_cock"/>
<key at="trigger" trigger="wp_reinbeck_shell_insert" callback="play_sound" class_name="base" param1="wp_reinbeck_shell_insert"/>
<key at="trigger" trigger="wp_foley_generic_lever_pull" callback="play_sound" class_name="base" param1="wp_foley_generic_lever_pull"/>
<key at="trigger" trigger="wp_foley_generic_back_in_hand" callback="play_sound" class_name="base" param1="wp_foley_generic_back_in_hand"/>
<key at="trigger" trigger="anim_act_01" callback="anim_clbk_unspawn_shotgun_shell" class_name="base"/>
<key at="exit" callback="anim_clbk_unspawn_shotgun_shell" class_name="base"/>
</keys>
</state>
animation_def
This file needs to be referenced inside the unit's .object file.
<dynamic_object>
... material_config and sequence_manager ...
<animation_def name="path/to/animation/definition" />
... the rest of the object definition ...
</dynamic_object>
Here's a breakdown. The following is a simplified version of anims/units/enemies/cop/cop_def.animation_def, which I reckon showcases this file well.
<xml>
<!-- Animatable sets -->
<animatable_set name="cop">
<!-- Root -->
<bone name="root_point" root="true" alignment="true" alias="all root"/>
<!-- Spine -->
<bone name="Hips" alias="all legs"/>
<bone name="Spine" alias="all upper upper_l upper_r lod"/>
<bone name="Spine1" alias="all upper upper_l upper_r lod"/>
<bone name="Spine2" alias="all upper upper_l upper_r lod"/>
<!-- Left leg -->
<bone name="LeftUpLeg" alias="all legs lod"/>
<bone name="LeftLeg" alias="all legs lod"/>
<bone name="LeftFoot" alias="all legs lod"/>
<!-- Right leg -->
<bone name="RightUpLeg" alias="all legs lod"/>
<bone name="RightLeg" alias="all legs lod"/>
<bone name="RightFoot" alias="all legs lod"/>
</animatable_set>
<!-- Blend sets -->
<blend_set name="all" animatable_set="cop">
<blend alias="all" weight="1.0"/>
</blend_set>
<blend_set name="upper_body" animatable_set="cop">
<blend alias="all" weight="1"/>
<blend name="Spine2" weight="0.85"/>
<blend name="Spine1" weight="0.4"/>
<blend name="Spine" weight="0.25"/>
<blend alias="legs" weight="0"/>
<blend alias="root" weight="0"/>
</blend_set>
<!-- IK Modifiers -->
<modifier name="look_upper_body" type="ik" animatable_set="cop" iterations="1" blend_in="0.5" blend_out="0.3">
... modifier params ...
</modifier>
<!-- Animation sets -->
<animation_set name="cop" animatable_set="cop">
<subset file="path/to/the/subset/file"/>
<subset file="any/other/subset/file"/>
<subset file="you/can/have/as/many/as/you/want"/>
</animation_set>
</xml>
- Animatable sets are where you directly reference the bones in your model. You can also give them aliases, separated by spaces, in the
aliaskey. Takeroot_point, which not only is declared as the root bone, but also assigned bothallandrootaliases. Specifying non-existant bones will not crash the game.
For every animatable set you will need:
- Blend sets, which define how two animations playing at once blend together. This does not refer to switching between animations, but rather, how much weight the animations have over the bones; every animation needs to be specified a blend set to use. See the video resource on blending for more info.
- When you don't want to blend (that is, play your animation with full bone influence), you can use an "all" set which applies a weight of 1 to all the bones. You use these in your animation subset.
- Animation sets, which need to define one or more
animation_subsetfiles.
Modifiers
For procedural animation, such as those seen with VR arm movements, or peer upper body rotation when they move their camera.
Types: group, stand, ik, shoulder, displacement, offset, link, mirror.
The IK type requires either a rotation or a position node. These can be of type script, lock or animation (this last one might be of interest for actual IK?)
The mirror type requires a config attribute which can only be biped. For each of its entries, it needs a first and a second attribute, with an optional transform attribute which can either be inverse or root.
animation_subset
This file needs to be referenced inside the animation_def file. It's arguably the easiest to get a grip on.
<xml>
<anim name="referencable_anim_name" file="path/to/the/animation/file" blend_set="all"/>
</xml>
- For each animation file, you define an animation name and a blend set to use. This animation name is what animation states reference to play the animation file.
The folks that worked with these files actually left us some extra info on making animations:
If the animation finishes inside the viewable area and does not finish outside of the viewable area, you must animate the root point. Or else the animation will not sync at drop in. For eample the police car arrives and stop in the viewable area, here the root point shoul be animated. The heicopter arrives and leaves the the viewable area, here all_no_root is allowed.
Make sure to follow that guideline in order to avoid sync issues!
Controlling animations through Sequence Managers
Use animation_redirect. Example:
<sequence name=" 'heli_suburbia_hover' " triggable="true">
<animation_redirect name=" 'suburbia_hover' "/>
</sequence>
Remember that this is the name you specify in to state actions that redirects you to a state.
Video resources
Common crash-causers
When working with these files, it is especially frustrating to try to track down the source of access violations. Below is a list of issues to check:
- Paths are misspelled on any of the files
- An XML structure was not properly closed
animation_deforanimation_state_machineare missing from the unit's dependencies list- The root bone defined in
animation_defdoes not exist - An animation was specified in an
emptyloopstate - An animation specified in a state does not exist
Appendix A: Lua methods
Controlling animations
The ASM of an unit can be referenced through the unit's anim_state_machine() method.
ASM:set_global(name, value): sets the global to the specified number value.ASM:set_global_soft(name, value)ASM:get_global(name): get the value of the global.ASM:set_parameter(state, parameter, value): Set the parameter inside the specified state to a number value.ASM:set_parameter_soft(state, parameter, value)ASM:set_speed(animation_idstring, speed): sets the speed of the animation.ASM:set_speed_soft(animation_idstring, speed)ASM:get_speed(state): get the speed of the state.ASM:is_playing(animation_idstring): return whether the animation is playing or not.ASM:segment_real_time(segment_idstring): return the current time of the segment.ASM:segment_relative_time(segment_idstring)ASM:segment_muted(segment_idstring)ASM:stop_segment(segment_idstring): stop all the animations in the segment.ASM:segment_state(segment_idstring): get the playing state on the specified segment.ASM:segment_state_object(segment_idstring)ASM:config():states(): return a list of states in the ASM.State:name():s(): return the state name.
ASM:state_in_group(state)ASM:set_enabled(enabled)ASM:set_animation_time_all_segments(time)ASM:play_raw(animation)ASM:play(animation)ASM:root_blending()ASM:set_root_blending(bool)ASM:instant_blending()ASM:set_instant_blending(bool)ASM:set_callback_object(lua_object)ASM:index_to_state_name(index)ASM:state_name_to_index(state)ASM:enabled_modifiers()ASM:force_modifier(modifier)ASM:allow_modifier(modifier)ASM:forbid_modifier(modifier)ASM:get_modifier(modifier)ASM:set_modifier_blend(modifier, blend)ASM:reset()
To play a ASM state, use the unit's play_redirect(redirect_name, time_offset) method. This returns an Idstring identifier for the animation. Alternatively, you can also use the unit's play_state(state_idstring, time_offset).
Controlling modifiers
Modifiers can be accessed through ASM:get_modifier(idstring_modifier_name).
Modifier:set_target_position(vector3)Modifier:set_target_rotation(rotation)Modifier:set_target_x(vector3)Modifier:set_target_y(vector3)Modifier:set_target_z(vector3)Modifier:set_target_x_look_at(vector3)Modifier:set_target_y_look_at(vector3)Modifier:set_target_z_look_at(vector3)Modifier:set_object(?)Modifier:blend()