541 lines
18 KiB
C#
541 lines
18 KiB
C#
using System.Threading;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace AibisDream.FrameAnimation
|
|
{
|
|
[DisallowMultipleComponent]
|
|
public sealed class FrameAnimationPlayer : MonoBehaviour
|
|
{
|
|
[SerializeField] private FrameAnimationGraph graph;
|
|
[SerializeField] private bool playOnEnable;
|
|
[SerializeField] private float speed = 1f;
|
|
|
|
private static long nextRequestId;
|
|
|
|
private IFrameAnimationTarget target;
|
|
private FrameAnimationPlaybackSession session;
|
|
private FrameAnimationPlaybackHandle activeHandle;
|
|
private FrameAnimationPlaybackState state = FrameAnimationPlaybackState.Stopped;
|
|
private bool _skipInitialPlayOnEnable;
|
|
private bool hasStartedPlayback;
|
|
|
|
public FrameAnimationGraph Graph => graph;
|
|
public bool PlayOnEnable => playOnEnable;
|
|
public FrameAnimationPlaybackState State => state;
|
|
public string CurrentPlayableId => session?.PlayableId ?? string.Empty;
|
|
public string CurrentClipId => session?.CurrentClipId ?? string.Empty;
|
|
public string CurrentNodeId => session?.CurrentNodeId ?? string.Empty;
|
|
public int CurrentFrameIndex => session?.CurrentFrameIndex ?? -1;
|
|
public float Speed => speed;
|
|
|
|
private void Awake()
|
|
{
|
|
_skipInitialPlayOnEnable = playOnEnable;
|
|
|
|
if (TryCreateTarget(out var resolvedTarget, out _))
|
|
{
|
|
target = resolvedTarget;
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (!playOnEnable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_skipInitialPlayOnEnable)
|
|
{
|
|
_skipInitialPlayOnEnable = false;
|
|
return;
|
|
}
|
|
|
|
TryAutoPlay();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (!playOnEnable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TryAutoPlay();
|
|
}
|
|
|
|
private void TryAutoPlay()
|
|
{
|
|
var handle = Play();
|
|
if (handle.IsCompleted && handle.Result.Reason == FrameAnimationCompletionReason.Failed)
|
|
{
|
|
Debug.LogError($"FrameAnimationPlayer 自动播放失败:{handle.Result.Error}", this);
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (state != FrameAnimationPlaybackState.Playing || session == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (target == null || !target.IsValid)
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.TargetMissing, "播放期间显示目标失效。");
|
|
return;
|
|
}
|
|
|
|
if (graph == null || session.CurrentClip == null)
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Graph 或 Clip 数据失效。");
|
|
return;
|
|
}
|
|
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.InvalidSpeed, "播放期间 Player speed 变为无效值。");
|
|
return;
|
|
}
|
|
|
|
var evaluation = session.Evaluate(Time.deltaTime, speed, ApplySprite);
|
|
if (evaluation.IsFailed)
|
|
{
|
|
FailActive(evaluation.Error.Code, evaluation.Error.Message);
|
|
}
|
|
else if (evaluation.IsCompleted)
|
|
{
|
|
CompleteNaturally(evaluation.EndBehavior);
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play()
|
|
{
|
|
var playableId = graph?.Settings?.DefaultPlayableId ?? string.Empty;
|
|
return PlayInternal(playableId, default);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play(string playableId)
|
|
{
|
|
return PlayInternal(playableId, default);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play(string playableId, FrameAnimationPlayOptions options)
|
|
{
|
|
return PlayInternal(playableId, options);
|
|
}
|
|
|
|
public bool TryBindGraph(
|
|
FrameAnimationGraph value,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
if (value == null)
|
|
{
|
|
error = new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.GraphMissing,
|
|
"不能为 FrameAnimationPlayer 绑定空 Graph。");
|
|
return false;
|
|
}
|
|
|
|
if (hasStartedPlayback ||
|
|
state != FrameAnimationPlaybackState.Stopped ||
|
|
session != null ||
|
|
(activeHandle != null && !activeHandle.IsCompleted))
|
|
{
|
|
error = new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.GraphBindingNotAllowed,
|
|
"FrameAnimationPlayer 开始播放后不允许绑定或替换 Graph。");
|
|
return false;
|
|
}
|
|
|
|
if (graph != null && graph != value)
|
|
{
|
|
error = new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.GraphBindingNotAllowed,
|
|
"FrameAnimationPlayer 已绑定其他 Graph。");
|
|
return false;
|
|
}
|
|
|
|
graph = value;
|
|
error = FrameAnimationPlaybackError.None;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 非循环 playable 立即应用最后一帧及结束行为;Loop 从头重新播放。
|
|
/// </summary>
|
|
public FrameAnimationPlaybackHandle RestoreTerminalState(string playableId)
|
|
{
|
|
var requestId = Interlocked.Increment(ref nextRequestId);
|
|
var handle = new FrameAnimationPlaybackHandle(requestId);
|
|
|
|
if (!isActiveAndEnabled)
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.PlayerNotReady,
|
|
"FrameAnimationPlayer 未启用或 GameObject 未激活。");
|
|
}
|
|
|
|
if (graph == null)
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.GraphMissing,
|
|
"FrameAnimationPlayer 没有绑定 FrameAnimationGraph。");
|
|
}
|
|
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.InvalidSpeed,
|
|
"FrameAnimationPlayer speed 无效。");
|
|
}
|
|
|
|
if (!TryCreateTarget(out var resolvedTarget, out var targetError))
|
|
{
|
|
return CompleteFailed(handle, playableId, targetError.Code, targetError.Message);
|
|
}
|
|
|
|
if (!FrameAnimationResolver.TryResolve(
|
|
graph,
|
|
playableId,
|
|
default,
|
|
out var plan,
|
|
out var resolveError))
|
|
{
|
|
return CompleteFailed(handle, playableId, resolveError.Code, resolveError.Message);
|
|
}
|
|
|
|
if (activeHandle != null && !activeHandle.IsCompleted)
|
|
{
|
|
CompleteHandle(
|
|
activeHandle,
|
|
CurrentPlayableId,
|
|
FrameAnimationCompletionReason.Replaced,
|
|
FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
hasStartedPlayback = true;
|
|
target = resolvedTarget;
|
|
target.Enabled = true;
|
|
var terminalStep = plan.Steps[plan.Steps.Count - 1];
|
|
if (terminalStep.TerminalEndBehavior == FrameClipEndBehavior.Loop)
|
|
{
|
|
session = new FrameAnimationPlaybackSession(plan);
|
|
activeHandle = handle;
|
|
state = FrameAnimationPlaybackState.Playing;
|
|
session.Start(ApplySprite);
|
|
return handle;
|
|
}
|
|
|
|
var lastFrameIndex = terminalStep.Clip.FrameCount - 1;
|
|
ApplySprite(terminalStep.Clip.Frames[lastFrameIndex]?.Sprite);
|
|
ApplyEndBehavior(terminalStep.TerminalEndBehavior);
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
CompleteHandle(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationCompletionReason.Completed,
|
|
FrameAnimationPlaybackError.None);
|
|
return handle;
|
|
}
|
|
|
|
public void Stop(FrameAnimationStopMode mode = FrameAnimationStopMode.HoldCurrentFrame)
|
|
{
|
|
TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None);
|
|
|
|
if (target == null || !target.IsValid)
|
|
{
|
|
if (!TryCreateTarget(out var resolvedTarget, out _))
|
|
{
|
|
return;
|
|
}
|
|
|
|
target = resolvedTarget;
|
|
}
|
|
|
|
ApplyStopMode(mode);
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
if (state == FrameAnimationPlaybackState.Playing && session != null)
|
|
{
|
|
state = FrameAnimationPlaybackState.Paused;
|
|
}
|
|
}
|
|
|
|
public void Resume()
|
|
{
|
|
if (state == FrameAnimationPlaybackState.Paused && session != null)
|
|
{
|
|
state = FrameAnimationPlaybackState.Playing;
|
|
}
|
|
}
|
|
|
|
public void SetSpeed(float value)
|
|
{
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(value))
|
|
{
|
|
Debug.LogError($"FrameAnimationPlayer speed 必须是有限且不小于 0 的数值,收到:{value}", this);
|
|
return;
|
|
}
|
|
|
|
speed = value;
|
|
}
|
|
|
|
internal void ConfigureForAuthoring(FrameAnimationGraph value, bool shouldPlayOnEnable, float playerSpeed)
|
|
{
|
|
graph = value;
|
|
playOnEnable = shouldPlayOnEnable;
|
|
speed = playerSpeed;
|
|
hasStartedPlayback = false;
|
|
}
|
|
|
|
internal void EvaluateForTests(double deltaTimeSeconds)
|
|
{
|
|
if (state != FrameAnimationPlaybackState.Playing || session == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySprite);
|
|
if (evaluation.IsFailed)
|
|
{
|
|
FailActive(evaluation.Error.Code, evaluation.Error.Message);
|
|
}
|
|
else if (evaluation.IsCompleted)
|
|
{
|
|
CompleteNaturally(evaluation.EndBehavior);
|
|
}
|
|
}
|
|
|
|
private FrameAnimationPlaybackHandle PlayInternal(
|
|
string playableId,
|
|
FrameAnimationPlayOptions options)
|
|
{
|
|
var requestId = Interlocked.Increment(ref nextRequestId);
|
|
var handle = new FrameAnimationPlaybackHandle(requestId);
|
|
|
|
if (!isActiveAndEnabled)
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.PlayerNotReady,
|
|
"FrameAnimationPlayer 未启用或 GameObject 未激活。");
|
|
}
|
|
|
|
if (graph == null)
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.GraphMissing,
|
|
"FrameAnimationPlayer 没有绑定 FrameAnimationGraph。");
|
|
}
|
|
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
return CompleteFailed(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationPlaybackErrorCode.InvalidSpeed,
|
|
"FrameAnimationPlayer speed 无效。");
|
|
}
|
|
|
|
if (!TryCreateTarget(out var resolvedTarget, out var targetError))
|
|
{
|
|
return CompleteFailed(handle, playableId, targetError.Code, targetError.Message);
|
|
}
|
|
|
|
if (!FrameAnimationResolver.TryResolve(graph, playableId, options, out var plan, out var resolveError))
|
|
{
|
|
return CompleteFailed(handle, playableId, resolveError.Code, resolveError.Message);
|
|
}
|
|
|
|
hasStartedPlayback = true;
|
|
|
|
if (activeHandle != null && !activeHandle.IsCompleted)
|
|
{
|
|
CompleteHandle(activeHandle, CurrentPlayableId, FrameAnimationCompletionReason.Replaced, FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
session = new FrameAnimationPlaybackSession(plan);
|
|
activeHandle = handle;
|
|
state = FrameAnimationPlaybackState.Playing;
|
|
target = resolvedTarget;
|
|
target.Enabled = true;
|
|
session.Start(ApplySprite);
|
|
return handle;
|
|
}
|
|
|
|
private bool TryCreateTarget(
|
|
out IFrameAnimationTarget resolvedTarget,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
var spriteRenderer = GetComponent<SpriteRenderer>();
|
|
var image = GetComponent<Image>();
|
|
if (spriteRenderer != null && image != null)
|
|
{
|
|
resolvedTarget = null;
|
|
error = new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.TargetConflict,
|
|
"FrameAnimationPlayer 所在对象不能同时包含 SpriteRenderer 和 Image。");
|
|
return false;
|
|
}
|
|
|
|
if (spriteRenderer == null && image == null)
|
|
{
|
|
resolvedTarget = null;
|
|
error = new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.TargetMissing,
|
|
"FrameAnimationPlayer 所在对象缺少 SpriteRenderer 或 Image。");
|
|
return false;
|
|
}
|
|
|
|
resolvedTarget = spriteRenderer != null
|
|
? new SpriteRendererFrameAnimationTarget(spriteRenderer)
|
|
: new ImageFrameAnimationTarget(image);
|
|
error = FrameAnimationPlaybackError.None;
|
|
return true;
|
|
}
|
|
|
|
private FrameAnimationPlaybackHandle CompleteFailed(
|
|
FrameAnimationPlaybackHandle handle,
|
|
string playableId,
|
|
FrameAnimationPlaybackErrorCode code,
|
|
string message)
|
|
{
|
|
CompleteHandle(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationCompletionReason.Failed,
|
|
new FrameAnimationPlaybackError(code, message));
|
|
return handle;
|
|
}
|
|
|
|
private void CompleteNaturally(FrameClipEndBehavior endBehavior)
|
|
{
|
|
var handle = activeHandle;
|
|
var playableId = CurrentPlayableId;
|
|
ApplyEndBehavior(endBehavior);
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
CompleteHandle(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationCompletionReason.Completed,
|
|
FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
private void FailActive(FrameAnimationPlaybackErrorCode code, string message)
|
|
{
|
|
var handle = activeHandle;
|
|
var playableId = CurrentPlayableId;
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
CompleteHandle(
|
|
handle,
|
|
playableId,
|
|
FrameAnimationCompletionReason.Failed,
|
|
new FrameAnimationPlaybackError(code, message));
|
|
}
|
|
|
|
private void TerminateActive(
|
|
FrameAnimationCompletionReason reason,
|
|
FrameAnimationPlaybackError error)
|
|
{
|
|
if (activeHandle == null || activeHandle.IsCompleted)
|
|
{
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
return;
|
|
}
|
|
|
|
var handle = activeHandle;
|
|
var playableId = CurrentPlayableId;
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
CompleteHandle(handle, playableId, reason, error);
|
|
}
|
|
|
|
private static void CompleteHandle(
|
|
FrameAnimationPlaybackHandle handle,
|
|
string playableId,
|
|
FrameAnimationCompletionReason reason,
|
|
FrameAnimationPlaybackError error)
|
|
{
|
|
handle?.TryComplete(new FrameAnimationPlaybackResult(handle.RequestId, playableId, reason, error));
|
|
}
|
|
|
|
private void ApplySprite(Sprite sprite)
|
|
{
|
|
if (target != null && target.IsValid)
|
|
{
|
|
target.Sprite = sprite;
|
|
}
|
|
}
|
|
|
|
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
|
{
|
|
switch (endBehavior)
|
|
{
|
|
case FrameClipEndBehavior.HoldLastFrame:
|
|
break;
|
|
case FrameClipEndBehavior.Clear:
|
|
if (target != null && target.IsValid)
|
|
{
|
|
target.Enabled = true;
|
|
target.Sprite = null;
|
|
}
|
|
break;
|
|
case FrameClipEndBehavior.HideTarget:
|
|
if (target != null && target.IsValid)
|
|
{
|
|
target.Enabled = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ApplyStopMode(FrameAnimationStopMode mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case FrameAnimationStopMode.HoldCurrentFrame:
|
|
break;
|
|
case FrameAnimationStopMode.Clear:
|
|
target.Enabled = true;
|
|
target.Sprite = null;
|
|
break;
|
|
case FrameAnimationStopMode.HideTarget:
|
|
target.Enabled = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|