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 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}