308 lines
13 KiB
C#
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) { }
|
|
}
|
|
}
|
|
}
|