Animancer Component

Characters need a few other features in addition to the animation functions of the AnimancerComponent, so rather than having two separate components and needing to both be referenced by the Character, we just have a CharacterAnimancerComponent Inherit from the base AnimancerComponent.

The main things it manages are:

  • The direction the character is Facing.
  • The character's currently active Hit Boxes.

Fields

The Serialized Fields and OnValidate method are self-explanatory:

public sealed class CharacterAnimancerComponent : AnimancerComponent
{
    [SerializeField]
    private SpriteRenderer _Renderer;
    public SpriteRenderer Renderer => _Renderer;

    [SerializeField]
    private Character _Character;
    public Character Character => _Character;

#if UNITY_EDITOR
    private void OnValidate()
    {
        gameObject.GetComponentInParentOrChildren(ref _Renderer);
        gameObject.GetComponentInParentOrChildren(ref _Character);
    }
#endif

Fading

Most of the Platformer Game Kit would work exactly the same with 3D Models or bone based Sprite animations, but since this script manages the direction the character is Facing, it is written specifically for Sprite based animations which means that Cross Fading will not actually do anything useful so it uses the DontAllowFade system to log a warning if ever a fade occurs by accident (generally due to an incorrectly configured Transition):

#if UNITY_ASSERTIONS
    private void Awake()
    {
        DontAllowFade.Assert(this);
    }
#endif

There is also a separate script called DefaultFadeDuration.cs in the Utilities folder to automatically set the AnimancerPlayable.DefaultFadeDuration to 0 instead of the usual 0.25:

namespace Animancer
{
    internal static class DefaultFadeDuration
    {
        [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void Initialize() => AnimancerPlayable.DefaultFadeDuration = 0;
    }
}

Even though that script is part of the Platformer Game Kit, it's important for that initialization to be done by a class named DefaultFadeDuration in the Animancer namespace so that Animancer can find and run it immediately when it loads (before even the [RuntimeInitializeOnLoadMethod] gets executed) so that Transitions can use the correct value as their default.

Facing

This system determines whether a character is facing left or right by simply using the Flip X toggle on the SpriteRenderer so it exposes several properties for accessing it and converting to and from actual directions:

    public bool FacingLeft
    {
        get => _Renderer.flipX;
        set => _Renderer.flipX = value;
    }

    public float FacingX
    {
        get => _Renderer.flipX ? -1f : 1f;
        set
        {
            if (value != 0)
                _Renderer.flipX = value < 0;
        }
    }

    public Vector2 Facing
    {
        get => new Vector2(FacingX, 0);
        set => FacingX = value.x;
    }

Every Update, if the character's current state's CanTurn property returns true, the Facing is set to match their MovementDirection:

    private void Update()
    {
        if (Character.StateMachine.CurrentState.CanTurn)
            Facing = Character.MovementDirection;
    }

Hit Boxes

The Combat system uses Animancer Events to determine when to activate each of the Hit Boxes in an attack animation. Each frame can have different hit details as shown in the following images:

The events are set up so that the attack data could potentially be shared by multiple characters, which means they can't directly reference their character and need to instead get the character each time an event occurs using one of the following methods:

    public static CharacterAnimancerComponent GetCurrent() => Get(AnimancerEvent.CurrentState);

    public static CharacterAnimancerComponent Get(AnimancerNode node) => Get(node.Root);
    
    public static CharacterAnimancerComponent Get(AnimancerPlayable animancer) => animancer.Component as CharacterAnimancerComponent;

Even though each attack can consist of multiple different hit boxes, a single attack can only hit each target once so the system needs to keep track of the character's active hit boxes and the targets they have already hit:

    private Dictionary<HitData, HitTrigger> _ActiveHits;
    private HashSet<Hit.ITarget> _IgnoreHits;

    public void AddHitBox(HitData data)
    {
        if (_IgnoreHits == null)
        {
            ObjectPool.Acquire(out _ActiveHits);
            ObjectPool.Acquire(out _IgnoreHits);
        }

        _ActiveHits.Add(data, HitTrigger.Activate(Character, data, FacingLeft, _IgnoreHits));
    }

    public void RemoveHitBox(HitData data)
    {
        if (_ActiveHits.TryGetValue(data, out var trigger))
        {
            trigger.Deactivate();
            _ActiveHits.Remove(data);
        }
    }

And when an attack ends or this script gets disabled (generally because it was destroyed) it clears all the hit boxes and returns the set of objects that have been hit to the ObjectPool:

    public void EndHitSequence()
    {
        if (_IgnoreHits == null)
            return;

        ClearHitBoxes();
        ObjectPool.Release(ref _ActiveHits);
        ObjectPool.Release(ref _IgnoreHits);
    }

    public void ClearHitBoxes()
    {
        if (_ActiveHits != null)
        {
            foreach (var trigger in _ActiveHits.Values)
                trigger.Deactivate();
            _ActiveHits.Clear();
        }
    }

    protected override void OnDisable()
    {
        EndHitSequence();
        base.OnDisable();
    }
}