Files
aibis-dream/Assets/Tests/FrameAnimation/EditMode/FrameAnimationTimelineTests.cs
T

308 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AibisDream.FrameAnimation.Timeline;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.TestTools;
using UnityEngine.Timeline;
using UnityEngine.UI;
namespace AibisDream.FrameAnimation.Tests.EditMode
{
public sealed class FrameAnimationTimelineTests
{
private readonly List<UnityEngine.Object> createdObjects = new List<UnityEngine.Object>();
[TearDown]
public void TearDown()
{
for (var index = createdObjects.Count - 1; index >= 0; index--)
{
if (createdObjects[index] != null)
{
UnityEngine.Object.DestroyImmediate(createdObjects[index]);
}
}
createdObjects.Clear();
}
[Test]
public void TimelineClip_ReportsDurationAndSupportedCapabilities()
{
var holdClip = CreateClip("Hold", FrameClipEndBehavior.HoldLastFrame, 2f, null, null);
var loopClip = CreateClip("Loop", FrameClipEndBehavior.Loop, 1f, null, null);
var asset = Track(ScriptableObject.CreateInstance<FrameAnimationTimelineClip>());
asset.SetFrameClipForTests(holdClip);
Assert.That(asset.duration, Is.EqualTo(0.1d).Within(0.000001d));
Assert.That(asset.TrySynchronizeSourceDuration(out var synchronizedDuration), Is.True);
Assert.That(synchronizedDuration, Is.EqualTo(0.1d).Within(0.000001d));
Assert.That(asset.TrySynchronizeSourceDuration(out _), Is.False);
Assert.That(asset.clipCaps.HasFlag(ClipCaps.ClipIn), Is.True);
Assert.That(asset.clipCaps.HasFlag(ClipCaps.SpeedMultiplier), Is.True);
Assert.That(asset.clipCaps.HasFlag(ClipCaps.Blending), Is.False);
Assert.That(asset.clipCaps.HasFlag(ClipCaps.Looping), Is.False);
asset.SetFrameClipForTests(loopClip);
Assert.That(asset.TrySynchronizeSourceDuration(out _), Is.True);
Assert.That(asset.clipCaps.HasFlag(ClipCaps.Looping), Is.True);
asset.SetFrameClipForTests(null);
Assert.That(asset.duration, Is.EqualTo(1d));
}
[Test]
public void Track_ScrubsBothDirectionsAndHonorsClipInAndSpeedMultiplier()
{
var first = CreateSprite(Color.red);
var second = CreateSprite(Color.blue);
var clip = CreateClip("Scrub", FrameClipEndBehavior.HoldLastFrame, 1f, first, second);
var playerObject = Track(new GameObject("Timeline Sprite Player"));
var renderer = playerObject.AddComponent<SpriteRenderer>();
var player = playerObject.AddComponent<FrameClipPlayer>();
var setup = CreateTimeline(player, clip, 0d, 0.2d);
setup.Clip.clipIn = 0.05d;
setup.Clip.timeScale = 2d;
Evaluate(setup.Director, 0.03d);
Assert.That(renderer.sprite, Is.SameAs(second));
Evaluate(setup.Director, 0d);
Assert.That(renderer.sprite, Is.SameAs(first));
Evaluate(setup.Director, 0.03d);
Assert.That(renderer.sprite, Is.SameAs(second));
}
[Test]
public void Track_GapUsesPreviousTerminalStateAndOverlapPrefersLaterClip()
{
var baseline = CreateSprite(Color.white);
var firstSprite = CreateSprite(Color.red);
var secondSprite = CreateSprite(Color.blue);
var hold = CreateClip("Hold", FrameClipEndBehavior.HoldLastFrame, 1f, firstSprite);
var later = CreateClip("Later", FrameClipEndBehavior.HoldLastFrame, 1f, secondSprite);
var playerObject = Track(new GameObject("Timeline Overlap Player"));
var renderer = playerObject.AddComponent<SpriteRenderer>();
renderer.sprite = baseline;
var player = playerObject.AddComponent<FrameClipPlayer>();
var setup = CreateTimeline(player, hold, 0.1d, 0.3d);
AddClip(setup.Track, later, 0.2d, 0.1d);
Evaluate(setup.Director, 0.25d);
Assert.That(renderer.sprite, Is.SameAs(secondSprite));
Evaluate(setup.Director, 0.35d);
Assert.That(renderer.sprite, Is.SameAs(firstSprite));
Evaluate(setup.Director, 0.45d);
Assert.That(renderer.sprite, Is.SameAs(firstSprite));
Evaluate(setup.Director, -0.01d);
Assert.That(renderer.sprite, Is.SameAs(baseline));
}
[Test]
public void Track_AppliesClearAndHideTerminalStates()
{
var sprite = CreateSprite(Color.green);
var clear = CreateClip("Clear", FrameClipEndBehavior.Clear, 1f, sprite);
var hide = CreateClip("Hide", FrameClipEndBehavior.HideTarget, 1f, sprite);
var playerObject = Track(new GameObject("Timeline End States"));
var renderer = playerObject.AddComponent<SpriteRenderer>();
var player = playerObject.AddComponent<FrameClipPlayer>();
var setup = CreateTimeline(player, clear, 0d, 0.1d);
AddClip(setup.Track, hide, 0.2d, 0.1d);
Evaluate(setup.Director, 0.15d);
Assert.That(renderer.enabled, Is.True);
Assert.That(renderer.sprite, Is.Null);
Evaluate(setup.Director, 0.35d);
Assert.That(renderer.enabled, Is.False);
}
[Test]
public void Track_SamplesImageAndZeroSpeedClipStaysOnFirstFrame()
{
var first = CreateSprite(Color.yellow);
var second = CreateSprite(Color.black);
var clip = CreateClip("Zero", FrameClipEndBehavior.HoldLastFrame, 0f, first, second);
var playerObject = Track(new GameObject(
"Timeline Image Player",
typeof(RectTransform),
typeof(CanvasRenderer)));
var image = playerObject.AddComponent<Image>();
var player = playerObject.AddComponent<FrameClipPlayer>();
var setup = CreateTimeline(player, clip, 0d, 1d);
Evaluate(setup.Director, 0.75d);
Assert.That(image.sprite, Is.SameAs(first));
Assert.That(setup.Asset.duration, Is.EqualTo(1d));
}
[Test]
public void Track_InvalidClipLogsStructuredErrorWithoutThrowing()
{
var invalid = CreateClip("Empty", FrameClipEndBehavior.HoldLastFrame, 1f);
var playerObject = Track(new GameObject("Timeline Invalid Clip"));
playerObject.AddComponent<SpriteRenderer>();
var player = playerObject.AddComponent<FrameClipPlayer>();
var setup = CreateTimeline(player, invalid, 0d, 1d);
LogAssert.Expect(
LogType.Error,
new Regex("Frame Animation Timeline 播放失败.*InvalidPlayableData"));
Assert.DoesNotThrow(() => Evaluate(setup.Director, 0.25d));
}
[Test]
public void Track_GathersSpriteAndEnabledPropertiesForPreview()
{
var clip = CreateClip(
"Preview",
FrameClipEndBehavior.HoldLastFrame,
1f,
CreateSprite(Color.white));
var spriteObject = Track(new GameObject("Timeline Sprite Preview"));
spriteObject.AddComponent<SpriteRenderer>();
var spritePlayer = spriteObject.AddComponent<FrameClipPlayer>();
var spriteSetup = CreateTimeline(spritePlayer, clip, 0d, 0.1d);
var spriteCollector = new RecordingPropertyCollector();
spriteSetup.Track.GatherProperties(spriteSetup.Director, spriteCollector);
Assert.That(spriteCollector.Properties, Does.Contain("SpriteRenderer:m_Sprite"));
Assert.That(spriteCollector.Properties, Does.Contain("SpriteRenderer:m_Enabled"));
var imageObject = Track(new GameObject(
"Timeline Image Preview",
typeof(RectTransform),
typeof(CanvasRenderer)));
imageObject.AddComponent<Image>();
var imagePlayer = imageObject.AddComponent<FrameClipPlayer>();
var imageSetup = CreateTimeline(imagePlayer, clip, 0d, 0.1d);
var imageCollector = new RecordingPropertyCollector();
imageSetup.Track.GatherProperties(imageSetup.Director, imageCollector);
Assert.That(imageCollector.Properties, Does.Contain("Image:m_Sprite"));
Assert.That(imageCollector.Properties, Does.Contain("Image:m_Enabled"));
}
private TimelineSetup CreateTimeline(
FrameClipPlayer player,
FrameClip clip,
double start,
double duration)
{
var timeline = Track(ScriptableObject.CreateInstance<TimelineAsset>());
var track = Track(timeline.CreateTrack<FrameAnimationTrack>());
var timelineClip = AddClip(track, clip, start, duration);
var directorObject = Track(new GameObject("Frame Animation Director"));
var director = directorObject.AddComponent<PlayableDirector>();
director.playableAsset = timeline;
director.SetGenericBinding(track, player);
return new TimelineSetup(
director,
track,
timelineClip,
(FrameAnimationTimelineClip)timelineClip.asset);
}
private TimelineClip AddClip(
FrameAnimationTrack track,
FrameClip clip,
double start,
double duration)
{
var timelineClip = track.CreateClip<FrameAnimationTimelineClip>();
var asset = Track((FrameAnimationTimelineClip)timelineClip.asset);
asset.SetFrameClipForTests(clip);
timelineClip.start = start;
timelineClip.duration = duration;
return timelineClip;
}
private static void Evaluate(PlayableDirector director, double time)
{
director.time = Math.Max(0d, time);
director.Evaluate();
}
private FrameClip CreateClip(
string id,
FrameClipEndBehavior endBehavior,
float speed,
params Sprite[] sprites)
{
var clip = Track(ScriptableObject.CreateInstance<FrameClip>());
var frames = new List<FrameAnimationFrame>();
for (var index = 0; index < sprites.Length; index++)
{
frames.Add(new FrameAnimationFrame(sprites[index], 100, $"frame_{index}", index));
}
clip.Configure(id, id, frames, speed, endBehavior);
return clip;
}
private Sprite CreateSprite(Color color)
{
var texture = Track(new Texture2D(2, 2));
texture.SetPixels(new[] { color, color, color, color });
texture.Apply();
return Track(Sprite.Create(texture, new Rect(0, 0, 2, 2), Vector2.one * 0.5f));
}
private T Track<T>(T value) where T : UnityEngine.Object
{
createdObjects.Add(value);
return value;
}
private readonly struct TimelineSetup
{
internal PlayableDirector Director { get; }
internal FrameAnimationTrack Track { get; }
internal TimelineClip Clip { get; }
internal FrameAnimationTimelineClip Asset { get; }
internal TimelineSetup(
PlayableDirector director,
FrameAnimationTrack track,
TimelineClip clip,
FrameAnimationTimelineClip asset)
{
Director = director;
Track = track;
Clip = clip;
Asset = asset;
}
}
private sealed class RecordingPropertyCollector : IPropertyCollector
{
internal List<string> Properties { get; } = new List<string>();
public void PushActiveGameObject(GameObject gameObject) { }
public void PopActiveGameObject() { }
public void AddFromClip(AnimationClip clip) { }
public void AddFromClips(IEnumerable<AnimationClip> clips) { }
public void AddFromName<T>(string name) where T : Component =>
Properties.Add($"{typeof(T).Name}:{name}");
public void AddFromName(string name) { }
public void AddFromClip(GameObject obj, AnimationClip clip) { }
public void AddFromClips(GameObject obj, IEnumerable<AnimationClip> clips) { }
public void AddFromName<T>(GameObject obj, string name) where T : Component =>
Properties.Add($"{typeof(T).Name}:{name}");
public void AddFromName(GameObject obj, string name) { }
public void AddFromName(Component component, string name) { }
public void AddFromComponent(GameObject obj, Component component) { }
public void AddObjectProperties(UnityEngine.Object obj, AnimationClip clip) { }
}
}
}