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 System.Collections.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Performance;
11using osu.Framework.Graphics.Sprites;
12using osu.Framework.Timing;
13
14namespace osu.Framework.Tests.Visual.Containers
15{
16 public class TestSceneLifetimeManagementContainer : FrameworkTestScene
17 {
18 private ManualClock manualClock;
19 private TestContainer container;
20
21 [SetUp]
22 public void SetUp() => Schedule(() =>
23 {
24 manualClock = new ManualClock();
25
26 Children = new Drawable[]
27 {
28 container = new TestContainer
29 {
30 Clock = new FramedClock(manualClock),
31 },
32 };
33 });
34
35 private void skipTo(double time)
36 {
37 AddStep($"Set time to {time}", () => manualClock.CurrentTime = time);
38 }
39
40 private void validate(int numAlive)
41 {
42 AddAssert($"{numAlive} alive children", () =>
43 {
44 int num = 0;
45
46 foreach (var child in container.InternalChildren)
47 {
48 num += child.IsAlive ? 1 : 0;
49 Assert.AreEqual(child.ShouldBeAlive, child.IsAlive, $"Aliveness is invalid for {child}");
50 }
51
52 return num == numAlive;
53 });
54 }
55
56 [Test]
57 public void TestBasic()
58 {
59 AddStep("Add children", () =>
60 {
61 container.AddInternal(new TestChild(-1, 1));
62 container.AddInternal(new TestChild(0, 1));
63 container.AddInternal(new TestChild(0, 2));
64 container.AddInternal(new TestChild(1, 2));
65 container.AddInternal(new TestChild(2, 2));
66 container.AddInternal(new TestChild(2, 3));
67 });
68 validate(3);
69 skipTo(1);
70 validate(2);
71 skipTo(2);
72 validate(1);
73 skipTo(0);
74 validate(3);
75 skipTo(3);
76 validate(0);
77 }
78
79 [Test]
80 public void TestAddLoadedDrawable()
81 {
82 TestChild child = null;
83
84 AddStep("add child", () => container.AddInternal(child = new TestChild(0, 2)));
85 skipTo(1);
86 AddStep("remove child", () => container.RemoveInternal(child));
87 AddStep("add same child", () => container.AddInternal(child));
88 validate(1);
89 }
90
91 [Test]
92 public void TestDynamicChange()
93 {
94 TestChild a = null, b = null, c = null, d = null;
95 AddStep("Add children", () =>
96 {
97 container.AddInternal(a = new TestChild(-1, 0));
98 container.AddInternal(b = new TestChild(0, 1));
99 container.AddInternal(c = new TestChild(0, 1));
100 container.AddInternal(d = new TestChild(1, 2));
101 });
102 validate(2);
103 AddStep("Change lifetime", () =>
104 {
105 a.LifetimeEnd = 1;
106 b.LifetimeStart = 1;
107 c.LifetimeEnd = 0;
108 d.LifetimeStart = 0;
109 });
110 validate(2);
111 AddStep("Change lifetime", () =>
112 {
113 foreach (var x in new[] { a, b, c, d })
114 {
115 x.LifetimeStart += 1;
116 x.LifetimeEnd += 1;
117 }
118 });
119 validate(1);
120 AddStep("Change lifetime", () =>
121 {
122 foreach (var x in new[] { a, b, c, d })
123 {
124 x.LifetimeStart -= 1;
125 x.LifetimeEnd -= 1;
126 }
127 });
128 validate(2);
129 }
130
131 [Test]
132 public void TestBoundaryCrossing()
133 {
134 TestChild a = null, b = null, c = null;
135 AddStep("Add children", () =>
136 {
137 container.AddInternal(a = new TestChild(-1, 0));
138 container.AddInternal(b = new TestChild(0, 1));
139 container.AddInternal(c = new TestChild(1, 2));
140 });
141 skipTo(2);
142 AddStep("Check crossings", () =>
143 {
144 a.CheckCrossings();
145 b.CheckCrossings(new LifetimeBoundaryCrossedEvent(b, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward));
146 c.CheckCrossings(
147 new LifetimeBoundaryCrossedEvent(c, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Forward),
148 new LifetimeBoundaryCrossedEvent(c, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward));
149 });
150 skipTo(1);
151 AddStep("Check crossings", () =>
152 {
153 a.CheckCrossings();
154 b.CheckCrossings();
155 c.CheckCrossings(new LifetimeBoundaryCrossedEvent(c, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward));
156 });
157 skipTo(-1);
158 AddStep("Check crossings", () =>
159 {
160 a.CheckCrossings(
161 new LifetimeBoundaryCrossedEvent(a, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward));
162 b.CheckCrossings(new LifetimeBoundaryCrossedEvent(b, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward),
163 new LifetimeBoundaryCrossedEvent(b, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward));
164 c.CheckCrossings(new LifetimeBoundaryCrossedEvent(c, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward));
165 });
166 }
167
168 [Test]
169 public void TestLifetimeChangeOnCallback()
170 {
171 AddStep("Add children", () =>
172 {
173 TestChild a;
174 container.AddInternal(a = new TestChild(0, 1));
175 container.OnCrossing += e =>
176 {
177 var kind = e.Kind;
178 var direction = e.Direction;
179 if (kind == LifetimeBoundaryKind.End && direction == LifetimeBoundaryCrossingDirection.Forward)
180 a.LifetimeEnd = 2;
181 else if (kind == LifetimeBoundaryKind.Start && direction == LifetimeBoundaryCrossingDirection.Backward)
182 a.LifetimeEnd = 1;
183 else if (kind == LifetimeBoundaryKind.Start && direction == LifetimeBoundaryCrossingDirection.Forward)
184 a.LifetimeStart = a.LifetimeStart == 0 ? 1 : 0;
185 };
186 });
187 skipTo(1);
188 validate(1);
189 skipTo(-1);
190 validate(0);
191 skipTo(0);
192 validate(0);
193 skipTo(1);
194 validate(1);
195 }
196
197 [Test]
198 public void TestLifetimeMutatingChildren()
199 {
200 AddStep("detach container", () => Remove(container));
201
202 TestLifetimeMutatingChild first = null, second = null;
203 AddStep("add children", () =>
204 {
205 container.AddInternal(first = new TestLifetimeMutatingChild(3, 5));
206 container.AddInternal(second = new TestLifetimeMutatingChild(3, 5));
207 });
208
209 AddStep("process single frame when children alive", () =>
210 {
211 manualClock.CurrentTime = 4;
212 container.UpdateSubTree();
213 });
214 AddAssert("both children processed", () => first.Processed && second.Processed);
215 }
216
217 [Test, Ignore("Takes too long. Unignore when you changed relevant code.")]
218 public void TestFuzz()
219 {
220 var rng = new Random(2222);
221
222 void randomLifetime(out double l, out double r)
223 {
224 l = rng.Next(5);
225 r = rng.Next(5);
226
227 if (l > r)
228 (l, r) = (r, l);
229
230 ++r;
231 }
232
233 void checkAll()
234 {
235 Schedule(() =>
236 {
237 foreach (var child in container.InternalChildren)
238 Assert.AreEqual(child.ShouldBeAlive, child.IsAlive, $"Aliveness is invalid for {child}");
239 });
240 }
241
242 void addChild()
243 {
244 randomLifetime(out var l, out var r);
245 container.AddInternal(new TestChild(l, r));
246 checkAll();
247 }
248
249 void removeChild()
250 {
251 var child = container.InternalChildren[rng.Next(container.InternalChildren.Count)];
252 Console.WriteLine($"removeChild: {child.ChildID}");
253 container.RemoveInternal(child);
254 }
255
256 void changeLifetime()
257 {
258 var child = container.InternalChildren[rng.Next(container.InternalChildren.Count)];
259 randomLifetime(out var l, out var r);
260 Console.WriteLine($"changeLifetime: {child.ChildID}, {l}, {r}");
261 child.LifetimeStart = l;
262 child.LifetimeEnd = r;
263
264 // This is called from boundary crossing events and results in timing issues if the LTMC is not updated in time. Force an update here to prevent such issues.
265 container.UpdateSubTree();
266 checkAll();
267 }
268
269 void changeTime()
270 {
271 int time = rng.Next(6);
272 Console.WriteLine($"changeTime: {time}");
273 manualClock.CurrentTime = time;
274 checkAll();
275 }
276
277 AddStep("init", () =>
278 {
279 addChild();
280 container.OnCrossing += e =>
281 {
282 Console.WriteLine($"OnCrossing({e})");
283 changeLifetime();
284 };
285 });
286
287 int count = 1;
288
289 for (int i = 0; i < 1000; i++)
290 {
291 switch (rng.Next(3))
292 {
293 case 0:
294 if (count < 20)
295 {
296 AddStep("Add child", addChild);
297 count += 1;
298 }
299 else
300 {
301 AddStep("Remove child", removeChild);
302 count -= 1;
303 }
304
305 break;
306
307 case 1:
308 AddStep("Change lifetime", changeLifetime);
309 break;
310
311 case 2:
312 AddStep("Change time", changeTime);
313 break;
314 }
315 }
316 }
317
318 public class TestChild : SpriteText
319 {
320 public override bool RemoveWhenNotAlive => false;
321 public List<LifetimeBoundaryCrossedEvent> Crossings = new List<LifetimeBoundaryCrossedEvent>();
322 public int StartDelta, EndDelta;
323
324 public TestChild(double lifetimeStart, double lifetimeEnd)
325 {
326 LifetimeStart = lifetimeStart;
327 LifetimeEnd = lifetimeEnd;
328 Text = ".";
329 }
330
331 protected override void Update()
332 {
333 Y = ChildID * Font.Size;
334 Text = $"{ChildID}: {LifetimeStart}..{LifetimeEnd} [{string.Join(", ", Crossings.Select(x => x.ToString()))}]";
335 }
336
337 public void CheckCrossings(params LifetimeBoundaryCrossedEvent[] expected)
338 {
339 Assert.AreEqual(expected, Crossings, $"{nameof(CheckCrossings)} for child {ChildID}");
340 Crossings.Clear();
341 }
342 }
343
344 public class TestLifetimeMutatingChild : TestChild
345 {
346 public bool Processed { get; private set; }
347
348 public TestLifetimeMutatingChild(double lifetimeStart, double lifetimeEnd)
349 : base(lifetimeStart, lifetimeEnd)
350 {
351 }
352
353 protected override void Update()
354 {
355 base.Update();
356
357 LifetimeEnd = LifetimeStart;
358 Processed = true;
359 }
360 }
361
362 public class TestContainer : LifetimeManagementContainer
363 {
364 public event Action<LifetimeBoundaryCrossedEvent> OnCrossing;
365
366 protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
367 {
368 if (e.Child is TestChild c)
369 {
370 c.Crossings.Add(e);
371 int d = e.Direction == LifetimeBoundaryCrossingDirection.Forward ? 1 : -1;
372 if (e.Kind == LifetimeBoundaryKind.Start)
373 c.StartDelta += d;
374 else
375 c.EndDelta += d;
376 Assert.IsTrue(Math.Abs(c.StartDelta) <= 1 && Math.Abs(c.EndDelta) <= 1);
377 }
378
379 OnCrossing?.Invoke(e);
380 }
381
382 public new void UpdateSubTree() => base.UpdateSubTree();
383 }
384 }
385}