A game framework written with osu! in mind.
at master 462 lines 22 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; 6 7namespace osu.Framework.Graphics.Transforms 8{ 9 /// <summary> 10 /// A sequence of <see cref="Transform"/>s all operating upon the same <see cref="ITransformable"/> 11 /// of type <typeparamref name="T"/>. 12 /// Exposes various operations to extend the sequence by additional <see cref="Transforms"/> such as 13 /// delays, loops, continuations, and events. 14 /// </summary> 15 /// <typeparam name="T"> 16 /// The type of the <see cref="ITransformable"/> the <see cref="Transform"/>s in this sequence operate upon. 17 /// </typeparam> 18 public class TransformSequence<T> : ITransformSequence where T : class, ITransformable 19 { 20 /// <summary> 21 /// A delegate that generates a new <see cref="TransformSequence{T}"/> on a given <paramref name="origin"/>. 22 /// </summary> 23 /// <param name="origin">The origin to generate a <see cref="TransformSequence{T}"/> for.</param> 24 /// <returns>The generated <see cref="TransformSequence{T}"/>.</returns> 25 public delegate TransformSequence<T> Generator(T origin); 26 27 private readonly T origin; 28 29 private readonly List<Transform> transforms = new List<Transform>(1); // the most common usage of transforms sees one transform being added. 30 31 private bool hasCompleted = true; 32 33 private readonly double startTime; 34 private double currentTime; 35 private double endTime => Math.Max(currentTime, lastEndTime); 36 37 private Transform last; 38 private double lastEndTime; 39 40 private bool hasEnd => lastEndTime != double.PositiveInfinity; 41 42 /// <summary> 43 /// Creates a new empty <see cref="TransformSequence{T}"/> attached to a given <paramref name="origin"/>. 44 /// </summary> 45 /// <param name="origin">The <typeparamref name="T"/> to attach the new <see cref="TransformSequence{T}"/> to.</param> 46 public TransformSequence(T origin) 47 { 48 if (origin == null) 49 throw new ArgumentNullException(nameof(origin), $"May not create a {nameof(TransformSequence<T>)} with a null {nameof(origin)}."); 50 51 this.origin = origin; 52 startTime = currentTime = lastEndTime = origin.TransformStartTime; 53 } 54 55 private void onLoopingTransform() 56 { 57 // As soon as we have an infinitely looping transform, 58 // completion no longer makes sense. 59 if (last != null) 60 last.CompletionTargetSequence = null; 61 62 last = null; 63 lastEndTime = double.PositiveInfinity; 64 hasCompleted = false; 65 } 66 67 public TransformSequence<T> TransformTo<TValue>(string propertyOrFieldName, TValue newValue, double duration = 0, Easing easing = Easing.None) => 68 Append(o => o.TransformTo(propertyOrFieldName, newValue, duration, easing)); 69 70 /// <summary> 71 /// Adds an existing <see cref="Transform"/> operating on <see cref="origin"/> to this <see cref="TransformSequence{T}"/>. 72 /// </summary> 73 /// <param name="transform">The <see cref="Transform"/> to add.</param> 74 internal void Add(Transform transform) 75 { 76 if (!ReferenceEquals(transform.TargetTransformable, origin)) 77 { 78 throw new InvalidOperationException( 79 $"{nameof(transform)} must operate upon {nameof(origin)}={origin}, but operates upon {transform.TargetTransformable}."); 80 } 81 82 transforms.Add(transform); 83 84 transform.CompletionTargetSequence = null; 85 transform.AbortTargetSequence = this; 86 87 if (transform.IsLooping) 88 onLoopingTransform(); 89 90 // Update last transform for completion callback 91 if (last == null || transform.EndTime > lastEndTime) 92 { 93 if (last != null) 94 last.CompletionTargetSequence = null; 95 96 last = transform; 97 last.CompletionTargetSequence = this; 98 lastEndTime = last.EndTime; 99 hasCompleted = false; 100 } 101 } 102 103 /// <summary> 104 /// Appends multiple <see cref="Generator"/>s to this <see cref="TransformSequence{T}"/>. 105 /// </summary> 106 /// <param name="childGenerators">The <see cref="Generator"/>s to be appended.</param> 107 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 108 public TransformSequence<T> Append(IEnumerable<Generator> childGenerators) 109 { 110 foreach (var p in childGenerators) 111 Append(p); 112 113 return this; 114 } 115 116 /// <summary> 117 /// Appends a <see cref="Generator"/>s to this <see cref="TransformSequence{T}"/>. 118 /// The <see cref="Generator"/> is invoked within a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/> 119 /// such that the generated <see cref="TransformSequence{T}"/> starts at the correct point in time. 120 /// Its <see cref="Transform"/>s are then merged into this <see cref="TransformSequence{T}"/>. 121 /// </summary> 122 /// <param name="childGenerator">The <see cref="Generator"/> to be appended.</param> 123 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 124 public TransformSequence<T> Append(Generator childGenerator) 125 { 126 TransformSequence<T> child; 127 using (origin.BeginAbsoluteSequence(currentTime, false)) 128 child = childGenerator(origin); 129 130 if (!ReferenceEquals(child.origin, origin)) 131 throw new InvalidOperationException($"May not append {nameof(TransformSequence<T>)} with different origin."); 132 133 var oldLast = last; 134 foreach (var t in child.transforms) 135 Add(t); 136 137 // If we flatten a child into ourselves that already completed, then 138 // we need to make sure to update the hasCompleted value, too, since 139 // the already completed final transform will no longer fire any events. 140 if (oldLast != last) 141 hasCompleted = child.hasCompleted; 142 143 return this; 144 } 145 146 /// <summary> 147 /// Invokes <paramref name="originFunc"/> inside a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/> 148 /// such that <see cref="ITransformable.TransformStartTime"/> is the current time of this <see cref="TransformSequence{T}"/>. 149 /// It is the responsibility of <paramref name="originFunc"/> to make appropriate use of <see cref="ITransformable.TransformStartTime"/>. 150 /// </summary> 151 /// <typeparam name="TResult">The return type of <paramref name="originFunc"/>.</typeparam> 152 /// <param name="originFunc">The function to be invoked.</param> 153 /// <param name="result">The resulting value of the invocation of <paramref name="originFunc"/>.</param> 154 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 155 public TransformSequence<T> Append<TResult>(Func<T, TResult> originFunc, out TResult result) 156 { 157 using (origin.BeginAbsoluteSequence(currentTime, false)) 158 result = originFunc(origin); 159 160 return this; 161 } 162 163 /// <summary> 164 /// Invokes <paramref name="originAction"/> inside a <see cref="Transformable.BeginAbsoluteSequence(double, bool)"/> 165 /// such that <see cref="ITransformable.TransformStartTime"/> is the current time of this <see cref="TransformSequence{T}"/>. 166 /// It is the responsibility of <paramref name="originAction"/> to make appropriate use of <see cref="ITransformable.TransformStartTime"/>. 167 /// </summary> 168 /// <param name="originAction">The function to be invoked.</param> 169 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 170 public TransformSequence<T> Append(Action<T> originAction) 171 { 172 using (origin.BeginAbsoluteSequence(currentTime, false)) 173 originAction(origin); 174 175 return this; 176 } 177 178 private void subscribeComplete(Action func) 179 { 180 if (onComplete != null) 181 { 182 throw new InvalidOperationException( 183 "May not subscribe completion multiple times." + 184 $"This exception is also caused by calling {nameof(Then)} or {nameof(Finally)} on an infinitely looping {nameof(TransformSequence<T>)}."); 185 } 186 187 onComplete = func; 188 189 // Completion can be immediately triggered by instant transforms, 190 // and therefore when subscribing we need to take into account 191 // potential previous completions. 192 if (hasCompleted) 193 func(); 194 } 195 196 private void subscribeAbort(Action func) 197 { 198 if (onAbort != null) 199 throw new InvalidOperationException("May not subscribe abort multiple times."); 200 201 // No need to worry about new transforms immediately aborting, so 202 // we can just subscribe here and be sure abort couldn't have been 203 // triggered already. 204 onAbort = func; 205 } 206 207 private Action onComplete; 208 private Action onAbort; 209 210 /// <summary> 211 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>. 212 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to 213 /// this <see cref="TransformSequence{T}"/> and then repeated <paramref name="numIters"/> times 214 /// with <paramref name="pause"/> milliseconds between iterations. 215 /// </summary> 216 /// <param name="pause">The pause between iterations in milliseconds.</param> 217 /// <param name="numIters">The number of iterations.</param> 218 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param> 219 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 220 public TransformSequence<T> Loop(double pause, int numIters, params Generator[] childGenerators) 221 { 222 Append(o => 223 { 224 var childSequence = new TransformSequence<T>(o); 225 childSequence.Append(childGenerators); 226 childSequence.Loop(pause, numIters); 227 return childSequence; 228 }); 229 230 return this; 231 } 232 233 /// <summary> 234 /// Repeats all <see cref="Transform"/>s within this <see cref="TransformSequence{T}"/> 235 /// <paramref name="numIters"/> times with <paramref name="pause"/> milliseconds between iterations. 236 /// </summary> 237 /// <param name="pause">The pause between iterations in milliseconds.</param> 238 /// <param name="numIters">The number of iterations.</param> 239 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 240 public TransformSequence<T> Loop(double pause, int numIters) 241 { 242 if (numIters < 1) 243 throw new InvalidOperationException($"May not {nameof(Loop)} for fewer than 1 iteration ({numIters} attempted)."); 244 245 if (!hasEnd) 246 throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence<T>)}."); 247 248 var iterDuration = endTime - startTime + pause; 249 var toLoop = transforms.ToArray(); 250 251 // Duplicate existing transforms numIters times 252 for (int i = 1; i < numIters; ++i) 253 { 254 foreach (var t in toLoop) 255 { 256 var clone = t.Clone(); 257 258 clone.StartTime += i * iterDuration; 259 clone.EndTime += i * iterDuration; 260 261 clone.AppliedToEnd = false; 262 clone.Applied = false; 263 264 Add(clone); 265 t.TargetTransformable.AddTransform(clone); 266 } 267 } 268 269 return this; 270 } 271 272 /// <summary> 273 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>. 274 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to 275 /// this <see cref="TransformSequence{T}"/> and then repeated indefinitely. 276 /// </summary> 277 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param> 278 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 279 public TransformSequence<T> Loop(params Generator[] childGenerators) => Loop(0, childGenerators); 280 281 /// <summary> 282 /// Append a looping <see cref="TransformSequence{T}"/> to this <see cref="TransformSequence{T}"/>. 283 /// All <see cref="Transform"/>s generated by <paramref name="childGenerators"/> are appended to 284 /// this <see cref="TransformSequence{T}"/> and then repeated indefinitely with <paramref name="pause"/> 285 /// milliseconds between iterations. 286 /// </summary> 287 /// <param name="pause">The pause between iterations in milliseconds.</param> 288 /// <param name="childGenerators">The functions to generate the <see cref="TransformSequence{T}"/>s to be looped.</param> 289 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 290 public TransformSequence<T> Loop(double pause, params Generator[] childGenerators) 291 { 292 Append(o => 293 { 294 var childSequence = new TransformSequence<T>(o); 295 childSequence.Append(childGenerators); 296 childSequence.Loop(pause); 297 return childSequence; 298 }); 299 300 return this; 301 } 302 303 /// <summary> 304 /// Repeats all <see cref="Transform"/>s within this <see cref="TransformSequence{T}"/> indefinitely. 305 /// </summary> 306 /// <param name="pause">The pause between iterations in milliseconds.</param> 307 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 308 public TransformSequence<T> Loop(double pause = 0) 309 { 310 if (!hasEnd) 311 throw new InvalidOperationException($"Can not perform {nameof(Loop)} on an endless {nameof(TransformSequence<T>)}."); 312 313 var iterDuration = endTime - startTime + pause; 314 315 foreach (var t in transforms) 316 { 317 var tmpOnAbort = t.AbortTargetSequence; 318 t.AbortTargetSequence = null; 319 t.TargetTransformable.RemoveTransform(t); 320 t.AbortTargetSequence = tmpOnAbort; 321 322 // Update start and end times such that no transformations need to be instantly 323 // looped right after they're added. This is required so that transforms can be 324 // inserted in the correct order such that none of them trigger abortions on 325 // each other due to instant re-sorting upon adding. 326 double currentTransformTime = t.TargetTransformable.Time.Current; 327 328 while (t.EndTime <= currentTransformTime) 329 { 330 t.StartTime += iterDuration; 331 t.EndTime += iterDuration; 332 } 333 } 334 335 // This sort is required such that no abortions happen. 336 var sortedTransforms = new List<Transform>(transforms); 337 sortedTransforms.Sort(Transform.COMPARER); 338 339 foreach (var t in sortedTransforms) 340 { 341 t.IsLooping = true; 342 t.LoopDelay = iterDuration; 343 344 t.Applied = false; 345 t.AppliedToEnd = false; // we want to force a reprocess of this transform. it may have been applied-to-end in the Add, but not correctly looped as a result. 346 347 t.TargetTransformable.AddTransform(t, t.TransformID); 348 } 349 350 onLoopingTransform(); 351 return this; 352 } 353 354 /// <summary> 355 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s to the latest end time of all 356 /// <see cref="Transform"/>s in this <see cref="TransformSequence{T}"/>. 357 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>. 358 /// </summary> 359 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param> 360 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 361 public TransformSequence<T> Then(params Generator[] childGenerators) => Then(0, childGenerators); 362 363 /// <summary> 364 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s to the latest end time of all 365 /// <see cref="Transform"/>s in this <see cref="TransformSequence{T}"/> plus <paramref name="delay"/> milliseconds. 366 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>. 367 /// </summary> 368 /// <param name="delay">The delay after the latest end time of all <see cref="Transform"/>s.</param> 369 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param> 370 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 371 public TransformSequence<T> Then(double delay, params Generator[] childGenerators) 372 { 373 if (!hasEnd) 374 throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence<T>)}."); 375 376 // "Then" simply sets the currentTime to endTime to continue where the last transform left off, 377 // followed by a subsequent delay call. 378 currentTime = endTime; 379 return Delay(delay, childGenerators); 380 } 381 382 /// <summary> 383 /// Advances the start time of future appended <see cref="TransformSequence{T}"/>s by <paramref name="delay"/> milliseconds. 384 /// Then, <paramref name="childGenerators"/> are appended via <see cref="Append(IEnumerable{Generator})"/>. 385 /// </summary> 386 /// <param name="delay">The delay to advance the start time by.</param> 387 /// <param name="childGenerators">The optional <see cref="Generator"/>s for <see cref="TransformSequence{T}"/>s to be appended.</param> 388 /// <returns>This <see cref="TransformSequence{T}"/>.</returns> 389 public TransformSequence<T> Delay(double delay, params Generator[] childGenerators) 390 { 391 // After a delay statement, future transforms are appended after a currentTime which got offset by a delay. 392 currentTime += delay; 393 return Append(childGenerators); 394 } 395 396 /// <summary> 397 /// Registers a callback <paramref name="function"/> which is triggered once all <see cref="Transform"/>s in this 398 /// <see cref="TransformSequence{T}"/> complete successfully. 399 /// If all <see cref="Transform"/>s already completed successfully at the point of this call, then 400 /// <paramref name="function"/> is triggered immediately. 401 /// Only a single callback function may be registered. 402 /// </summary> 403 /// <param name="function">The callback function.</param> 404 public void OnComplete(Action<T> function) 405 { 406 if (!hasEnd) 407 throw new InvalidOperationException($"Can not perform {nameof(Then)} on an endless {nameof(TransformSequence<T>)}."); 408 409 subscribeComplete(() => function(origin)); 410 } 411 412 /// <summary> 413 /// Registers a callback <paramref name="function"/> which is triggered once any <see cref="Transform"/> in this 414 /// <see cref="TransformSequence{T}"/> is aborted (e.g. by another <see cref="Transform"/> overriding it). 415 /// Only a single callback function may be registered. 416 /// </summary> 417 /// <param name="function">The callback function.</param> 418 public void OnAbort(Action<T> function) => subscribeAbort(() => function(origin)); 419 420 /// <summary> 421 /// Registers a callback <paramref name="function"/> which is triggered once any <see cref="Transform"/> in this 422 /// <see cref="TransformSequence{T}"/> is aborted or when all <see cref="Transform"/>s complete successfully. 423 /// This is equivalent with calling both <see cref="OnComplete(Action{T})"/> and <see cref="OnAbort(Action{T})"/>. 424 /// Only a single callback function may be registered. 425 /// </summary> 426 /// <param name="function">The callback function.</param> 427 public void Finally(Action<T> function) 428 { 429 if (hasEnd) 430 OnComplete(function); 431 OnAbort(function); 432 } 433 434 void ITransformSequence.TransformAborted() 435 { 436 if (transforms.Count == 0) 437 return; 438 439 // No need for OnAbort events to trigger anymore, since 440 // we are already aware of the abortion. 441 foreach (var t in transforms) 442 { 443 t.AbortTargetSequence = null; 444 t.CompletionTargetSequence = null; 445 446 if (!t.HasStartValue) 447 t.TargetTransformable.RemoveTransform(t); 448 } 449 450 transforms.Clear(); 451 last = null; 452 453 onAbort?.Invoke(); 454 } 455 456 void ITransformSequence.TransformCompleted() 457 { 458 hasCompleted = true; 459 onComplete?.Invoke(); 460 } 461 } 462}