|
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 |
|
} |
This comment has been minimized.
This is the animation controller code as talked about in this blog post https://delphic.me.uk/blog/seekers_animation_system