A game framework written with osu! in mind.
at master 396 lines 18 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 osu.Framework.Allocation; 8using osu.Framework.Timing; 9using osu.Framework.Utils; 10 11namespace osu.Framework.Graphics.Transforms 12{ 13 /// <summary> 14 /// A type of object which can have <see cref="Transform"/>s operating upon it. 15 /// An implementer of this class must call <see cref="UpdateTransforms"/> to 16 /// update and apply its <see cref="Transform"/>s. 17 /// </summary> 18 public abstract class Transformable : ITransformable 19 { 20 /// <summary> 21 /// The clock that is used to provide the timing for this object's <see cref="Transform"/>s. 22 /// </summary> 23 public abstract IFrameBasedClock Clock { get; set; } 24 25 /// <summary> 26 /// The current frame's time as observed by this class's <see cref="Clock"/>. 27 /// </summary> 28 public FrameTimeInfo Time => Clock.TimeInfo; 29 30 /// <summary> 31 /// The starting time to use for new <see cref="Transform"/>s. 32 /// </summary> 33 public double TransformStartTime => (Clock?.CurrentTime ?? 0) + TransformDelay; 34 35 /// <summary> 36 /// Delay from the current time until new <see cref="Transform"/>s are started, in milliseconds. 37 /// </summary> 38 protected double TransformDelay { get; private set; } 39 40 /// <summary> 41 /// A lazily-initialized list of <see cref="Transform"/>s applied to this object. 42 /// </summary> 43 public IEnumerable<Transform> Transforms => targetGroupingTrackers.SelectMany(t => t.Transforms); 44 45 /// <summary> 46 /// Retrieves the <see cref="Transform"/>s for a given target member. 47 /// </summary> 48 /// <param name="targetMember">The target member to find the <see cref="Transform"/>s for.</param> 49 /// <returns>An enumeration over the transforms for the target member.</returns> 50 public IEnumerable<Transform> TransformsForTargetMember(string targetMember) => 51 getTrackerFor(targetMember)?.Transforms ?? Enumerable.Empty<Transform>(); 52 53 /// <summary> 54 /// The end time in milliseconds of the latest transform enqueued for this <see cref="Transformable"/>. 55 /// Will return the current time value if no transforms are present. 56 /// </summary> 57 public double LatestTransformEndTime 58 { 59 get 60 { 61 //expiry should happen either at the end of the last transform or using the current sequence delay (whichever is highest). 62 double max = TransformStartTime; 63 64 foreach (var tracker in targetGroupingTrackers) 65 { 66 for (int i = 0; i < tracker.Transforms.Count; i++) 67 { 68 var t = tracker.Transforms[i]; 69 if (t.EndTime > max) 70 max = t.EndTime + 1; //adding 1ms here ensures we can expire on the current frame without issue. 71 } 72 } 73 74 return max; 75 } 76 } 77 78 /// <summary> 79 /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. 80 /// </summary> 81 public virtual bool RemoveCompletedTransforms { get; internal set; } = true; 82 83 /// <summary> 84 /// Resets <see cref="TransformDelay"/> and processes updates to this class based on loaded <see cref="Transform"/>s. 85 /// </summary> 86 protected void UpdateTransforms() 87 { 88 TransformDelay = 0; 89 updateTransforms(Time.Current); 90 } 91 92 private double lastUpdateTransformsTime; 93 94 private readonly List<TargetGroupingTransformTracker> targetGroupingTrackers = new List<TargetGroupingTransformTracker>(); 95 96 private TargetGroupingTransformTracker getTrackerFor(string targetMember) 97 { 98 foreach (var t in targetGroupingTrackers) 99 { 100 if (t.TargetMembers.Contains(targetMember)) 101 return t; 102 } 103 104 return null; 105 } 106 107 private TargetGroupingTransformTracker getTrackerForGrouping(string targetGrouping, bool createIfNotExisting) 108 { 109 foreach (var t in targetGroupingTrackers) 110 { 111 if (t.TargetGrouping == targetGrouping) 112 return t; 113 } 114 115 if (!createIfNotExisting) 116 return null; 117 118 var tracker = new TargetGroupingTransformTracker(this, targetGrouping); 119 targetGroupingTrackers.Add(tracker); 120 return tracker; 121 } 122 123 /// <summary> 124 /// Process updates to this class based on loaded <see cref="Transform"/>s. This does not reset <see cref="TransformDelay"/>. 125 /// This is used for performing extra updates on <see cref="Transform"/>s when new <see cref="Transform"/>s are added. 126 /// </summary> 127 /// <param name="time">The point in time to update transforms to.</param> 128 /// <param name="forceRewindReprocess">Whether prior transforms should be reprocessed even if a rewind was not detected.</param> 129 private void updateTransforms(double time, bool forceRewindReprocess = false) 130 { 131 bool rewinding = lastUpdateTransformsTime > time || forceRewindReprocess; 132 lastUpdateTransformsTime = time; 133 134 // collection may grow due to abort / completion events. 135 for (var i = 0; i < targetGroupingTrackers.Count; i++) 136 targetGroupingTrackers[i].UpdateTransforms(time, rewinding); 137 } 138 139 /// <summary> 140 /// Removes a <see cref="Transform"/>. 141 /// </summary> 142 /// <param name="toRemove">The <see cref="Transform"/> to remove.</param> 143 public void RemoveTransform(Transform toRemove) 144 { 145 EnsureTransformMutationAllowed(); 146 147 getTrackerForGrouping(toRemove.TargetGrouping, false)?.RemoveTransform(toRemove); 148 149 toRemove.TriggerAbort(); 150 } 151 152 /// <summary> 153 /// Clears <see cref="Transform"/>s. 154 /// </summary> 155 /// <param name="propagateChildren">Whether we also clear the <see cref="Transform"/>s of children.</param> 156 /// <param name="targetMember"> 157 /// An optional <see cref="Transform.TargetMember"/> name of <see cref="Transform"/>s to clear. 158 /// Null for clearing all <see cref="Transform"/>s. 159 /// </param> 160 public virtual void ClearTransforms(bool propagateChildren = false, string targetMember = null) 161 { 162 EnsureTransformMutationAllowed(); 163 164 ClearTransformsAfter(double.NegativeInfinity, propagateChildren, targetMember); 165 } 166 167 /// <summary> 168 /// Removes <see cref="Transform"/>s that start after <paramref name="time"/>. 169 /// </summary> 170 /// <param name="time">The time to clear <see cref="Transform"/>s after.</param> 171 /// <param name="propagateChildren">Whether to also clear such <see cref="Transform"/>s of children.</param> 172 /// <param name="targetMember"> 173 /// An optional <see cref="Transform.TargetMember"/> name of <see cref="Transform"/>s to clear. 174 /// Null for clearing all <see cref="Transform"/>s. 175 /// </param> 176 public virtual void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) 177 { 178 EnsureTransformMutationAllowed(); 179 180 if (targetMember != null) 181 { 182 getTrackerFor(targetMember)?.ClearTransformsAfter(time, targetMember); 183 } 184 else 185 { 186 // collection may grow due to abort / completion events. 187 for (var i = 0; i < targetGroupingTrackers.Count; i++) 188 targetGroupingTrackers[i].ClearTransformsAfter(time); 189 } 190 } 191 192 /// <summary> 193 /// Applies <see cref="Transform"/>s at a point in time. This may only be called if <see cref="RemoveCompletedTransforms"/> is set to false. 194 /// <para> 195 /// This does not change the clock time. 196 /// </para> 197 /// </summary> 198 /// <param name="time">The time to apply <see cref="Transform"/>s at.</param> 199 /// <param name="propagateChildren">Whether to also apply children's <see cref="Transform"/>s at <paramref name="time"/>.</param> 200 public virtual void ApplyTransformsAt(double time, bool propagateChildren = false) 201 { 202 EnsureTransformMutationAllowed(); 203 204 if (RemoveCompletedTransforms) throw new InvalidOperationException($"Cannot arbitrarily apply transforms with {nameof(RemoveCompletedTransforms)} active."); 205 206 updateTransforms(time); 207 } 208 209 /// <summary> 210 /// Finishes specified <see cref="Transform"/>s, using their <see cref="Transform{TValue}.EndValue"/>. 211 /// </summary> 212 /// <param name="propagateChildren">Whether we also finish the <see cref="Transform"/>s of children.</param> 213 /// <param name="targetMember"> 214 /// An optional <see cref="Transform.TargetMember"/> name of <see cref="Transform"/>s to finish. 215 /// Null for finishing all <see cref="Transform"/>s. 216 /// </param> 217 public virtual void FinishTransforms(bool propagateChildren = false, string targetMember = null) 218 { 219 EnsureTransformMutationAllowed(); 220 221 if (targetMember != null) 222 { 223 getTrackerFor(targetMember)?.FinishTransforms(targetMember); 224 } 225 else 226 { 227 // collection may grow due to abort / completion events. 228 for (var i = 0; i < targetGroupingTrackers.Count; i++) 229 targetGroupingTrackers[i].FinishTransforms(); 230 } 231 } 232 233 /// <summary> 234 /// Add a delay duration to <see cref="TransformDelay"/>, in milliseconds. 235 /// </summary> 236 /// <param name="duration">The delay duration to add.</param> 237 /// <param name="propagateChildren">Whether we also delay down the child tree.</param> 238 /// <returns>This</returns> 239 internal virtual void AddDelay(double duration, bool propagateChildren = false) => TransformDelay += duration; 240 241 /// <summary> 242 /// Start a sequence of <see cref="Transform"/>s with a (cumulative) relative delay applied. 243 /// </summary> 244 /// <param name="delay">The offset in milliseconds from current time. Note that this stacks with other nested sequences.</param> 245 /// <param name="recursive">Whether this should be applied to all children. True by default.</param> 246 /// <returns>An <see cref="InvokeOnDisposal"/> to be used in a using() statement.</returns> 247 public IDisposable BeginDelayedSequence(double delay, bool recursive = true) 248 { 249 EnsureTransformMutationAllowed(); 250 251 if (delay == 0) 252 return null; 253 254 AddDelay(delay, recursive); 255 double newTransformDelay = TransformDelay; 256 257 return new ValueInvokeOnDisposal<DelayedSequenceSender>(new DelayedSequenceSender(this, delay, recursive, newTransformDelay), sender => 258 { 259 if (!Precision.AlmostEquals(sender.NewTransformDelay, sender.Transformable.TransformDelay)) 260 { 261 throw new InvalidOperationException( 262 $"{nameof(sender.Transformable.TransformStartTime)} at the end of delayed sequence is not the same as at the beginning, but should be. " + 263 $"(begin={sender.NewTransformDelay} end={sender.Transformable.TransformDelay})"); 264 } 265 266 AddDelay(-sender.Delay, sender.Recursive); 267 }); 268 } 269 270 /// An ad-hoc struct used as a closure environment in <see cref="BeginDelayedSequence" />. 271 private readonly struct DelayedSequenceSender 272 { 273 public readonly Transformable Transformable; 274 public readonly double Delay; 275 public readonly bool Recursive; 276 public readonly double NewTransformDelay; 277 278 public DelayedSequenceSender(Transformable transformable, double delay, bool recursive, double newTransformDelay) 279 { 280 Transformable = transformable; 281 Delay = delay; 282 Recursive = recursive; 283 NewTransformDelay = newTransformDelay; 284 } 285 } 286 287 /// <summary> 288 /// Start a sequence of <see cref="Transform"/>s from an absolute time value (adjusts <see cref="TransformStartTime"/>). 289 /// </summary> 290 /// <param name="newTransformStartTime">The new value for <see cref="TransformStartTime"/>.</param> 291 /// <param name="recursive">Whether this should be applied to all children. True by default.</param> 292 /// <returns>An <see cref="InvokeOnDisposal"/> to be used in a using() statement.</returns> 293 /// <exception cref="InvalidOperationException">Absolute sequences should never be nested inside another existing sequence.</exception> 294 public virtual IDisposable BeginAbsoluteSequence(double newTransformStartTime, bool recursive = true) 295 { 296 EnsureTransformMutationAllowed(); 297 298 return createAbsoluteSequenceAction(newTransformStartTime); 299 } 300 301 internal virtual void CollectAbsoluteSequenceActionsFromSubTree(double newTransformStartTime, List<AbsoluteSequenceSender> actions) 302 { 303 actions.Add(createAbsoluteSequenceAction(newTransformStartTime)); 304 } 305 306 private AbsoluteSequenceSender createAbsoluteSequenceAction(double newTransformStartTime) 307 { 308 double oldTransformDelay = TransformDelay; 309 double newTransformDelay = TransformDelay = newTransformStartTime - (Clock?.CurrentTime ?? 0); 310 311 return new AbsoluteSequenceSender(this, oldTransformDelay, newTransformDelay); 312 } 313 314 /// An ad-hoc struct used as a closure environment in <see cref="BeginAbsoluteSequence" />. 315 internal readonly struct AbsoluteSequenceSender : IDisposable 316 { 317 public readonly Transformable Sender; 318 319 public readonly double OldTransformDelay; 320 public readonly double NewTransformDelay; 321 322 public AbsoluteSequenceSender(Transformable sender, double oldTransformDelay, double newTransformDelay) 323 { 324 OldTransformDelay = oldTransformDelay; 325 NewTransformDelay = newTransformDelay; 326 327 Sender = sender; 328 } 329 330 public void Dispose() 331 { 332 if (!Precision.AlmostEquals(NewTransformDelay, Sender.TransformDelay)) 333 { 334 throw new InvalidOperationException( 335 $"{nameof(Sender.TransformStartTime)} at the end of absolute sequence is not the same as at the beginning, but should be. " + 336 $"(begin={NewTransformDelay} end={Sender.TransformDelay})"); 337 } 338 339 Sender.TransformDelay = OldTransformDelay; 340 } 341 } 342 343 /// <summary> 344 /// Adds to this object a <see cref="Transform"/> which was previously populated using this object via 345 /// <see cref="TransformableExtensions.PopulateTransform{TValue, TEasing, TThis}"/>. 346 /// Added <see cref="Transform"/>s are immediately applied, and therefore have an immediate effect on this object if the current time of this 347 /// object falls within <see cref="Transform.StartTime"/> and <see cref="Transform.EndTime"/>. 348 /// If <see cref="Clock"/> is null, e.g. because this object has just been constructed, then the given transform will be finished instantaneously. 349 /// </summary> 350 /// <param name="transform">The <see cref="Transform"/> to be added.</param> 351 /// <param name="customTransformID">When not null, the <see cref="Transform.TransformID"/> to assign for ordering.</param> 352 public void AddTransform(Transform transform, ulong? customTransformID = null) 353 { 354 EnsureTransformMutationAllowed(); 355 356 if (transform == null) 357 throw new ArgumentNullException(nameof(transform)); 358 359 if (!ReferenceEquals(transform.TargetTransformable, this)) 360 { 361 throw new InvalidOperationException( 362 $"{nameof(transform)} must have been populated via {nameof(TransformableExtensions)}.{nameof(TransformableExtensions.PopulateTransform)} " + 363 "using this object prior to being added."); 364 } 365 366 if (Clock == null) 367 { 368 if (!transform.HasStartValue) 369 { 370 transform.ReadIntoStartValue(); 371 transform.HasStartValue = true; 372 } 373 374 transform.Apply(transform.EndTime); 375 transform.TriggerComplete(); 376 377 return; 378 } 379 380 getTrackerForGrouping(transform.TargetGrouping, true).AddTransform(transform, customTransformID); 381 382 // If our newly added transform could have an immediate effect, then let's 383 // make this effect happen immediately. 384 // This is done globally instead of locally in the single member tracker 385 // to keep the transformable's state consistent (e.g. with lastUpdateTransformsTime) 386 if (transform.StartTime < Time.Current || transform.EndTime <= Time.Current) 387 updateTransforms(Time.Current, !RemoveCompletedTransforms && transform.StartTime <= Time.Current); 388 } 389 390 /// <summary> 391 /// Check whether the current thread is valid for operating on thread-safe properties. 392 /// Will throw on failure. 393 /// </summary> 394 internal abstract void EnsureTransformMutationAllowed(); 395 } 396}