feat: 帧动画系统Timeline兼容
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Editor")]
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Timeline")]
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.PlayMode")]
|
||||
|
||||
@@ -56,6 +56,34 @@ namespace AibisDream.FrameAnimation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Editor-time source settings owned by an external FrameClip asset.
|
||||
/// This is intentionally separate from graph-owned FrameClipImportInfo.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class FrameClipImportSource
|
||||
{
|
||||
[SerializeField] private Texture2D texture;
|
||||
[SerializeField] private TextAsset asepriteJson;
|
||||
[SerializeField] private Vector2 pivot = new Vector2(0.5f, 0.5f);
|
||||
[SerializeField] private bool manageSpriteSlicing;
|
||||
[SerializeField] private string lastSourceHash = string.Empty;
|
||||
[SerializeField] private string lastImportedTagName = string.Empty;
|
||||
|
||||
public Texture2D Texture => texture;
|
||||
public TextAsset AsepriteJson => asepriteJson;
|
||||
public Vector2 Pivot => pivot;
|
||||
public bool ManageSpriteSlicing => manageSpriteSlicing;
|
||||
public string LastSourceHash => lastSourceHash;
|
||||
public string LastImportedTagName => lastImportedTagName;
|
||||
|
||||
internal void SetLastImport(string sourceHash, string tagName)
|
||||
{
|
||||
lastSourceHash = sourceHash ?? string.Empty;
|
||||
lastImportedTagName = tagName ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class AnimationFlow
|
||||
{
|
||||
|
||||
@@ -407,6 +407,15 @@ namespace AibisDream.FrameAnimation
|
||||
out IFrameAnimationTarget resolvedTarget,
|
||||
out FrameAnimationPlaybackError error)
|
||||
{
|
||||
if (GetComponent<FrameClipPlayer>() != 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)
|
||||
|
||||
@@ -135,15 +135,14 @@ namespace AibisDream.FrameAnimation
|
||||
|
||||
if (clipMatches.Count == 1)
|
||||
{
|
||||
return TryResolveClip(clipMatches[0], playableId, options, out plan, out error);
|
||||
return TryResolveClip(clipMatches[0], options, out plan, out error);
|
||||
}
|
||||
|
||||
return TryResolveFlow(graph, flowMatches[0], options, out plan, out error);
|
||||
}
|
||||
|
||||
private static bool TryResolveClip(
|
||||
internal static bool TryResolveClip(
|
||||
FrameClip clip,
|
||||
string playableId,
|
||||
FrameAnimationPlayOptions options,
|
||||
out ResolvedPlaybackPlan plan,
|
||||
out FrameAnimationPlaybackError error)
|
||||
@@ -156,7 +155,7 @@ namespace AibisDream.FrameAnimation
|
||||
|
||||
var endBehavior = options.EndBehaviorOverride ?? clip.DefaultEndBehavior;
|
||||
plan = new ResolvedPlaybackPlan(
|
||||
playableId,
|
||||
clip != null ? clip.Id : string.Empty,
|
||||
false,
|
||||
new[] { new ResolvedPlaybackStep(clip, string.Empty, clip.Speed, endBehavior) });
|
||||
return true;
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace AibisDream.FrameAnimation
|
||||
[SerializeField] private FrameClipEndBehavior defaultEndBehavior = FrameClipEndBehavior.HoldLastFrame;
|
||||
[SerializeField] private bool hasImportInfo;
|
||||
[SerializeField] private FrameClipImportInfo importInfo;
|
||||
[SerializeField] private bool hasStandaloneImportSource;
|
||||
[SerializeField] private FrameClipImportSource standaloneImportSource;
|
||||
|
||||
public string Id => id;
|
||||
public string DisplayName => displayName;
|
||||
@@ -21,6 +23,9 @@ namespace AibisDream.FrameAnimation
|
||||
public FrameClipEndBehavior DefaultEndBehavior => defaultEndBehavior;
|
||||
public FrameClipImportInfo ImportInfo => hasImportInfo ? importInfo : null;
|
||||
public bool IsImported => ImportInfo != null;
|
||||
public FrameClipImportSource StandaloneImportSource =>
|
||||
hasStandaloneImportSource ? standaloneImportSource : null;
|
||||
public bool HasStandaloneImportSource => StandaloneImportSource != null;
|
||||
|
||||
public int FrameCount => frames?.Count ?? 0;
|
||||
|
||||
@@ -115,5 +120,18 @@ namespace AibisDream.FrameAnimation
|
||||
? new List<FrameAnimationFrame>(value)
|
||||
: new List<FrameAnimationFrame>();
|
||||
}
|
||||
|
||||
internal FrameClipImportSource GetOrCreateStandaloneImportSource()
|
||||
{
|
||||
standaloneImportSource ??= new FrameClipImportSource();
|
||||
hasStandaloneImportSource = true;
|
||||
return standaloneImportSource;
|
||||
}
|
||||
|
||||
internal void ClearStandaloneImportSource()
|
||||
{
|
||||
hasStandaloneImportSource = false;
|
||||
standaloneImportSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 192967735fd44d642bee1a6c5ba29717
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66e7d3aa7a5d4bf3838316bbebed8117
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "AibisDream.FrameAnimation.Timeline",
|
||||
"rootNamespace": "AibisDream.FrameAnimation.Timeline",
|
||||
"references": [
|
||||
"AibisDream.FrameAnimation.Runtime",
|
||||
"Unity.Timeline",
|
||||
"UnityEngine.UI"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a8d75ec8acb4b18ae787ed85e8401b8
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.PlayMode")]
|
||||
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Timeline.Editor")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df7035ef1c2d40218ccf85f294e34735
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b32641aa64c6449994fe91525598e451
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "AibisDream.FrameAnimation.Timeline.Editor",
|
||||
"rootNamespace": "AibisDream.FrameAnimation.Timeline.Editor",
|
||||
"references": [
|
||||
"AibisDream.FrameAnimation.Runtime",
|
||||
"AibisDream.FrameAnimation.Timeline",
|
||||
"Unity.Timeline",
|
||||
"Unity.Timeline.Editor"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dcd49d5096747bda63e1b94c699b67b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using UnityEditor.Timeline;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Timeline.Editor
|
||||
{
|
||||
[CustomTimelineEditor(typeof(FrameAnimationTimelineClip))]
|
||||
internal sealed class FrameAnimationTimelineClipEditor : ClipEditor
|
||||
{
|
||||
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
|
||||
{
|
||||
base.OnCreate(clip, track, clonedFrom);
|
||||
SynchronizeSource(clip);
|
||||
}
|
||||
|
||||
public override void OnClipChanged(TimelineClip clip)
|
||||
{
|
||||
base.OnClipChanged(clip);
|
||||
SynchronizeSource(clip);
|
||||
}
|
||||
|
||||
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
|
||||
{
|
||||
var options = base.GetClipOptions(clip);
|
||||
if (clip?.asset is not FrameAnimationTimelineClip asset)
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
if (asset.FrameClip == null)
|
||||
{
|
||||
options.errorText = "未指定 FrameClip";
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void SynchronizeSource(TimelineClip clip)
|
||||
{
|
||||
if (clip?.asset is not FrameAnimationTimelineClip asset ||
|
||||
!asset.TrySynchronizeSourceDuration(out var duration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
clip.duration = duration;
|
||||
clip.displayName = asset.FrameClip != null &&
|
||||
!string.IsNullOrEmpty(asset.FrameClip.DisplayName)
|
||||
? asset.FrameClip.DisplayName
|
||||
: "Frame Animation Clip";
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9f814325d9449478d6688741c38204f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Timeline
|
||||
{
|
||||
[Serializable]
|
||||
[DisplayName("Frame Animation Clip")]
|
||||
public sealed class FrameAnimationTimelineClip : PlayableAsset, ITimelineClipAsset
|
||||
{
|
||||
private const double FallbackDurationSeconds = 1d;
|
||||
|
||||
[SerializeField, Tooltip("要由 Timeline 播放的独立 FrameClip 资产。")]
|
||||
private FrameClip frameClip;
|
||||
[SerializeField, HideInInspector] private FrameClip synchronizedDurationSource;
|
||||
[SerializeField, HideInInspector] private bool hasSynchronizedDuration;
|
||||
|
||||
public FrameClip FrameClip => frameClip;
|
||||
|
||||
public override double duration => GetEffectiveDuration(frameClip);
|
||||
|
||||
public ClipCaps clipCaps
|
||||
{
|
||||
get
|
||||
{
|
||||
var caps = ClipCaps.ClipIn | ClipCaps.SpeedMultiplier;
|
||||
if (frameClip != null && frameClip.DefaultEndBehavior == FrameClipEndBehavior.Loop)
|
||||
{
|
||||
caps |= ClipCaps.Looping;
|
||||
}
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
|
||||
{
|
||||
var playable = ScriptPlayable<FrameAnimationTimelineBehaviour>.Create(graph);
|
||||
playable.GetBehaviour().FrameClip = frameClip;
|
||||
return playable;
|
||||
}
|
||||
|
||||
internal void SetFrameClipForTests(FrameClip value)
|
||||
{
|
||||
frameClip = value;
|
||||
}
|
||||
|
||||
internal bool TrySynchronizeSourceDuration(out double sourceDuration)
|
||||
{
|
||||
sourceDuration = duration;
|
||||
if (hasSynchronizedDuration && synchronizedDurationSource == frameClip)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronizedDurationSource = frameClip;
|
||||
hasSynchronizedDuration = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static double GetEffectiveDuration(FrameClip value)
|
||||
{
|
||||
if (value == null || value.TotalDurationMs <= 0 || value.Speed <= 0f)
|
||||
{
|
||||
return FallbackDurationSeconds;
|
||||
}
|
||||
return value.TotalDurationMs / 1000d / value.Speed;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationTimelineBehaviour : PlayableBehaviour
|
||||
{
|
||||
internal FrameClip FrameClip { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c908631f9d7446ac86914dce3f1927af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Timeline
|
||||
{
|
||||
[DisplayName("Frame Animation Track")]
|
||||
[TrackColor(0.35f, 0.72f, 0.95f)]
|
||||
[TrackBindingType(typeof(FrameClipPlayer))]
|
||||
[TrackClipType(typeof(FrameAnimationTimelineClip))]
|
||||
public sealed class FrameAnimationTrack : TrackAsset
|
||||
{
|
||||
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
|
||||
{
|
||||
var playable = ScriptPlayable<FrameAnimationTrackMixerBehaviour>.Create(graph, inputCount);
|
||||
playable.GetBehaviour().SetClips(
|
||||
GetClips()
|
||||
.Select((clip, index) => new FrameAnimationTimelineClipInfo(clip, index))
|
||||
.ToArray());
|
||||
return playable;
|
||||
}
|
||||
|
||||
public override void GatherProperties(PlayableDirector director, IPropertyCollector driver)
|
||||
{
|
||||
var player = director != null ? director.GetGenericBinding(this) as FrameClipPlayer : null;
|
||||
if (player != null)
|
||||
{
|
||||
var spriteRenderer = player.GetComponent<SpriteRenderer>();
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
driver.AddFromName<SpriteRenderer>(player.gameObject, "m_Sprite");
|
||||
driver.AddFromName<SpriteRenderer>(player.gameObject, "m_Enabled");
|
||||
}
|
||||
|
||||
var image = player.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
driver.AddFromName<Image>(player.gameObject, "m_Sprite");
|
||||
driver.AddFromName<Image>(player.gameObject, "m_Enabled");
|
||||
}
|
||||
}
|
||||
base.GatherProperties(director, driver);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct FrameAnimationTimelineClipInfo
|
||||
{
|
||||
internal TimelineClip TimelineClip { get; }
|
||||
internal FrameAnimationTimelineClip Asset { get; }
|
||||
internal double Start { get; }
|
||||
internal double End { get; }
|
||||
internal int Order { get; }
|
||||
|
||||
internal FrameAnimationTimelineClipInfo(TimelineClip timelineClip, int order)
|
||||
{
|
||||
TimelineClip = timelineClip;
|
||||
Asset = timelineClip?.asset as FrameAnimationTimelineClip;
|
||||
Start = timelineClip?.start ?? 0d;
|
||||
End = timelineClip?.end ?? 0d;
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationTrackMixerBehaviour : PlayableBehaviour
|
||||
{
|
||||
private const double TimeEpsilon = 0.0000001d;
|
||||
private IReadOnlyList<FrameAnimationTimelineClipInfo> clips =
|
||||
Array.Empty<FrameAnimationTimelineClipInfo>();
|
||||
private FrameClipPlayer currentPlayer;
|
||||
private FrameClip terminalClip;
|
||||
private string lastErrorKey = string.Empty;
|
||||
|
||||
internal void SetClips(IReadOnlyList<FrameAnimationTimelineClipInfo> value)
|
||||
{
|
||||
clips = value ?? Array.Empty<FrameAnimationTimelineClipInfo>();
|
||||
}
|
||||
|
||||
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
|
||||
{
|
||||
var player = playerData as FrameClipPlayer;
|
||||
if (player == null)
|
||||
{
|
||||
ReleaseCurrentPlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPlayer != player)
|
||||
{
|
||||
ReleaseCurrentPlayer();
|
||||
currentPlayer = player;
|
||||
ReportError(currentPlayer.BeginTimelineControl(), currentPlayer);
|
||||
}
|
||||
|
||||
var timelineTime = playable.GetTime();
|
||||
var activeIndex = FindActiveClipIndex(playable, timelineTime);
|
||||
if (activeIndex >= 0)
|
||||
{
|
||||
var clipInfo = clips[activeIndex];
|
||||
terminalClip = clipInfo.Asset?.FrameClip;
|
||||
var localTime = playable.GetInput(activeIndex).GetTime();
|
||||
localTime = NormalizeLoopTime(terminalClip, localTime);
|
||||
ReportError(currentPlayer.SampleTimeline(terminalClip, localTime), currentPlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = FindPreviousClip(timelineTime);
|
||||
if (previous.HasValue)
|
||||
{
|
||||
terminalClip = previous.Value.Asset?.FrameClip;
|
||||
ReportError(currentPlayer.ApplyTimelineEndBehavior(terminalClip), currentPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
terminalClip = null;
|
||||
currentPlayer.RestoreTimelineBaseline();
|
||||
lastErrorKey = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnGraphStop(Playable playable)
|
||||
{
|
||||
ReleaseCurrentPlayer();
|
||||
}
|
||||
|
||||
public override void OnPlayableDestroy(Playable playable)
|
||||
{
|
||||
ReleaseCurrentPlayer();
|
||||
}
|
||||
|
||||
private int FindActiveClipIndex(Playable playable, double timelineTime)
|
||||
{
|
||||
var selected = -1;
|
||||
var selectedStart = double.NegativeInfinity;
|
||||
var count = Math.Min(playable.GetInputCount(), clips.Count);
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var clip = clips[index];
|
||||
var isInside = timelineTime + TimeEpsilon >= clip.Start &&
|
||||
timelineTime < clip.End - TimeEpsilon;
|
||||
if (!isInside || playable.GetInputWeight(index) <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (clip.Start > selectedStart ||
|
||||
(Math.Abs(clip.Start - selectedStart) <= TimeEpsilon && index > selected))
|
||||
{
|
||||
selected = index;
|
||||
selectedStart = clip.Start;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private FrameAnimationTimelineClipInfo? FindPreviousClip(double timelineTime)
|
||||
{
|
||||
FrameAnimationTimelineClipInfo? selected = null;
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
if (clip.End > timelineTime + TimeEpsilon)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!selected.HasValue || clip.End > selected.Value.End ||
|
||||
(Math.Abs(clip.End - selected.Value.End) <= TimeEpsilon &&
|
||||
clip.Order > selected.Value.Order))
|
||||
{
|
||||
selected = clip;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static double NormalizeLoopTime(FrameClip clip, double localTime)
|
||||
{
|
||||
if (clip == null || clip.DefaultEndBehavior != FrameClipEndBehavior.Loop)
|
||||
{
|
||||
return Math.Max(0d, localTime);
|
||||
}
|
||||
|
||||
var duration = FrameAnimationTimelineClip.GetEffectiveDuration(clip);
|
||||
if (duration <= 0d)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
return Math.Max(0d, localTime) % duration;
|
||||
}
|
||||
|
||||
private void ReleaseCurrentPlayer()
|
||||
{
|
||||
if (currentPlayer != null)
|
||||
{
|
||||
currentPlayer.ReleaseTimelineControl(terminalClip);
|
||||
}
|
||||
currentPlayer = null;
|
||||
terminalClip = null;
|
||||
lastErrorKey = string.Empty;
|
||||
}
|
||||
|
||||
private void ReportError(FrameAnimationPlaybackError error, UnityEngine.Object context)
|
||||
{
|
||||
if (error.Code == FrameAnimationPlaybackErrorCode.None)
|
||||
{
|
||||
lastErrorKey = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var key = $"{error.Code}:{error.Message}";
|
||||
if (lastErrorKey == key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lastErrorKey = key;
|
||||
Debug.LogError($"Frame Animation Timeline 播放失败:{error}", context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d1afb0ade1f46879f33b8173a66abbf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user