Animation
Overview
The character animation in fighting games is crucial to gameplay as it drives gameplay by reacting to player input (attack, defense, combos) as well as dictates where the current hitboxes and hurtboxes of each character are at any given time.
The Fighting Sample uses the Custom Animator to enable deterministic animation (See the Addons > Custom Animator
page for more information) and extends to include features needed in fighting games.
Animator Parameters
The Custom Animator Graph is baked with the animation parameters from the Unity Animator in the exact same order as they are presented in when the baking takes place. To facilitate the runtime manipulation of these parameters, the Fighting Sample has created the CAParameters enum. It lists all parameters included by default in the Fighting Sample and explicitly assigns them their exact index.
Having the CAParameters set up in such a fashion allows to use the same interface to:
- set parameters at runtime by casting the enum to an int; and,
- present the parameters in the
InputCommandDataBase
used for creating new input sequences.
Custom Animator Extensions
The Fighting Sample extends the Custom Animator with FighterAnimatorState
, CustomAnimatorBehaviour
and CustomAnimatorStateSystem
.
Custom Animator State System
The CustomAnimatorStateSystem
is a simple SystemSignalsOnly
system. Its purpose is to react to the OnEnter, OnUpdate and OnExit state related signals fired by the AnimatorState.
C#
ISignalOnAnimatorStateEnter
ISignalOnAnimatorStateUpdate
ISignalOnAnimatorStateExit
It then sets selected parameters on the entity's Custom Animator, iterates over the behaviours included in the FightAnimationState
and runs the complementary methods on those (OnEnter, OnUpdate and OnExit).
Fighter Animator State
By default, the CustomAnimator
uses the AnimatorState
asset to update, blend and get the motion of a particular animation state. The Fighting Sample extends this asset by including a reference to a FigherAnimatorState
asset.
C#
// See CustomAnimatorState.cs
public AssetRefFighterAnimatorState StateAsset;
The FighterAnimatorState
is an asset whose purpose is twofold:
- it contains the various
FighterAnimatorBehaviours
associated with a given state. TheseFighterAnimatorBehaviours
are set up at edit-time as part of theQTASB Container
in the Unity Animator before baking the Quantum Animator Assets. - it contains the position of a character’s hurt boxes in the form of a
HurtBoxSet
for a given animation state. TheHurtBoxSet
specifies the position of a character’s hurt boxes for each frame of an animation and updates the hurtBoxList variable of the Fighter component each tick accordingly.
Quantum Animator State Behaviour
The Fighting Sample enables animation state behaviour for the Custom Animator via the abstract asset class CustomAnimatorBehaviour
.
C#
public abstract unsafe partial class CustomAnimatorBehaviour
{
public abstract void OnEnter(Frame f, EntityRef entity, CustomAnimator* animator);
/// <summary>
/// Performed during a state's update.
/// <returns>If true, the state will stop updating behaviours. Usually done if a transition occurs mid-state</returns>
public abstract bool OnUpdate(Frame f, EntityRef entity, CustomAnimator* animator);
public abstract void OnExit(Frame f, EntityRef entity, CustomAnimator* animator);
}
FighterAnimatorBehaviour
The FighterAnimatorBehaviour
derives from CustomAnimatorBehaviour
asset. It is an abstract asset too and customises the OnEnter()
, OnUpdate()
and OnExit()
methods to be used for making concrete implementations of “active” states.
C#
public abstract unsafe class FighterAnimatorBehaviour : CustomAnimatorBehaviour
{
public override unsafe void OnEnter(Frame f, EntityRef entity, CustomAnimator* animator) {
GetFighterAndTransform(f, entity, out Fighter* fighter, out Transform3D* transform);
OnEnter(f, entity, fighter, animator, transform);
}
private static void GetFighterAndTransform(Frame f, EntityRef entity, out Fighter* fighter, out Transform3D* transform) {
fighter = f.Unsafe.GetPointer<Fighter>(entity);
transform = f.Unsafe.GetPointer<Transform3D>(entity);
}
public override unsafe bool OnUpdate(Frame f, EntityRef entity, CustomAnimator* animator) {
GetFighterAndTransform(f, entity, out Fighter* fighter, out Transform3D* transform);
return OnUpdate(f, entity, fighter, animator, transform);
}
public override unsafe void OnExit(Frame f, EntityRef entity, CustomAnimator* animator) {
GetFighterAndTransform(f, entity, out Fighter* fighter, out Transform3D* transform);
OnExit(f, entity, fighter, animator, transform);
}
public abstract void OnEnter(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform);
public abstract bool OnUpdate(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform);
public abstract void OnExit(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform);
}
An active state is characterised by a functionality attempting to immediately affect the gameplay (OnEnter()
or OnUpdate()
) and the absence of timer. For instance the QTASBResetCombo
behaviour resets an entity’s Combo Count when it is entered.
C#
public class QTASBResetCombo : FighterAnimatorBehaviour
{
public override unsafe void OnEnter(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform)
{
fighter->comboCount = 0;
frame.Events.OnComboCountChanged(fighter->index, fighter->comboCount);
}
}
The QTASBSetBlockState
on the other hand updates a character’s blocking state each frame OnUpdate()
.
C#
public override bool OnUpdate(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* anim, Transform3D* transform)
{
if (CustomAnimator.GetInteger(frame, anim, CAParameters.Horizontal_I_Index) < 0)
{
fighter->blocking = CustomAnimator.GetInteger(frame, anim,
CAParameters.Vertical_I_Index) < 0 ? BlockState.Low : BlockState.High;
}
else
{
fighter->blocking = BlockState.None;
}
return false;
}
QTAnimatorTriggerBehaviour
An alternative to “active” behaviours are “passive” behaviours. These are only triggered ONCE and only after an animation has been continuously its current state for the amount of time defined by the triggerTime
parameter. This behaviour is governed by the abstract asset class QTAnimatorTriggerBehaviour
.
C#
public abstract unsafe class QTAnimatorTriggerBehaviour : FighterAnimatorBehaviour
{
public FP triggerTime;
public override unsafe bool OnUpdate(Frame frame, EntityRef entity, Fighter* fighter, CustomAnimator* animator, Transform3D* transform)
{
var triggeredList = frame.ResolveList(animator->TriggeredStateBehaviours);
if (triggeredList.IndexOf(Guid) >= 0)
return false;
if (CustomAnimator.GetActiveWorldTime(frame, animator) >= triggerTime)
{
triggeredList.Add(Guid);
return OnTriggerBehaviour(frame, entity, fighter, animator, transform);
}
return false;
}
protected abstract unsafe bool OnTriggerBehaviour(Frame frame, EntityRef entity, Fighter* fighter, CustomAnimator* animator, Transform3D* transform);
public override unsafe void OnExit(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform)
{
var triggeredList = frame.ResolveList(animator->TriggeredStateBehaviours);
triggeredList.Remove(Guid);
}
}
The QTABCreateProjectile for example is used to trigger the creation of a projectile when the player performs an attack.
C#
public class QTABCreateProjectile : QTAnimatorTriggerBehaviour
{
public FPVector3 startPoint;
public AssetRefEntityPrototype projectileEntityPrototype;
public override unsafe void OnEnter(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform)
{
}
protected override unsafe bool OnTriggerBehaviour(Frame frame, EntityRef fighterRef, Fighter* fighter, CustomAnimator* animator, Transform3D* transform)
{
var prototypeRef = frame.Assets.Prototype(projectileEntityPrototype);
var prototype = frame.Create(prototypeRef);
var proj = frame.Get<Projectile>(prototype);
var projT = frame.Get<Transform3D>(prototype);
var projHB = frame.Get<HitBox>(prototype);
proj.side = fighter->side;
FPVector3 pos = startPoint;
pos.X *= fighter->side;
projT.Position = transform->Position + pos;
projHB.active = true;
projHB.attacker = fighterRef;
projHB.target = frame.Global->fighters[1 - fighter->index];
projHB.bounds.Center = projT.Position.XY;
frame.Set(prototype, proj);
frame.Set(prototype, projT);
frame.Set(prototype, projHB);
frame.Events.PlayAudioEffect(projT.Position.XY, frame.Assets.AudioPlaybackData(proj.sfxOnCreate));
return false;
}
}