A game framework written with osu! in mind.
at master 382 lines 16 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.Diagnostics; 7using System.Linq; 8using osu.Framework.Lists; 9 10namespace osu.Framework.Graphics.Transforms 11{ 12 /// <summary> 13 /// Tracks the lifetime of transforms for one specified target member. 14 /// </summary> 15 internal class TargetGroupingTransformTracker 16 { 17 /// <summary> 18 /// A list of <see cref="Transform"/>s associated with the <see cref="TargetGrouping"/>. 19 /// </summary> 20 public IReadOnlyList<Transform> Transforms => transforms; 21 22 /// <summary> 23 /// The member this instance is tracking. 24 /// </summary> 25 public readonly string TargetGrouping; 26 27 private readonly SortedList<Transform> transforms = new SortedList<Transform>(Transform.COMPARER); 28 29 private readonly Transformable transformable; 30 31 private readonly Queue<(ITransformSequence sequence, Action<ITransformSequence> action)> removalActions = new Queue<(ITransformSequence, Action<ITransformSequence>)>(); 32 33 /// <summary> 34 /// Used to assign a monotonically increasing ID to <see cref="Transform"/>s as they are added. This member is 35 /// incremented whenever a <see cref="Transform"/> is added. 36 /// </summary> 37 private ulong currentTransformID; 38 39 /// <summary> 40 /// The index of the last transform in <see cref="transforms"/> to be applied to completion. 41 /// </summary> 42 private readonly Dictionary<string, int> lastAppliedTransformIndices = new Dictionary<string, int>(); 43 44 /// <summary> 45 /// All <see cref="Transform.TargetMember"/>s which are handled by this tracker. 46 /// </summary> 47 public IEnumerable<string> TargetMembers => targetMembers; 48 49 private readonly HashSet<string> targetMembers = new HashSet<string>(); 50 51 public TargetGroupingTransformTracker(Transformable transformable, string targetGrouping) 52 { 53 TargetGrouping = targetGrouping; 54 this.transformable = transformable; 55 } 56 57 public void UpdateTransforms(in double time, bool rewinding) 58 { 59 if (rewinding && !transformable.RemoveCompletedTransforms) 60 { 61 resetLastAppliedCache(); 62 63 var appliedToEndReverts = new List<string>(); 64 65 // Under the case that completed transforms are not removed, reversing the clock is permitted. 66 // We need to first look back through all the transforms and apply the start values of the ones that were previously 67 // applied, but now exist in the future relative to the current time. 68 for (int i = transforms.Count - 1; i >= 0; i--) 69 { 70 var t = transforms[i]; 71 72 // rewind logic needs to only run on transforms which have been applied at least once. 73 if (!t.Applied) 74 continue; 75 76 // some specific transforms can be marked as non-rewindable. 77 if (!t.Rewindable) 78 continue; 79 80 if (time >= t.StartTime) 81 { 82 // we are in the middle of this transform, so we want to mark as not-completely-applied. 83 // note that we should only do this for the last transform of each TargetMember to avoid incorrect application order. 84 // the actual application will be in the main loop below now that AppliedToEnd is false. 85 if (!appliedToEndReverts.Contains(t.TargetMember)) 86 { 87 t.AppliedToEnd = false; 88 appliedToEndReverts.Add(t.TargetMember); 89 } 90 } 91 else 92 { 93 // we are before the start time of this transform, so we want to eagerly apply the value at current time and mark as not-yet-applied. 94 // this transform will not be applied again unless we play forward in the future. 95 t.Apply(time); 96 t.Applied = false; 97 t.AppliedToEnd = false; 98 } 99 } 100 } 101 102 for (int i = getLastAppliedIndex(); i < transforms.Count; ++i) 103 { 104 var t = transforms[i]; 105 106 var tCanRewind = !transformable.RemoveCompletedTransforms && t.Rewindable; 107 108 bool flushAppliedCache = false; 109 110 if (time < t.StartTime) 111 break; 112 113 if (!t.Applied) 114 { 115 // This is the first time we are updating this transform. 116 // We will find other still active transforms which act on the same target member and remove them. 117 // Since following transforms acting on the same target member are immediately removed when a 118 // new one is added, we can be sure that previous transforms were added before this one and can 119 // be safely removed. 120 for (int j = getLastAppliedIndex(t.TargetMember); j < i; ++j) 121 { 122 var u = transforms[j]; 123 if (u.TargetMember != t.TargetMember) continue; 124 125 if (!u.AppliedToEnd) 126 // we may have applied the existing transforms too far into the future. 127 // we want to prepare to potentially read into the newly activated transform's StartTime, 128 // so we should re-apply using its StartTime as a basis. 129 u.Apply(t.StartTime); 130 131 if (!tCanRewind) 132 { 133 transforms.RemoveAt(j--); 134 flushAppliedCache = true; 135 i--; 136 137 if (u.AbortTargetSequence != null) 138 removalActions.Enqueue((u.AbortTargetSequence, s => s.TransformAborted())); 139 } 140 else 141 u.AppliedToEnd = true; 142 } 143 } 144 145 if (!t.HasStartValue) 146 { 147 t.ReadIntoStartValue(); 148 t.HasStartValue = true; 149 } 150 151 if (!t.AppliedToEnd) 152 { 153 t.Apply(time); 154 155 t.AppliedToEnd = time >= t.EndTime; 156 157 if (t.AppliedToEnd) 158 { 159 if (!tCanRewind) 160 { 161 transforms.RemoveAt(i--); 162 flushAppliedCache = true; 163 } 164 165 if (t.IsLooping) 166 { 167 if (tCanRewind) 168 { 169 t.IsLooping = false; 170 t = t.Clone(); 171 } 172 173 t.AppliedToEnd = false; 174 t.Applied = false; 175 t.HasStartValue = false; 176 177 t.IsLooping = true; 178 179 t.StartTime += t.LoopDelay; 180 t.EndTime += t.LoopDelay; 181 182 // this could be added back at a lower index than where we are currently iterating, but 183 // running the same transform twice isn't a huge deal. 184 transforms.Add(t); 185 flushAppliedCache = true; 186 } 187 else if (t.CompletionTargetSequence != null) 188 removalActions.Enqueue((t.CompletionTargetSequence, s => s.TransformCompleted())); 189 } 190 } 191 192 if (flushAppliedCache) 193 resetLastAppliedCache(); 194 // if this transform is applied to end, we can be sure that all previous transforms have been completed. 195 else if (t.AppliedToEnd) 196 setLastAppliedIndex(t.TargetMember, i + 1); 197 } 198 199 invokePendingRemovalActions(); 200 } 201 202 /// <summary> 203 /// Adds to this object a <see cref="Transform"/> which was previously populated using this object via 204 /// <see cref="TransformableExtensions.PopulateTransform{TValue, TEasing, TThis}"/>. 205 /// Added <see cref="Transform"/>s are immediately applied, and therefore have an immediate effect on this object if the current time of this 206 /// object falls within <see cref="Transform.StartTime"/> and <see cref="Transform.EndTime"/>. 207 /// If <see cref="Transformable.Clock"/> is null, e.g. because this object has just been constructed, then the given transform will be finished instantaneously. 208 /// </summary> 209 /// <param name="transform">The <see cref="Transform"/> to be added.</param> 210 /// <param name="customTransformID">When not null, the <see cref="Transform.TransformID"/> to assign for ordering.</param> 211 public void AddTransform(Transform transform, ulong? customTransformID = null) 212 { 213 Debug.Assert(!(transform.TransformID == 0 && transforms.Contains(transform)), $"Zero-id {nameof(Transform)}s should never be contained already."); 214 215 if (transform.TargetGrouping != TargetGrouping) 216 throw new ArgumentException($"Target grouping \"{transform.TargetGrouping}\" does not match this tracker's grouping \"{TargetGrouping}\".", nameof(transform)); 217 218 targetMembers.Add(transform.TargetMember); 219 220 // This contains check may be optimized away in the future, should it become a bottleneck 221 if (transform.TransformID != 0 && transforms.Contains(transform)) 222 throw new InvalidOperationException($"{nameof(Transformable)} may not contain the same {nameof(Transform)} more than once."); 223 224 transform.TransformID = customTransformID ?? ++currentTransformID; 225 int insertionIndex = transforms.Add(transform); 226 resetLastAppliedCache(); 227 228 // Remove all existing following transforms touching the same property as this one. 229 for (int i = insertionIndex + 1; i < transforms.Count; ++i) 230 { 231 var t = transforms[i]; 232 233 if (t.TargetMember == transform.TargetMember) 234 { 235 transforms.RemoveAt(i--); 236 if (t.AbortTargetSequence != null) 237 removalActions.Enqueue((t.AbortTargetSequence, s => s.TransformAborted())); 238 } 239 } 240 241 invokePendingRemovalActions(); 242 } 243 244 /// <summary> 245 /// Removes a <see cref="Transform"/>. 246 /// </summary> 247 /// <param name="toRemove">The <see cref="Transform"/> to remove.</param> 248 public void RemoveTransform(Transform toRemove) 249 { 250 transforms.Remove(toRemove); 251 resetLastAppliedCache(); 252 } 253 254 /// <summary> 255 /// Removes <see cref="Transform"/>s that start after <paramref name="time"/>. 256 /// </summary> 257 /// <param name="time">The time to clear <see cref="Transform"/>s after.</param> 258 /// <param name="targetMember"> 259 /// An optional <see cref="Transform.TargetMember"/> name of <see cref="Transform"/>s to clear. 260 /// Null for clearing all <see cref="Transform"/>s. 261 /// </param> 262 public virtual void ClearTransformsAfter(double time, string targetMember = null) 263 { 264 resetLastAppliedCache(); 265 266 if (targetMember == null) 267 { 268 for (int i = 0; i < transforms.Count; i++) 269 { 270 var t = transforms[i]; 271 272 if (t.StartTime >= time) 273 { 274 transforms.RemoveAt(i--); 275 if (t.AbortTargetSequence != null) 276 removalActions.Enqueue((t.AbortTargetSequence, s => s.TransformAborted())); 277 } 278 } 279 } 280 else 281 { 282 for (int i = 0; i < transforms.Count; i++) 283 { 284 var t = transforms[i]; 285 286 if (t.TargetMember == targetMember && t.StartTime >= time) 287 { 288 transforms.RemoveAt(i--); 289 if (t.AbortTargetSequence != null) 290 removalActions.Enqueue((t.AbortTargetSequence, s => s.TransformAborted())); 291 } 292 } 293 } 294 295 invokePendingRemovalActions(); 296 } 297 298 /// <summary> 299 /// Finishes specified <see cref="Transform"/>s, using their <see cref="Transform{TValue}.EndValue"/>. 300 /// </summary> 301 /// <param name="targetMember"> 302 /// An optional <see cref="Transform.TargetMember"/> name of <see cref="Transform"/>s to finish. 303 /// Null for finishing all <see cref="Transform"/>s. 304 /// </param> 305 public virtual void FinishTransforms(string targetMember = null) 306 { 307 Func<Transform, bool> toFlushPredicate; 308 if (targetMember == null) 309 toFlushPredicate = t => !t.IsLooping; 310 else 311 toFlushPredicate = t => !t.IsLooping && t.TargetMember == targetMember; 312 313 // Flush is undefined for endlessly looping transforms 314 var toFlush = transforms.Where(toFlushPredicate).ToArray(); 315 316 transforms.RemoveAll(t => toFlushPredicate(t)); 317 resetLastAppliedCache(); 318 319 foreach (Transform t in toFlush) 320 { 321 if (!t.HasStartValue) 322 { 323 t.ReadIntoStartValue(); 324 t.HasStartValue = true; 325 } 326 327 t.Apply(t.EndTime); 328 t.TriggerComplete(); 329 } 330 } 331 332 private void invokePendingRemovalActions() 333 { 334 while (removalActions.TryDequeue(out var item)) 335 item.action(item.sequence); 336 } 337 338 /// <summary> 339 /// Retrieve the last transform index that was <see cref="Transform.AppliedToEnd"/>. 340 /// </summary> 341 /// <param name="targetMember">An optional target member. If null, the lowest common last application is returned.</param> 342 private int getLastAppliedIndex(string targetMember = null) 343 { 344 if (targetMember == null) 345 { 346 int min = int.MaxValue; 347 348 foreach (int i in lastAppliedTransformIndices.Values) 349 { 350 if (i < min) 351 min = i; 352 } 353 354 return min; 355 } 356 357 if (lastAppliedTransformIndices.TryGetValue(targetMember, out int val)) 358 return val; 359 360 return 0; 361 } 362 363 /// <summary> 364 /// Set the last transform index that was <see cref="Transform.AppliedToEnd"/> for a specific target member. 365 /// </summary> 366 /// <param name="targetMember">The target member to set the index of.</param> 367 /// <param name="index">The index of the transform in <see cref="transforms"/>.</param> 368 private void setLastAppliedIndex(string targetMember, int index) 369 { 370 lastAppliedTransformIndices[targetMember] = index; 371 } 372 373 /// <summary> 374 /// Reset the last applied index cache completely. 375 /// </summary> 376 private void resetLastAppliedCache() 377 { 378 foreach (var tracked in targetMembers) 379 lastAppliedTransformIndices[tracked] = 0; 380 } 381 } 382}