A game framework written with osu! in mind.
at master 385 lines 13 kB view raw
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}