2025-09-24 11:24:38 +05:00

745 lines
21 KiB
C#

namespace Fusion.Addons.AnimationController
{
using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
#pragma warning disable 0109
/// <summary>
/// Animation controller component.
/// </summary>
[DisallowMultipleComponent]
public partial class AnimationController : NetworkBehaviour, IAfterSpawned, IAfterClientPredictionReset, IAfterTick
{
// CONSTANTS
public const string LOGGING_SCRIPT_DEFINE = "ANIMATIONS_LOG";
public const string PROFILING_SCRIPT_DEFINE = "ANIMATIONS_PROFILE";
// PUBLIC MEMBERS
/// <summary>
/// Evaluated playable graph.
/// </summary>
public PlayableGraph Graph => _graph;
/// <summary>
/// Main playable mixer which mixes outputs from animation layers.
/// </summary>
public AnimationLayerMixerPlayable Mixer => _mixer;
/// <summary>
/// List of animation layers.
/// </summary>
public IList<AnimationLayer> Layers => _layers;
/// <summary>
/// Current animator.
/// </summary>
public Animator Animator => _animator;
/// <summary>
/// Controls whether update methods are driven by default Fusion methods or called manually using <c>ManualFixedUpdate()</c> and <c>ManualRenderUpdate()</c>.
/// </summary>
public bool HasManualUpdate => _hasManualUpdate;
/// <summary>
/// Returns <c>Runner.DeltaTime</c> in fixed update and <c>Time.deltaTime</c> in render update.
/// </summary>
public float DeltaTime => _deltaTime;
/// <summary>
/// <c>True</c> if the animation prediction is enabled in fixed update.
/// </summary>
[System.Obsolete("Use Object.IsInSimulation.")]
public bool IsPredictingInFixedUpdate => Object.IsInSimulation;
/// <summary>
/// <c>True</c> if the animation interpolation is enabled in fixed update.
/// </summary>
[System.Obsolete("Interpolation in fixed update has been removed.")]
public bool IsInterpolatingInFixedUpdate => false;
// PRIVATE MEMBERS
[SerializeField]
private Transform _root;
[SerializeField]
private Animator _animator;
private PlayableGraph _graph;
private AnimationLayerMixerPlayable _mixer;
private AnimationPlayableOutput _output;
private AnimationLayer[] _layers;
private bool _isSpawned;
private bool _hasManualUpdate;
private bool _evaluateOnResimulation;
private EvaluationSettings _fixedEvaluationSettings = new EvaluationSettings();
private EvaluationSettings _renderEvaluationSettings = new EvaluationSettings();
private float _deltaTime;
private static ProfilerMarker _fixedUpdateMarker = new ProfilerMarker("AnimationController.FixedUpdate");
private static ProfilerMarker _renderUpdateMarker = new ProfilerMarker("AnimationController.RenderUpdate");
private static ProfilerMarker _restoreStateMarker = new ProfilerMarker("AnimationController.RestoreState");
private static ProfilerMarker _afterTickMarker = new ProfilerMarker("AnimationController.AfterTick");
private static ProfilerMarker _interpolateMarker = new ProfilerMarker("AnimationController.Interpolate");
private static ProfilerMarker _evaluateMarker = new ProfilerMarker("AnimationController.Evaluate");
private static ProfilerMarker _evaluateGraphMarker = new ProfilerMarker("PlayableGraph.Evaluate");
// PUBLIC METHODS
/// <summary>
/// Returns current evaluation mode for given target.
/// <param name="target">Evaluation target.</param>
/// </summary>
public EEvaluationMode GetEvaluationMode(EEvaluationTarget target)
{
switch (target)
{
case EEvaluationTarget.None: { return EEvaluationMode.None; }
case EEvaluationTarget.FixedUpdate: { return _fixedEvaluationSettings.Mode; }
case EEvaluationTarget.RenderUpdate: { return _renderEvaluationSettings.Mode; }
default:
{
throw new NotImplementedException($"{target}");
}
}
}
/// <summary>
/// <para>Set evaluation mode for given target.</para>
/// <para>Settings <c>EEvaluationMode.Interlaced</c> requires additional parameters and <c>SetInterlacedEvaluation()</c> must be called.</para>
/// <para>Settings <c>EEvaluationMode.Periodic</c> requires additional parameters and <c>SetPeriodicEvaluation()</c> must be called.</para>
/// </summary>
public void SetEvaluationMode(EEvaluationTarget target, EEvaluationMode mode)
{
if (target == EEvaluationTarget.None)
return;
if (mode == EEvaluationMode.Interlaced)
throw new NotSupportedException($"Parameters required, please use {nameof(AnimationController)}.{nameof(SetInterlacedEvaluation)}()");
if (mode == EEvaluationMode.Periodic)
throw new NotSupportedException($"Parameters required, please use {nameof(AnimationController)}.{nameof(SetPeriodicEvaluation)}()");
if (target == EEvaluationTarget.FixedUpdate)
{
_fixedEvaluationSettings.Mode = mode;
}
else if (target == EEvaluationTarget.RenderUpdate)
{
_renderEvaluationSettings.Mode = mode;
}
else
{
throw new NotImplementedException($"{target}");
}
}
/// <summary>
/// Set interlaced evaluation for given target.
/// <param name="target">Evaluation target.</param>
/// <param name="frames">Evaluation runs once per [frames] fixed/render updates.</param>
/// <param name="seed">Initial frame offset to spread evaluation over multiple ticks/frames.</param>
/// </summary>
public void SetInterlacedEvaluation(EEvaluationTarget target, int frames, int seed)
{
if (frames < 0)
throw new ArgumentException(nameof(frames));
if (target == EEvaluationTarget.FixedUpdate)
{
_fixedEvaluationSettings.Mode = EEvaluationMode.Interlaced;
_fixedEvaluationSettings.Frames = frames;
_fixedEvaluationSettings.Seed = seed % frames;
}
else if (target == EEvaluationTarget.RenderUpdate)
{
_renderEvaluationSettings.Mode = EEvaluationMode.Interlaced;
_renderEvaluationSettings.Frames = frames;
_renderEvaluationSettings.Seed = seed % frames;
}
else
{
throw new NotImplementedException($"{target}");
}
}
/// <summary>
/// Set periodic evaluation for given target.
/// <param name="target">Evaluation target.</param>
/// <param name="period">Evaluation runs once per [period] seconds.</param>
/// <param name="setInitialOffset">If true, the current offset will be reset to [initialOffset].</param>
/// <param name="initialOffset">Initial time offset to start with.</param>
/// </summary>
public void SetPeriodicEvaluation(EEvaluationTarget target, float period, bool setInitialOffset = false, float initialOffset = 0.0f)
{
if (period <= 0.0f)
throw new ArgumentException(nameof(period));
if (setInitialOffset == true && initialOffset < 0.0f)
throw new ArgumentException(nameof(initialOffset));
if (target == EEvaluationTarget.FixedUpdate)
{
_fixedEvaluationSettings.Mode = EEvaluationMode.Periodic;
_fixedEvaluationSettings.Period = period;
if (setInitialOffset == true)
{
_fixedEvaluationSettings.Offset = initialOffset;
}
}
else if (target == EEvaluationTarget.RenderUpdate)
{
_renderEvaluationSettings.Mode = EEvaluationMode.Periodic;
_renderEvaluationSettings.Period = period;
if (setInitialOffset == true)
{
_renderEvaluationSettings.Offset = initialOffset;
}
}
else
{
throw new NotImplementedException($"{target}");
}
}
/// <summary>
/// Enable/disable evaluation on resimulation. This is important if evaluated bones have direct impact on following logic.
/// <param name="evaluateOnResimulation">If true, evaluation on resimulation will be enabled.</param>
/// </summary>
public void SetEvaluateOnResimulation(bool evaluateOnResimulation)
{
_evaluateOnResimulation = evaluateOnResimulation;
}
/// <summary>
/// Set Animator target for evaluation.
/// </summary>
public void SetAnimator(Animator animator)
{
_animator = animator;
if (_graph.IsValid() == false)
return;
if (_output.IsOutputValid() == true)
{
_graph.DestroyOutput(_output);
_output = default;
}
if (_animator != null)
{
_output = AnimationPlayableOutput.Create(_graph, name, _animator);
_output.SetSourcePlayable(_mixer);
}
}
/// <summary>
/// Controls whether update methods are driven by default Fusion methods or called manually using <c>ManualFixedUpdate()</c> and <c>ManualRenderUpdate()</c>.
/// </summary>
public void SetManualUpdate(bool hasManualUpdate)
{
_hasManualUpdate = hasManualUpdate;
}
/// <summary>
/// Manual fixed update execution, <c>SetManualUpdate(true)</c> must be called prior usage.
/// </summary>
public void ManualFixedUpdate()
{
if (_isSpawned == false)
return;
if (_hasManualUpdate == false)
throw new InvalidOperationException("Manual update is not set!");
_fixedUpdateMarker.Begin();
OnFixedUpdateInternal();
_fixedUpdateMarker.End();
}
/// <summary>
/// Manual render update execution, <c>SetManualUpdate(true)</c> must be called prior usage.
/// </summary>
public void ManualRenderUpdate()
{
if (_isSpawned == false)
return;
if (_hasManualUpdate == false)
throw new InvalidOperationException("Manual update is not set!");
_renderUpdateMarker.Begin();
OnRenderUpdateInternal();
_renderUpdateMarker.End();
}
/// <summary>
/// Returns first animation layer with the given name.
/// </summary>
/// <param name="layerName">Name of the layer.</param>
public AnimationLayer FindLayer(string layerName)
{
return FindLayer<AnimationLayer>(layerName);
}
/// <summary>
/// Returns first animation layer of type <c>T</c>.
/// </summary>
public T FindLayer<T>() where T : class
{
return FindLayer(default, out T layer) == true ? layer : default;
}
/// <summary>
/// Returns first animation layer of type <c>T</c> with the given name.
/// </summary>
/// <param name="layerName">Name of the layer.</param>
public T FindLayer<T>(string layerName) where T : class
{
return FindLayer(layerName, out T layer) == true ? layer : default;
}
/// <summary>
/// Returns first animation layer of type <c>T</c>.
/// </summary>
/// <param name="layer">Reference to the layer.</param>
public bool FindLayer<T>(out T layer) where T : class
{
return FindLayer(default, out layer);
}
/// <summary>
/// Returns first animation layer of type <c>T</c> with the given name.
/// </summary>
/// <param name="layerName">Name of the layer.</param>
/// <param name="layer">Reference to the layer.</param>
public bool FindLayer<T>(string layerName, out T layer) where T : class
{
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer animationLayer = layers[i];
if (animationLayer is T layerAsT && (layerName == default || string.CompareOrdinal(animationLayer.name, layerName) == 0))
{
layer = layerAsT;
return true;
}
}
layer = default;
return false;
}
/// <summary>
/// Returns first animation state with the given name, using Breadth-First Search.
/// </summary>
/// <param name="stateName">Name of the state.</param>
public AnimationState FindState(string stateName)
{
return FindState(stateName, out AnimationState state) == true ? state : default;
}
/// <summary>
/// Returns first animation state of type <c>T</c>, using Breadth-First Search.
/// </summary>
public T FindState<T>() where T : class
{
return FindState(default, out T state) == true ? state : default;
}
/// <summary>
/// Returns first animation state of type <c>T</c> with the given name, using Breadth-First Search.
/// </summary>
/// <param name="stateName">Name of the state.</param>
public T FindState<T>(string stateName) where T : class
{
return FindState(stateName, out T state) == true ? state : default;
}
/// <summary>
/// Returns first animation state of type <c>T</c>, using Breadth-First Search.
/// </summary>
/// <param name="state">Reference to the state.</param>
public bool FindState<T>(out T state) where T : class
{
return FindState(default, out state);
}
/// <summary>
/// Returns first animation state of type <c>T</c> with the given name, using Breadth-First Search.
/// </summary>
/// <param name="stateName">Name of the state.</param>
/// <param name="state">Reference to the state.</param>
public bool FindState<T>(string stateName, out T state) where T : class
{
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
if (layer.FindState(stateName, out state, true) == true)
return true;
}
state = default;
return false;
}
/// <summary>
/// Logs info message into console with frame/tick metadata.
/// </summary>
/// <param name="messages">Custom message objects.</param>
public void Log(params object[] messages)
{
AnimationUtility.Log(this, default, EAnimationLogType.Info, messages);
}
/// <summary>
/// Logs warning message into console with frame/tick metadata.
/// </summary>
/// <param name="messages">Custom message objects.</param>
public void LogWarning(params object[] messages)
{
AnimationUtility.Log(this, default, EAnimationLogType.Warning, messages);
}
/// <summary>
/// Logs error message into console with frame/tick metadata.
/// </summary>
/// <param name="messages">Custom message objects.</param>
public void LogError(params object[] messages)
{
AnimationUtility.Log(this, default, EAnimationLogType.Error, messages);
}
// AnimationController INTERFACE
protected virtual void OnInitialize() {}
protected virtual void OnDeinitialize() {}
protected virtual void OnSpawned() {}
protected virtual void OnDespawned() {}
protected virtual void OnFixedUpdate() {}
protected virtual void OnInterpolate() {}
protected virtual void OnEvaluate() {}
// NetworkBehaviour INTERFACE
public override sealed int? DynamicWordCount => GetNetworkDataWordCount();
public override sealed void Spawned()
{
_isSpawned = true;
_deltaTime = Runner.DeltaTime;
_graph = PlayableGraph.Create(name);
_graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
_mixer = AnimationLayerMixerPlayable.Create(_graph);
_output = AnimationPlayableOutput.Create(_graph, name, _animator);
_output.SetSourcePlayable(_mixer);
if (Object.HasStateAuthority == false)
{
ReadNetworkData();
}
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
layer.Spawned();
}
OnSpawned();
}
public override sealed void Despawned(NetworkRunner runner, bool hasState)
{
_isSpawned = false;
OnDespawned();
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers != null ? layers.Length : 0; i < count; ++i)
{
AnimationLayer layer = layers[i];
if (layer != null)
{
layer.Despawned();
}
}
if (_graph.IsValid() == true)
{
_graph.Destroy();
}
_hasManualUpdate = default;
_evaluateOnResimulation = default;
_fixedEvaluationSettings.Reset();
_renderEvaluationSettings.Reset();
}
public override sealed void FixedUpdateNetwork()
{
if (_hasManualUpdate == true)
return;
_fixedUpdateMarker.Begin();
OnFixedUpdateInternal();
_fixedUpdateMarker.End();
}
public override sealed void Render()
{
if (_hasManualUpdate == true)
return;
_renderUpdateMarker.Begin();
OnRenderUpdateInternal();
_renderUpdateMarker.End();
}
// IAfterSpawned INTERFACE
void IAfterSpawned.AfterSpawned()
{
if (Object.IsInSimulation == true)
{
WriteNetworkData();
}
}
// IAfterClientPredictionReset INTERFACE
void IAfterClientPredictionReset.AfterClientPredictionReset()
{
_restoreStateMarker.Begin();
ReadNetworkData();
_restoreStateMarker.End();
}
// IAfterTick INTERFACE
void IAfterTick.AfterTick()
{
_afterTickMarker.Begin();
WriteNetworkData();
_afterTickMarker.End();
}
// MonoBehaviour INTERFACE
protected virtual void Awake()
{
InitializeLayers();
InitializeNetworkProperties();
OnInitialize();
}
protected virtual void OnDestroy()
{
if (_isSpawned == true)
{
Despawned(null, false);
}
OnDeinitialize();
DeinitializeNetworkProperties();
DeinitializeLayers();
}
// PRIVATE METHODS
private void InitializeLayers()
{
if (_layers != null)
return;
List<AnimationLayer> activeLayers = new List<AnimationLayer>(8);
Transform root = _root;
for (int i = 0, count = root.childCount; i < count; ++i)
{
Transform child = root.GetChild(i);
AnimationLayer layer = child.GetComponentNoAlloc<AnimationLayer>();
if (layer != null && layer.enabled == true && layer.gameObject.activeSelf == true)
{
activeLayers.Add(layer);
}
}
_layers = activeLayers.ToArray();
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
layer.Initialize(this);
}
}
private void DeinitializeLayers()
{
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers != null ? layers.Length : 0; i < count; ++i)
{
AnimationLayer layer = layers[i];
if (layer != null)
{
layer.Deinitialize();
}
}
_layers = null;
}
private void OnFixedUpdateInternal()
{
if (Runner.Stage == default)
throw new InvalidOperationException();
_deltaTime = Runner.DeltaTime;
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
layer.ManualFixedUpdate();
}
OnFixedUpdate();
if (_fixedEvaluationSettings.Mode != EEvaluationMode.None && (_evaluateOnResimulation == true || Runner.IsResimulation == false))
{
bool evaluate = true;
if (_fixedEvaluationSettings.Mode == EEvaluationMode.Interlaced && _fixedEvaluationSettings.Frames > 1)
{
int targetSeed = Runner.Tick.Raw % _fixedEvaluationSettings.Frames;
if (_fixedEvaluationSettings.Seed != targetSeed)
{
evaluate = false;
}
}
else if (_fixedEvaluationSettings.Mode == EEvaluationMode.Periodic)
{
float deltaTime = Runner.DeltaTime;
float simulationTime = Runner.SimulationTime + _fixedEvaluationSettings.Offset;
float timeSincePeriod = simulationTime % _fixedEvaluationSettings.Period;
if (timeSincePeriod > deltaTime)
{
evaluate = false;
}
}
if (evaluate == true)
{
EvaluateInternal(false);
}
}
}
private void OnRenderUpdateInternal()
{
if (Runner.Stage != default)
throw new InvalidOperationException();
_deltaTime = Time.deltaTime;
if (_renderEvaluationSettings.Mode != EEvaluationMode.None)
{
bool evaluate = true;
if (_renderEvaluationSettings.Mode == EEvaluationMode.Interlaced && _renderEvaluationSettings.Frames > 1)
{
int targetSeed = Time.frameCount % _renderEvaluationSettings.Frames;
if (_renderEvaluationSettings.Seed != targetSeed)
{
evaluate = false;
}
}
else if (_renderEvaluationSettings.Mode == EEvaluationMode.Periodic)
{
_renderEvaluationSettings.Offset += Time.deltaTime;
if (_renderEvaluationSettings.Offset > _renderEvaluationSettings.Period)
{
_renderEvaluationSettings.Offset %= _renderEvaluationSettings.Period;
}
else
{
evaluate = false;
}
}
if (evaluate == true)
{
InterpolateInternal();
EvaluateInternal(true);
}
}
}
private void InterpolateInternal()
{
_interpolateMarker.Begin();
InterpolateNetworkData();
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
layer.Interpolate();
}
OnInterpolate();
_interpolateMarker.End();
}
private void EvaluateInternal(bool interpolated)
{
_evaluateMarker.Begin();
AnimationLayer[] layers = _layers;
for (int i = 0, count = layers.Length; i < count; ++i)
{
AnimationLayer layer = layers[i];
layer.SetPlayableInputWeights(interpolated);
}
_evaluateGraphMarker.Begin();
_graph.Evaluate();
_evaluateGraphMarker.End();
OnEvaluate();
_evaluateMarker.End();
}
private sealed class EvaluationSettings
{
public EEvaluationMode Mode = EEvaluationMode.Full;
public int Frames;
public int Seed;
public float Period;
public float Offset;
public void Reset()
{
Mode = EEvaluationMode.Full;
Frames = default;
Seed = default;
Period = default;
Offset = default;
}
}
}
}