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:
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();
}
}