615 lines
22 KiB
C#
615 lines
22 KiB
C#
using System;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace AibisDream.FrameAnimation
|
|
{
|
|
[DisallowMultipleComponent]
|
|
public sealed class FrameClipPlayer : MonoBehaviour
|
|
{
|
|
[SerializeField] private FrameClip clip;
|
|
[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 Action<Sprite> applySpriteCallback;
|
|
private bool skipInitialPlayOnEnable;
|
|
private bool timelineControlled;
|
|
private FrameClip timelineClip;
|
|
private Sprite timelineBaselineSprite;
|
|
private bool timelineBaselineEnabled;
|
|
|
|
public FrameClip Clip => clip;
|
|
public bool PlayOnEnable => playOnEnable;
|
|
public FrameAnimationPlaybackState State => state;
|
|
public FrameClip CurrentClip => session?.CurrentClip;
|
|
public string CurrentClipId => session?.CurrentClipId ?? 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) TryAutoPlay();
|
|
}
|
|
|
|
private void TryAutoPlay()
|
|
{
|
|
var handle = Play();
|
|
if (handle.IsCompleted && handle.Result.Reason == FrameAnimationCompletionReason.Failed)
|
|
{
|
|
Debug.LogError($"FrameClipPlayer 自动播放失败:{handle.Result.Error}", this);
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (timelineControlled) return;
|
|
Evaluate(Time.deltaTime);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
ReleaseTimelineControl(null);
|
|
TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
ReleaseTimelineControl(null);
|
|
TerminateActive(FrameAnimationCompletionReason.Stopped, FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play()
|
|
{
|
|
return PlayInternal(clip, default);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play(FrameAnimationPlayOptions options)
|
|
{
|
|
return PlayInternal(clip, options);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play(FrameClip value)
|
|
{
|
|
return PlayInternal(value, default);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle Play(FrameClip value, FrameAnimationPlayOptions options)
|
|
{
|
|
return PlayInternal(value, options);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle RestoreTerminalState()
|
|
{
|
|
return RestoreTerminalState(clip);
|
|
}
|
|
|
|
public FrameAnimationPlaybackHandle RestoreTerminalState(FrameClip value)
|
|
{
|
|
ReleaseTimelineControl(null);
|
|
var requestId = Interlocked.Increment(ref nextRequestId);
|
|
var handle = new FrameAnimationPlaybackHandle(requestId);
|
|
var playableId = value != null ? value.Id : string.Empty;
|
|
if (!TryPrepare(value, default, handle, out var plan, out var resolvedTarget)) return handle;
|
|
|
|
if (activeHandle != null && !activeHandle.IsCompleted)
|
|
{
|
|
CompleteHandle(activeHandle, CurrentClipId, FrameAnimationCompletionReason.Replaced,
|
|
FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
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(ApplySpriteCallback);
|
|
return handle;
|
|
}
|
|
|
|
ApplySprite(terminalStep.Clip.Frames[terminalStep.Clip.FrameCount - 1]?.Sprite);
|
|
ApplyEndBehavior(terminalStep.TerminalEndBehavior);
|
|
session = null;
|
|
activeHandle = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
CompleteHandle(handle, playableId, FrameAnimationCompletionReason.Completed,
|
|
FrameAnimationPlaybackError.None);
|
|
return handle;
|
|
}
|
|
|
|
public CustomYieldInstruction WaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
|
{
|
|
if (playbackHandle == null) throw new ArgumentNullException(nameof(playbackHandle));
|
|
return new FirstPassYieldInstruction(this, playbackHandle);
|
|
}
|
|
|
|
public void Stop(FrameAnimationStopMode mode = FrameAnimationStopMode.HoldCurrentFrame)
|
|
{
|
|
ReleaseTimelineControl(null);
|
|
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($"FrameClipPlayer speed 必须是有限且不小于 0 的数值,收到:{value}", this);
|
|
return;
|
|
}
|
|
speed = value;
|
|
}
|
|
|
|
internal void ConfigureForAuthoring(FrameClip value, bool shouldPlayOnEnable, float playerSpeed)
|
|
{
|
|
clip = value;
|
|
playOnEnable = shouldPlayOnEnable;
|
|
speed = playerSpeed;
|
|
}
|
|
|
|
internal void EvaluateForTests(double deltaTimeSeconds)
|
|
{
|
|
Evaluate(deltaTimeSeconds);
|
|
}
|
|
|
|
internal FrameAnimationPlaybackError BeginTimelineControl()
|
|
{
|
|
if (timelineControlled)
|
|
{
|
|
return FrameAnimationPlaybackError.None;
|
|
}
|
|
|
|
if (!TryCreateTarget(out var resolvedTarget, out var targetError))
|
|
{
|
|
return targetError;
|
|
}
|
|
|
|
TerminateActive(
|
|
FrameAnimationCompletionReason.Replaced,
|
|
FrameAnimationPlaybackError.None);
|
|
target = resolvedTarget;
|
|
timelineBaselineSprite = target.Sprite;
|
|
timelineBaselineEnabled = target.Enabled;
|
|
timelineClip = null;
|
|
timelineControlled = true;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
return FrameAnimationPlaybackError.None;
|
|
}
|
|
|
|
internal FrameAnimationPlaybackError SampleTimeline(FrameClip value, double timeSeconds)
|
|
{
|
|
var beginError = BeginTimelineControl();
|
|
if (beginError.Code != FrameAnimationPlaybackErrorCode.None)
|
|
{
|
|
return beginError;
|
|
}
|
|
|
|
if (value == null)
|
|
{
|
|
return new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.PlayableNotFound,
|
|
"Timeline Clip 没有绑定 FrameClip。");
|
|
}
|
|
|
|
if (timelineClip != value || session == null)
|
|
{
|
|
if (!FrameAnimationResolver.TryResolveClip(
|
|
value,
|
|
default,
|
|
out var plan,
|
|
out var resolveError))
|
|
{
|
|
session = null;
|
|
timelineClip = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
return resolveError;
|
|
}
|
|
|
|
timelineClip = value;
|
|
session = new FrameAnimationPlaybackSession(plan);
|
|
}
|
|
|
|
if (!target.Enabled)
|
|
{
|
|
target.Enabled = true;
|
|
}
|
|
state = FrameAnimationPlaybackState.Playing;
|
|
var evaluation = session.Seek(Math.Max(0d, timeSeconds), ApplySpriteCallback);
|
|
if (evaluation.IsFailed)
|
|
{
|
|
session = null;
|
|
timelineClip = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
return evaluation.Error;
|
|
}
|
|
|
|
if (evaluation.IsCompleted)
|
|
{
|
|
ApplyEndBehavior(evaluation.EndBehavior);
|
|
}
|
|
|
|
return FrameAnimationPlaybackError.None;
|
|
}
|
|
|
|
internal FrameAnimationPlaybackError ApplyTimelineEndBehavior(FrameClip value)
|
|
{
|
|
var beginError = BeginTimelineControl();
|
|
if (beginError.Code != FrameAnimationPlaybackErrorCode.None)
|
|
{
|
|
return beginError;
|
|
}
|
|
|
|
if (value == null)
|
|
{
|
|
return new FrameAnimationPlaybackError(
|
|
FrameAnimationPlaybackErrorCode.PlayableNotFound,
|
|
"Timeline Clip 没有绑定 FrameClip。");
|
|
}
|
|
|
|
if (value.DefaultEndBehavior == FrameClipEndBehavior.Loop)
|
|
{
|
|
if (timelineClip == value && session != null)
|
|
{
|
|
return FrameAnimationPlaybackError.None;
|
|
}
|
|
|
|
var duration = GetEffectiveClipDurationSeconds(value);
|
|
return SampleTimeline(value, Math.Max(0d, duration - 0.000001d));
|
|
}
|
|
|
|
if (value.DefaultEndBehavior == FrameClipEndBehavior.Clear ||
|
|
value.DefaultEndBehavior == FrameClipEndBehavior.HideTarget)
|
|
{
|
|
ApplyEndBehavior(value.DefaultEndBehavior);
|
|
return FrameAnimationPlaybackError.None;
|
|
}
|
|
|
|
var sampleError = SampleTimeline(value, GetEffectiveClipDurationSeconds(value));
|
|
if (sampleError.Code == FrameAnimationPlaybackErrorCode.None)
|
|
{
|
|
ApplyEndBehavior(value.DefaultEndBehavior);
|
|
}
|
|
return sampleError;
|
|
}
|
|
|
|
internal void RestoreTimelineBaseline()
|
|
{
|
|
if (!timelineControlled || target == null || !target.IsValid) return;
|
|
ApplySprite(timelineBaselineSprite);
|
|
if (target.Enabled != timelineBaselineEnabled)
|
|
{
|
|
target.Enabled = timelineBaselineEnabled;
|
|
}
|
|
session = null;
|
|
timelineClip = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
}
|
|
|
|
internal void ReleaseTimelineControl(FrameClip terminalClip)
|
|
{
|
|
if (!timelineControlled) return;
|
|
|
|
if (terminalClip != null)
|
|
{
|
|
ApplyTimelineEndBehavior(terminalClip);
|
|
}
|
|
else
|
|
{
|
|
RestoreTimelineBaseline();
|
|
}
|
|
|
|
timelineControlled = false;
|
|
timelineClip = null;
|
|
session = null;
|
|
state = FrameAnimationPlaybackState.Stopped;
|
|
}
|
|
|
|
private static double GetEffectiveClipDurationSeconds(FrameClip value)
|
|
{
|
|
if (value == null || value.TotalDurationMs <= 0 || value.Speed <= 0f)
|
|
{
|
|
return 0d;
|
|
}
|
|
return value.TotalDurationMs / 1000d / value.Speed;
|
|
}
|
|
|
|
private void Evaluate(double deltaTimeSeconds)
|
|
{
|
|
if (state != FrameAnimationPlaybackState.Playing || session == null) return;
|
|
if (target == null || !target.IsValid)
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.TargetMissing, "播放期间显示目标失效。");
|
|
return;
|
|
}
|
|
if (session.CurrentClip == null)
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.InvalidPlayableData, "播放期间 Clip 数据失效。");
|
|
return;
|
|
}
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
FailActive(FrameAnimationPlaybackErrorCode.InvalidSpeed, "播放期间 Player speed 变为无效值。");
|
|
return;
|
|
}
|
|
|
|
var evaluation = session.Evaluate(deltaTimeSeconds, speed, ApplySpriteCallback);
|
|
if (evaluation.IsFailed) FailActive(evaluation.Error.Code, evaluation.Error.Message);
|
|
else if (evaluation.IsCompleted) CompleteNaturally(evaluation.EndBehavior);
|
|
}
|
|
|
|
private FrameAnimationPlaybackHandle PlayInternal(FrameClip value, FrameAnimationPlayOptions options)
|
|
{
|
|
ReleaseTimelineControl(null);
|
|
var handle = new FrameAnimationPlaybackHandle(Interlocked.Increment(ref nextRequestId));
|
|
if (!TryPrepare(value, options, handle, out var plan, out var resolvedTarget)) return handle;
|
|
|
|
if (activeHandle != null && !activeHandle.IsCompleted)
|
|
{
|
|
CompleteHandle(activeHandle, CurrentClipId, FrameAnimationCompletionReason.Replaced,
|
|
FrameAnimationPlaybackError.None);
|
|
}
|
|
|
|
session = new FrameAnimationPlaybackSession(plan);
|
|
activeHandle = handle;
|
|
state = FrameAnimationPlaybackState.Playing;
|
|
target = resolvedTarget;
|
|
target.Enabled = true;
|
|
session.Start(ApplySpriteCallback);
|
|
return handle;
|
|
}
|
|
|
|
private bool TryPrepare(
|
|
FrameClip value,
|
|
FrameAnimationPlayOptions options,
|
|
FrameAnimationPlaybackHandle handle,
|
|
out ResolvedPlaybackPlan plan,
|
|
out IFrameAnimationTarget resolvedTarget)
|
|
{
|
|
plan = null;
|
|
resolvedTarget = null;
|
|
var playableId = value != null ? value.Id : string.Empty;
|
|
if (!isActiveAndEnabled)
|
|
{
|
|
CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.PlayerNotReady,
|
|
"FrameClipPlayer 未启用或 GameObject 未激活。");
|
|
return false;
|
|
}
|
|
if (value == null)
|
|
{
|
|
CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.PlayableNotFound,
|
|
"FrameClipPlayer 没有可播放的 FrameClip。");
|
|
return false;
|
|
}
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
CompleteFailed(handle, playableId, FrameAnimationPlaybackErrorCode.InvalidSpeed,
|
|
"FrameClipPlayer speed 无效。");
|
|
return false;
|
|
}
|
|
if (!TryCreateTarget(out resolvedTarget, out var targetError))
|
|
{
|
|
CompleteFailed(handle, playableId, targetError.Code, targetError.Message);
|
|
return false;
|
|
}
|
|
if (!FrameAnimationResolver.TryResolveClip(value, options, out plan, out var resolveError))
|
|
{
|
|
CompleteFailed(handle, playableId, resolveError.Code, resolveError.Message);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool TryCreateTarget(out IFrameAnimationTarget resolvedTarget, out FrameAnimationPlaybackError error)
|
|
{
|
|
if (GetComponent<FrameAnimationPlayer>() != null)
|
|
{
|
|
resolvedTarget = null;
|
|
error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetConflict,
|
|
"同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。");
|
|
return false;
|
|
}
|
|
var spriteRenderer = GetComponent<SpriteRenderer>();
|
|
var image = GetComponent<Image>();
|
|
if (spriteRenderer != null && image != null)
|
|
{
|
|
resolvedTarget = null;
|
|
error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetConflict,
|
|
"FrameClipPlayer 所在对象不能同时包含 SpriteRenderer 和 Image。");
|
|
return false;
|
|
}
|
|
if (spriteRenderer == null && image == null)
|
|
{
|
|
resolvedTarget = null;
|
|
error = new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.TargetMissing,
|
|
"FrameClipPlayer 所在对象缺少 SpriteRenderer 或 Image。");
|
|
return false;
|
|
}
|
|
resolvedTarget = spriteRenderer != null
|
|
? new SpriteRendererFrameAnimationTarget(spriteRenderer)
|
|
: new ImageFrameAnimationTarget(image);
|
|
error = FrameAnimationPlaybackError.None;
|
|
return true;
|
|
}
|
|
|
|
private static 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 = CurrentClipId;
|
|
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 = CurrentClipId;
|
|
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 = CurrentClipId;
|
|
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)
|
|
{
|
|
target.Sprite = sprite;
|
|
}
|
|
}
|
|
|
|
private Action<Sprite> ApplySpriteCallback =>
|
|
applySpriteCallback ??= ApplySprite;
|
|
|
|
private bool ShouldWaitForFirstPass(FrameAnimationPlaybackHandle playbackHandle)
|
|
{
|
|
return !playbackHandle.IsCompleted && activeHandle == playbackHandle && session != null &&
|
|
!session.HasCompletedFirstTerminalLoopCycle;
|
|
}
|
|
|
|
private sealed class FirstPassYieldInstruction : CustomYieldInstruction
|
|
{
|
|
private readonly FrameClipPlayer player;
|
|
private readonly FrameAnimationPlaybackHandle playbackHandle;
|
|
|
|
public FirstPassYieldInstruction(FrameClipPlayer player, FrameAnimationPlaybackHandle playbackHandle)
|
|
{
|
|
this.player = player;
|
|
this.playbackHandle = playbackHandle;
|
|
}
|
|
|
|
public override bool keepWaiting =>
|
|
player != null && player.ShouldWaitForFirstPass(playbackHandle);
|
|
}
|
|
|
|
private void ApplyEndBehavior(FrameClipEndBehavior endBehavior)
|
|
{
|
|
switch (endBehavior)
|
|
{
|
|
case FrameClipEndBehavior.Clear:
|
|
if (target != null && target.IsValid)
|
|
{
|
|
if (!target.Enabled)
|
|
{
|
|
target.Enabled = true;
|
|
}
|
|
ApplySprite(null);
|
|
}
|
|
break;
|
|
case FrameClipEndBehavior.HideTarget:
|
|
if (target != null && target.IsValid && target.Enabled)
|
|
{
|
|
target.Enabled = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ApplyStopMode(FrameAnimationStopMode mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case FrameAnimationStopMode.Clear:
|
|
if (!target.Enabled)
|
|
{
|
|
target.Enabled = true;
|
|
}
|
|
ApplySprite(null);
|
|
break;
|
|
case FrameAnimationStopMode.HideTarget:
|
|
if (target.Enabled)
|
|
{
|
|
target.Enabled = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|