This document is about: QUANTUM 2
SWITCH TO

Animation


Available in the Gaming Circle and Industries Circle
Circle

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. These FighterAnimatorBehaviours are set up at edit-time as part of the QTASB 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. The HurtBoxSet 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;
   }
}
Back to top