Files
aibis-dream/Assets/Scripts/FrameAnimation/Runtime/FrameClipPlayer.cs
T

584 lines
21 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 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(ApplySprite);
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);
session.Start(ApplySprite);
}
target.Enabled = true;
state = FrameAnimationPlaybackState.Playing;
var evaluation = session.Seek(Math.Max(0d, timeSeconds), ApplySprite);
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));
}
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;
target.Sprite = timelineBaselineSprite;
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, ApplySprite);
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(ApplySprite);
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;
}
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)
{
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.Clear:
target.Enabled = true;
target.Sprite = null;
break;
case FrameAnimationStopMode.HideTarget:
target.Enabled = false;
break;
}
}
}
}