Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Unity3D Playables API Based Character Animation Controller
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
[RequireComponent(typeof(Animator))]
public class AnimationController : MonoBehaviour
{
const string ANIMATION = "Animation";
PlayableGraph _playableGraph;
PlayableOutput _playableOutput;
AnimationMixerPlayable _mixerPlayable;
AnimationClipPlayable[] _clipPlayables;
void OnDestroy()
{
_playableGraph.Destroy();
}
public void Init(AnimationClip[] clips)
{
_playableGraph = PlayableGraph.Create();
_playableGraph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
_playableOutput = AnimationPlayableOutput.Create(_playableGraph, ANIMATION, GetComponent<Animator>());
_mixerPlayable = AnimationMixerPlayable.Create(_playableGraph, clips.Length);
_playableOutput.SetSourcePlayable(_mixerPlayable);
_clipPlayables = new AnimationClipPlayable[clips.Length];
for (int i = 0, l = _clipPlayables.Length; i < l; i++)
{
_clipPlayables[i] = AnimationClipPlayable.Create(_playableGraph, clips[i]);
_playableGraph.Connect(_clipPlayables[i], 0, _mixerPlayable, i);
}
_playableGraph.Play();
}
public bool IsComplete(int index)
{
return _clipPlayables[index].GetTime() > _clipPlayables[index].GetAnimationClip().length;
}
public float GetNormalizedTime(int index)
{
return (float)_clipPlayables[index].GetTime() / _clipPlayables[index].GetAnimationClip().length;
}
public void SetTime(int index, float time)
{
_clipPlayables[index].SetTime(time);
}
public void SetWeight(int index, float weight)
{
_mixerPlayable.SetInputWeight(index, weight);
}
public void SetSpeed(int index, float speed)
{
_clipPlayables[index].SetSpeed(speed);
}
public void SetDuration(int index, float duration)
{
SetSpeed(index, _clipPlayables[index].GetAnimationClip().length / duration);
}
}
using UnityEngine;
using System.Collections.Generic;
public interface IEntityAnimationController
{
void PlayNamedClip(string name, bool clamp = false, float? duration = null);
}
public class CharacterAnimationController : AnimationController, IEntityAnimationController
{
[System.Serializable]
public struct NamedAnimationClip
{
public string Name;
public AnimationClip Clip;
}
[System.Serializable]
public struct MovementBlendSettings
{
[UnityEngine.Serialization.FormerlySerializedAs("UseBlendSettings")]
public bool SyncMovementClips;
public float DesiredClipLength;
public float NormalizedLeftFootfallTime;
}
private struct MovementBlendValues
{
public float PlaybackSpeed;
public float Offset;
}
// ^^ Previous two structs allow clips of different lengths and footfall
// positions to be adjusted dynamically to blend well by this controller
// ideally the animations would be such that this was not necessary
[System.Serializable]
public struct MovementClip
{
public MovementAnimationDef Def;
public float SpeedRangeMin;
public float SpeedRangeMax;
}
[System.Serializable]
public struct IdleClip
{
public AnimationClip Clip;
public float Weighting;
}
public event System.Action OnLeftFootfall;
public event System.Action OnRightFootfall;
[SerializeField]
bool _blendMovementClips;
[SerializeField]
IdleClip[] _idleClips;
[SerializeField]
MovementClip[] _moveClips;
[SerializeField]
MovementBlendSettings _moveBlendSettings;
[SerializeField]
NamedAnimationClip[] _clips;
[SerializeField, Range(0,10)]
float _speed = 0;
MovementController _movementController;
// ^^ Not included in gist, just need something that exposes current character speed.
Transform _transform;
// ^^ Assumes this component is on visual root
int[] _idleClipPlayables;
int[] _movementClipPlayables;
Dictionary<string, int> _namedClipPlayables;
int _idleClipIndex = 0;
int _movementClipIndex = 0;
string _namedClipName = null;
bool _clampNamedClip;
float _previousNormalizedMovementTime;
MovementBlendValues[] _movementBlendValues;
public void Init(MovementController movementController)
{
_movementController = movementController;
}
void Awake()
{
_transform = transform;
int clipCount = _idleClips.Length + _moveClips.Length + _clips.Length;
var clips = new List<AnimationClip>(clipCount);
_idleClipPlayables = new int[_idleClips.Length];
_namedClipPlayables = new Dictionary<string, int>(_clips.Length);
for (int i = 0, l = _idleClipPlayables.Length; i < l; i++)
{
_idleClipPlayables[i] = clips.Count;
clips.Add(_idleClips[i].Clip);
}
_movementBlendValues = new MovementBlendValues[_moveClips.Length];
_movementClipPlayables = new int[_moveClips.Length];
for (int i = 0, l = _moveClips.Length; i < l; i++)
{
CalculateMovementBlendValues(i);
_movementClipPlayables[i] = clips.Count;
clips.Add(_moveClips[i].Def.Clip);
}
for (int i = 0, l = _clips.Length; i < l; i++)
{
string name = _clips[i].Name;
_namedClipPlayables[name] = clips.Count;
clips.Add(_clips[i].Clip);
}
base.Init(clips.ToArray());
SetMovementClipPlaybackSpeeds();
}
void Update()
{
if (_movementController != null)
{
// Some Really Basic Smoothing
_speed = Mathf.Lerp(_speed, _movementController.Speed, 0.5f);
}
if (_namedClipName != null && !_clampNamedClip && IsComplete(_namedClipPlayables[_namedClipName]))
{
SetWeight(_namedClipPlayables[_namedClipName], 0);
_namedClipName = null;
}
if (_namedClipName == null)
{
if (_speed < 0.05f && _idleClipPlayables.Length > 0)
{
SetIdleWeights();
}
else if (_movementClipPlayables.Length > 0)
{
if (_blendMovementClips)
{
SetBlendedMovementWeights();
CheckForFootfalls();
// ^^ Assumes either using the blend settings to enforce same footfall positions
// or that they are at approximately the same point in each clip
// In this mode foot falls do not vary by how fast you're moving
}
else
{
SetMovementWeights();
CheckForFootfalls();
}
}
}
}
public void Idle()
{
if (_namedClipName != null)
{
SetWeight(_namedClipPlayables[_namedClipName], 0);
}
_clampNamedClip = false;
_namedClipName = null;
SetIdleWeights();
}
public void PlayNamedClip(string name, bool clamp = false, float? duration = null)
{
if (_namedClipPlayables.ContainsKey(name))
{
_clampNamedClip = clamp;
_namedClipName = name;
SetTime(_namedClipPlayables[name], 0);
if (duration.HasValue)
{
SetDuration(_namedClipPlayables[name], duration.Value);
}
else
{
SetSpeed(_namedClipPlayables[name], 1);
}
int index = _namedClipPlayables[name];
for (int i = 0; i < _totalClipCount; i++)
{
if (i == index)
{
SetWeight(i, 1);
}
else
{
SetWeight(i, 0);
}
}
}
}
void CalculateMovementBlendValues(int i)
{
_movementBlendValues[i] = new MovementBlendValues();
if (_moveBlendSettings.SyncMovementClips)
{
int cycleCount = _moveClips[i].Def.LeftFootfallNormalizedTimes.Length;
_movementBlendValues[i].PlaybackSpeed = _moveClips[i].Def.Clip.length / (cycleCount * _moveBlendSettings.DesiredClipLength);
_movementBlendValues[i].Offset = _moveClips[i].Def.LeftFootfallNormalizedTimes[0] - _moveBlendSettings.NormalizedLeftFootfallTime;
if (_movementBlendValues[i].Offset < 0)
{
_movementBlendValues[i].Offset = 1 - _movementBlendValues[i].Offset;
}
}
else
{
_movementBlendValues[i].PlaybackSpeed = 1;
_movementBlendValues[i].Offset = 0;
}
}
void SetMovementClipPlaybackSpeeds()
{
if (_blendMovementClips)
{
for (int i = 0, l = _movementClipPlayables.Length; i < l; i++)
{
SetSpeed(_movementClipPlayables[i], _movementBlendValues[i].PlaybackSpeed);
}
}
}
void SetIdleWeights()
{
int mixerIndexOffset = _idleClipPlayables.Length;
for (int i = 0, l = _movementClipPlayables.Length; i < l; i++)
{
SetWeight(_movementClipPlayables[i], 0);
if (_blendMovementClips)
{
SetTime(_movementClipPlayables[i], _movementBlendValues[i].Offset);
}
else
{
SetTime(_movementClipPlayables[i], 0);
}
}
if (IsComplete(_idleClipPlayables[_idleClipIndex]))
{
_idleClipIndex = PickIdleIndex();
SetTime(_idleClipPlayables[_idleClipIndex], 0);
}
// Would be nice to blend these in out - more state transition blends!
for(int i = 0, l = _idleClipPlayables.Length; i < l; i++)
{
SetWeight(_idleClipPlayables[i], i == _idleClipIndex ? 1 : 0);
}
}
void SetMovementWeights()
{
// Uses each animation for a range of speeds changing the speed of play back to match.
// Scales to overspeed - but transitions instantly between animations
for (int i = 0, l = _idleClipPlayables.Length; i < l; i++)
{
SetWeight(_idleClipPlayables[i], 0);
}
_movementClipIndex = GetClosestMovementClipIndex(_speed);
for (int i = 0, l = _moveClips.Length; i < l; i++)
{
if (i == _movementClipIndex)
{
SetSpeed(_movementClipPlayables[i], _speed / GetTravelSpeed(i));
SetWeight(_movementClipPlayables[i], 1);
}
else
{
SetWeight(_movementClipPlayables[i], 0);
}
}
if (IsComplete(_movementClipPlayables[_movementClipIndex]))
{
SetTime(_movementClipPlayables[_movementClipIndex], 0);
}
}
void SetBlendedMovementWeights()
{
for(int i = 0, l = _idleClipPlayables.Length; i < l; i++)
{
SetWeight(_idleClipPlayables[i], 0);
}
_movementClipIndex = GetClosestMovementClipIndex(_speed);
// This method compares the travel speed of the clip to the speed of the agent
// and lerps between movement clips to pick a combination, does not alter playback speed
for (int i = 0, l = _moveClips.Length; i < l; i++)
{
float travelSpeed = GetTravelSpeed(i);
if (_speed >= travelSpeed)
{
float weight = 1;
if (i + 1 < l)
{
float upperTravelSpeed = GetTravelSpeed(i + 1);
weight = Mathf.Clamp01(1 - (_speed - travelSpeed) / (upperTravelSpeed - travelSpeed));
}
SetWeight(_movementClipPlayables[i], weight);
}
else
{
float weight = 1;
if (i > 0)
{
float lowerTravelSpeed = GetTravelSpeed(i - 1);
weight = Mathf.Clamp01((_speed - lowerTravelSpeed) / (travelSpeed - lowerTravelSpeed));
}
if (i == 0)
{
float lowerTravelSpeed = 0;
weight = Mathf.Clamp01((_speed - lowerTravelSpeed) / (travelSpeed - lowerTravelSpeed));
SetWeight(_idleClipPlayables[0], 1 - weight);
}
SetWeight(_movementClipPlayables[i], weight);
}
// Loop clips as necessary
if (IsComplete(_movementClipPlayables[i]))
{
SetTime(_movementClipPlayables[i], 0);
}
}
// Loop idle clip as necessary
if (IsComplete(_idleClipPlayables[0]))
{
SetTime(_idleClipPlayables[0], 0);
}
}
void CheckForFootfalls()
{
float normalizedMovementTime = GetNormalizedTime(_movementClipPlayables[_movementClipIndex]);
float[] footfalls;
if (OnLeftFootfall != null && _moveClips[_movementClipIndex].Def.LeftFootfallNormalizedTimes != null)
{
footfalls = _moveClips[_movementClipIndex].Def.LeftFootfallNormalizedTimes;
for (int i = 0, l = footfalls.Length; i < l; i++)
{
if (normalizedMovementTime >= footfalls[i]
&& (_previousNormalizedMovementTime < footfalls[i] || _previousNormalizedMovementTime > normalizedMovementTime))
{
OnLeftFootfall();
}
}
}
if (OnRightFootfall != null && _moveClips[_movementClipIndex].Def.RightFootfallNormalizedTimes != null)
{
footfalls = _moveClips[_movementClipIndex].Def.RightFootfallNormalizedTimes;
for (int i = 0, l = footfalls.Length; i < l; i++)
{
if (normalizedMovementTime >= footfalls[i]
&& (_previousNormalizedMovementTime < footfalls[i] || _previousNormalizedMovementTime > normalizedMovementTime))
{
OnRightFootfall();
}
}
}
_previousNormalizedMovementTime = normalizedMovementTime;
}
#region Helpers
float GetTravelSpeed(int moveClipIndex)
{
float travelSpeed = _moveClips[moveClipIndex].Def.TravelSpeed;
if (_blendMovementClips)
{
travelSpeed *= _movementBlendValues[moveClipIndex].PlaybackSpeed;
}
return travelSpeed * _transform.localScale.z; // Assumption of forward motion only
}
bool InSpeedRange(MovementClip movementClip, float speed)
{
return movementClip.SpeedRangeMin * _transform.localScale.z <= _speed && movementClip.SpeedRangeMax * _transform.localScale.z >= speed;
}
int GetClosestMovementClipIndex(float speed)
{
if (_moveClips == null || _moveClips.Length == 0)
{
return -1;
}
// Prioritise current clip if there are overlaps
// NOTE: on player characters want overlap ranges to prevent back
// and forth between clips around the boundary due to input drift
if (InSpeedRange(_moveClips[_movementClipIndex], speed))
{
return _movementClipIndex;
}
int result = 0;
float minDistance = Mathf.Infinity;
for(int i = 0, l = _moveClips.Length; i < l; i++)
{
if (InSpeedRange(_moveClips[i], speed))
{
result = i;
break;
}
float distance = Mathf.Min(
Mathf.Abs(_moveClips[i].SpeedRangeMin * _transform.localScale.z - speed),
Mathf.Abs(_moveClips[i].SpeedRangeMax * _transform.localScale.z - speed));
if (distance < minDistance)
{
minDistance = distance;
result = i;
}
}
return result;
}
int PickIdleIndex()
{
if (_idleClips.Length == 1)
{
return 0;
}
float totalWeight = 0;
for(int i = 0, l = _idleClips.Length; i < l; i++)
{
totalWeight += _idleClips[i].Weighting;
}
float random = Random.value * totalWeight;
float evaluatedWeight = 0;
for (int i = 0, l = _idleClips.Length; i < l; i++)
{
if (random <= evaluatedWeight + _idleClips[i].Weighting)
{
return i;
}
evaluatedWeight += _idleClips[i].Weighting;
}
// Technically will never be reached but C# doesn't know that
return 0;
}
#endregion
}
@delphic

This comment has been minimized.

Copy link
Owner Author

commented Apr 11, 2019

This is the animation controller code as talked about in this blog post https://delphic.me.uk/blog/seekers_animation_system

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.