A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using NUnit.Framework;
6using osu.Framework.Allocation;
7using osu.Framework.Graphics;
8using osu.Framework.Graphics.Animations;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Sprites;
11using osu.Framework.Graphics.Textures;
12using osu.Framework.IO.Stores;
13using osu.Framework.Testing;
14using osu.Framework.Timing;
15
16namespace osu.Framework.Tests.Visual.Sprites
17{
18 public class TestSceneAnimation : FrameworkTestScene
19 {
20 private SpriteText timeText;
21
22 private ManualClock clock;
23
24 private TestAnimation animation;
25 private Container animationContainer;
26
27 [Resolved]
28 private FontStore fontStore { get; set; }
29
30 [SetUpSteps]
31 public void SetUpSteps()
32 {
33 AddStep("load container", () =>
34 {
35 Children = new Drawable[]
36 {
37 animationContainer = new Container
38 {
39 RelativeSizeAxes = Axes.Both,
40 Clock = new FramedClock(clock = new ManualClock()),
41 },
42 timeText = new SpriteText { Text = "Animation is loading..." }
43 };
44 });
45
46 loadNewAnimation();
47
48 AddStep("Reset clock", () => clock.CurrentTime = 0);
49 }
50
51 [Test]
52 public void TestFrameSeeking()
53 {
54 AddAssert("frame count is correct", () => animation.FrameCount == TestAnimation.LOADABLE_FRAMES);
55 AddUntilStep("wait for frames to pass", () => animation.CurrentFrameIndex > 10);
56 AddStep("stop animation", () => animation.Stop());
57 AddAssert("is stopped", () => !animation.IsPlaying);
58
59 AddStep("goto frame 60", () => animation.GotoFrame(60));
60 AddAssert("is at frame 60", () => animation.CurrentFrameIndex == 60);
61
62 AddStep("goto frame 30", () => animation.GotoFrame(30));
63 AddAssert("is at frame 30", () => animation.CurrentFrameIndex == 30);
64
65 AddStep("goto frame 60", () => animation.GotoFrame(60));
66 AddAssert("is at frame 60", () => animation.CurrentFrameIndex == 60);
67
68 AddStep("start animation", () => animation.Play());
69 AddUntilStep("continues to frame 70", () => animation.CurrentFrameIndex == 70);
70 }
71
72 [Test]
73 public void TestStartFromCurrentTime()
74 {
75 AddAssert("Animation is near start", () => animation.PlaybackPosition < 1000);
76
77 AddWaitStep("Wait some", 20);
78
79 loadNewAnimation();
80
81 AddAssert("Animation is near start", () => animation.PlaybackPosition < 1000);
82 }
83
84 [Test]
85 public void TestStoppedAnimationIsAtZero()
86 {
87 loadNewAnimation(postLoadAction: a => a.Stop());
88 AddAssert("Animation is at start", () => animation.PlaybackPosition == 0);
89 }
90
91 [Test]
92 public void TestStoppedAnimationIsAtSpecifiedFrame()
93 {
94 loadNewAnimation(postLoadAction: a => a.GotoAndStop(2));
95 AddAssert("Animation is at specific frame", () => animation.PlaybackPosition == 500);
96 }
97
98 [Test]
99 public void TestPauseThenResume()
100 {
101 loadNewAnimation(false, postLoadAction: a => a.Stop());
102
103 AddWaitStep("wait some", 10);
104
105 AddStep("play", () => animation.Play());
106
107 AddAssert("time is near start", () => animation.CurrentFrameIndex < 2);
108 }
109
110 [Test]
111 public void TestStartFromOngoingTime()
112 {
113 AddWaitStep("Wait some", 20);
114
115 loadNewAnimation(false);
116
117 AddAssert("Animation is not near start", () => animation.PlaybackPosition > 1000);
118 }
119
120 [Test]
121 public void TestSetCustomClockWithCurrentTime()
122 {
123 AddAssert("Animation is near start", () => animation.PlaybackPosition < 1000);
124
125 AddUntilStep("Animation is not near start", () => animation.PlaybackPosition > 1000);
126
127 double posBefore = 0;
128
129 AddStep("store position", () => posBefore = animation.PlaybackPosition);
130
131 AddStep("Set custom clock", () => animation.Clock = new FramedOffsetClock(null) { Offset = 10000 });
132
133 AddAssert("Animation continued playing at current position", () => animation.PlaybackPosition - posBefore < 1000);
134 }
135
136 [Test]
137 public void TestSetCustomClockWithOngoingTime()
138 {
139 loadNewAnimation(false);
140
141 AddAssert("Animation is near start", () => animation.PlaybackPosition < 1000);
142
143 AddUntilStep("Animation is not near start", () => animation.PlaybackPosition > 1000);
144
145 AddStep("Set custom clock", () => animation.Clock = new FramedOffsetClock(null) { Offset = 10000 });
146
147 AddAssert("Animation is not near start", () => animation.PlaybackPosition > 1000);
148 }
149
150 [Test]
151 public void TestJumpForward()
152 {
153 AddStep("Jump ahead by 10 seconds", () => clock.CurrentTime += 10000);
154 AddUntilStep("Animation seeked", () => animation.PlaybackPosition >= 10000);
155 }
156
157 [Test]
158 public void TestJumpBack()
159 {
160 AddStep("Jump ahead by 10 seconds", () => clock.CurrentTime += 10000);
161 AddUntilStep("Animation seeked", () => animation.PlaybackPosition >= 10000);
162
163 AddStep("Jump back by 10 seconds", () => clock.CurrentTime -= 10000);
164 AddUntilStep("Animation seeked", () => animation.PlaybackPosition < 10000);
165 }
166
167 [Test]
168 public void TestAnimationDoesNotLoopIfDisabled()
169 {
170 AddStep("Seek to end", () => clock.CurrentTime = animation.Duration);
171 AddUntilStep("Animation seeked", () => animation.PlaybackPosition >= animation.Duration - 1000);
172
173 AddWaitStep("Wait for playback", 10);
174 AddAssert("Not looped", () => animation.PlaybackPosition >= animation.Duration - 1000);
175 }
176
177 [Test]
178 public void TestAnimationLoopsIfEnabled()
179 {
180 AddStep("Set looping", () => animation.Loop = true);
181 AddStep("Seek to end", () => clock.CurrentTime = animation.Duration - 2000);
182 AddUntilStep("Animation seeked", () => animation.PlaybackPosition >= animation.Duration - 1000);
183
184 AddWaitStep("Wait for playback", 10);
185 AddUntilStep("Looped", () => animation.PlaybackPosition < animation.Duration - 1000);
186 }
187
188 [Test]
189 public void TestTransformBeforeLoaded()
190 {
191 AddStep("set time to future", () => clock.CurrentTime = 10000);
192
193 loadNewAnimation(postLoadAction: a =>
194 {
195 a.Alpha = 0;
196 a.FadeInFromZero(10).Then().FadeOutFromOne(1000);
197 });
198
199 AddAssert("Is visible", () => animation.Alpha > 0);
200 }
201
202 [Test]
203 public void TestStartFromFutureTimeWithInitialSeek()
204 {
205 AddStep("set time to future", () => clock.CurrentTime = 10000);
206
207 loadNewAnimation(false, a =>
208 {
209 a.PlaybackPosition = -10000;
210 });
211
212 AddAssert("Animation is at beginning", () => animation.PlaybackPosition < 1000);
213 }
214
215 [Test]
216 public void TestGotoZeroOnFirstFrameVisible()
217 {
218 loadNewAnimation();
219
220 AddStep("set time to 1000", () => clock.CurrentTime = 1000);
221 AddStep("hide animation", () => animation.Hide());
222
223 AddStep("set time = 2000", () => clock.CurrentTime = 2000);
224 AddStep("goto(0) and show", () =>
225 {
226 animation.GotoFrame(0);
227 animation.Show();
228 });
229
230 // Note: We won't get PlaybackPosition=0 here because the test runner increments the clock by at least 200ms per step, so 1000 is a safe value.
231 AddAssert("animation restarted from 0", () => animation.PlaybackPosition < 1000);
232 }
233
234 [TestCase(0)]
235 [TestCase(48)]
236 public void TestGotoFrameBeforeLoaded(int frame)
237 {
238 AddStep("create new animation", () => animation = new TestAnimation(true, fontStore)
239 {
240 Loop = false
241 });
242 AddStep($"go to frame {frame}", () => animation.GotoFrame(frame));
243
244 AddStep("load animation", () => animationContainer.Child = animation);
245
246 AddAssert($"animation is at frame {frame}", () => animation.CurrentFrameIndex == frame);
247 }
248
249 [Test]
250 public void TestClearFrames()
251 {
252 loadNewAnimation();
253
254 AddUntilStep("animation is playing", () => animation.CurrentFrameIndex > 0);
255
256 AddStep("clear frames", () => animation.ClearFrames());
257 AddAssert("animation duration is 0", () => animation.Duration == 0);
258 AddAssert("animation is at start", () => animation.CurrentFrameIndex == 0);
259 }
260
261 private void loadNewAnimation(bool startFromCurrent = true, Action<TestAnimation> postLoadAction = null)
262 {
263 AddStep("load animation", () =>
264 {
265 animationContainer.Child = animation = new TestAnimation(startFromCurrent, fontStore)
266 {
267 Loop = false,
268 };
269
270 postLoadAction?.Invoke(animation);
271 });
272
273 AddUntilStep("Wait for animation to load", () => animation.IsLoaded);
274 }
275
276 protected override void Update()
277 {
278 base.Update();
279
280 if (clock != null)
281 clock.CurrentTime += Clock.ElapsedFrameTime;
282
283 if (animation != null)
284 {
285 timeText.Text = $"playback: {animation.PlaybackPosition:N0} current frame: {animation.CurrentFrameIndex} total frames: {animation.FramesProcessed}";
286 }
287 }
288
289 private class TestAnimation : TextureAnimation
290 {
291 public const int LOADABLE_FRAMES = 72;
292
293 public int FramesProcessed;
294
295 // fontStore passed in via ctor to be able to test scenarios where an animation
296 // already has frames before load
297 public TestAnimation(bool startFromCurrent, FontStore fontStore)
298 : base(startFromCurrent)
299 {
300 Anchor = Anchor.Centre;
301 Origin = Anchor.Centre;
302
303 for (int i = 0; i < LOADABLE_FRAMES; i++)
304 {
305 AddFrame(new Texture(fontStore.Get(null, (char)('0' + i))?.Texture.TextureGL)
306 {
307 ScaleAdjust = 1 + i / 40f,
308 }, 250);
309 }
310 }
311
312 protected override void DisplayFrame(Texture content)
313 {
314 FramesProcessed++;
315 base.DisplayFrame(content);
316 }
317 }
318 }
319}