A game framework written with osu! in mind.
at master 247 lines 10 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.Threading; 6using osu.Framework.Allocation; 7using osu.Framework.Extensions.PolygonExtensions; 8using osu.Framework.Graphics.Primitives; 9using osu.Framework.Layout; 10using osu.Framework.Threading; 11 12namespace osu.Framework.Graphics.Containers 13{ 14 /// <summary> 15 /// A container which asynchronously loads specified content. 16 /// Has the ability to delay the loading until it has been visible on-screen for a specified duration. 17 /// In order to benefit from delayed load, we must be inside a <see cref="ScrollContainer{T}"/>. 18 /// </summary> 19 public class DelayedLoadWrapper : CompositeDrawable 20 { 21 [Resolved] 22 protected Game Game { get; private set; } 23 24 private readonly Func<Drawable> createFunc; 25 26 /// <summary> 27 /// Creates a <see cref="Container"/> that will asynchronously load the given <see cref="Drawable"/> with a delay. 28 /// </summary> 29 /// <remarks>If <see cref="timeBeforeLoad"/> is set to 0, the loading process will begin on the next Update call.</remarks> 30 /// <param name="content">The <see cref="Drawable"/> to be loaded.</param> 31 /// <param name="timeBeforeLoad">The delay in milliseconds before loading can begin.</param> 32 public DelayedLoadWrapper(Drawable content, double timeBeforeLoad = 500) 33 : this(timeBeforeLoad) 34 { 35 Content = content ?? throw new ArgumentNullException(nameof(content), $@"{nameof(DelayedLoadWrapper)} required non-null {nameof(content)}."); 36 } 37 38 /// <summary> 39 /// Creates a <see cref="Container"/> that will asynchronously load the given <see cref="Drawable"/> with a delay. 40 /// This constructor is preferred due to avoiding construction of the loadable content until a load is actually triggered. 41 /// </summary> 42 /// <remarks>If <see cref="timeBeforeLoad"/> is set to 0, the loading process will begin on the next Update call.</remarks> 43 /// <param name="createFunc">A function which created future content.</param> 44 /// <param name="timeBeforeLoad">The delay in milliseconds before loading can begin.</param> 45 public DelayedLoadWrapper(Func<Drawable> createFunc, double timeBeforeLoad = 500) 46 : this(timeBeforeLoad) 47 { 48 this.createFunc = createFunc; 49 } 50 51 private DelayedLoadWrapper(double timeBeforeLoad) 52 { 53 this.timeBeforeLoad = timeBeforeLoad; 54 55 AddLayout(optimisingContainerCache); 56 AddLayout(isIntersectingCache); 57 } 58 59 private Drawable content; 60 61 public Drawable Content 62 { 63 get => content; 64 protected set 65 { 66 if (content == value) 67 return; 68 69 content = value; 70 71 if (content == null) 72 return; 73 74 AutoSizeAxes = Axes.None; 75 RelativeSizeAxes = Axes.None; 76 77 RelativeSizeAxes = content.RelativeSizeAxes; 78 AutoSizeAxes = (content as CompositeDrawable)?.AutoSizeAxes ?? AutoSizeAxes; 79 } 80 } 81 82 /// <summary> 83 /// The amount of time on-screen in milliseconds before we begin a load of children. 84 /// </summary> 85 private readonly double timeBeforeLoad; 86 87 private double timeVisible; 88 89 protected virtual bool ShouldLoadContent => timeVisible > timeBeforeLoad; 90 91 private CancellationTokenSource cancellationTokenSource; 92 private ScheduledDelegate scheduledAddition; 93 94 protected override void Update() 95 { 96 base.Update(); 97 98 // This code can be expensive, so only run if we haven't yet loaded. 99 if (DelayedLoadCompleted || DelayedLoadTriggered) return; 100 101 if (!IsIntersecting) 102 timeVisible = 0; 103 else 104 timeVisible += Time.Elapsed; 105 106 if (ShouldLoadContent) 107 BeginDelayedLoad(); 108 } 109 110 protected void BeginDelayedLoad() 111 { 112 if (DelayedLoadTriggered || DelayedLoadCompleted) 113 throw new InvalidOperationException("Load has already started!"); 114 115 Content ??= createFunc(); 116 117 DelayedLoadTriggered = true; 118 DelayedLoadStarted?.Invoke(Content); 119 120 cancellationTokenSource = new CancellationTokenSource(); 121 122 // The callback is run on the game's scheduler since DelayedLoadUnloadWrapper needs to unload when no updates are being received. 123 LoadComponentAsync(Content, EndDelayedLoad, scheduler: Game.Scheduler, cancellation: cancellationTokenSource.Token); 124 } 125 126 protected virtual void EndDelayedLoad(Drawable content) 127 { 128 timeVisible = 0; 129 130 // This code is running on the game's scheduler, while this wrapper may have been async disposed, so the addition is scheduled locally to prevent adding to disposed wrappers. 131 scheduledAddition = Schedule(() => 132 { 133 AddInternal(content); 134 135 DelayedLoadCompleted = true; 136 DelayedLoadComplete?.Invoke(content); 137 }); 138 } 139 140 internal override void UnbindAllBindables() 141 { 142 base.UnbindAllBindables(); 143 CancelTasks(); 144 } 145 146 protected override void Dispose(bool isDisposing) 147 { 148 base.Dispose(isDisposing); 149 CancelTasks(); 150 } 151 152 protected virtual void CancelTasks() 153 { 154 isIntersectingCache.Invalidate(); 155 156 cancellationTokenSource?.Cancel(); 157 cancellationTokenSource = null; 158 159 scheduledAddition?.Cancel(); 160 scheduledAddition = null; 161 } 162 163 /// <summary> 164 /// Fired when delayed async load has started. 165 /// </summary> 166 public event Action<Drawable> DelayedLoadStarted; 167 168 /// <summary> 169 /// Fired when delayed async load completes. Should be used to perform transitions. 170 /// </summary> 171 public event Action<Drawable> DelayedLoadComplete; 172 173 /// <summary> 174 /// True if the load task for our content has been started. 175 /// Will remain true even after load is completed. 176 /// </summary> 177 protected bool DelayedLoadTriggered; 178 179 /// <summary> 180 /// True if the content has been added to the drawable hierarchy. 181 /// </summary> 182 public bool DelayedLoadCompleted { get; protected set; } 183 184 private readonly LayoutValue optimisingContainerCache = new LayoutValue(Invalidation.Parent); 185 private readonly LayoutValue isIntersectingCache = new LayoutValue(Invalidation.All); 186 private ScheduledDelegate isIntersectingResetDelegate; 187 188 protected bool IsIntersecting { get; private set; } 189 190 internal IOnScreenOptimisingContainer OptimisingContainer { get; private set; } 191 192 internal IOnScreenOptimisingContainer FindParentOptimisingContainer() => FindClosestParent<IOnScreenOptimisingContainer>(); 193 194 protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) 195 { 196 var result = base.OnInvalidate(invalidation, source); 197 198 // For every invalidation, we schedule a reset of IsIntersecting to the game. 199 // This is done since UpdateSubTreeMasking() may not be invoked in the current frame, as a result of presence/masking changes anywhere in our super-tree. 200 // It is important that this is scheduled such that it occurs on the NEXT frame, in order to give this wrapper a chance to load its contents. 201 // For example, if a parent invalidated this wrapper every frame, IsIntersecting would be false by the time Update() is run and may only become true at the very end of the frame. 202 // The scheduled delegate will be cancelled if this wrapper has its UpdateSubTreeMasking() invoked, as more accurate intersections can be computed there instead. 203 if (isIntersectingResetDelegate == null) 204 { 205 isIntersectingResetDelegate = Game?.Scheduler.AddDelayed(() => IsIntersecting = false, 0); 206 result = true; 207 } 208 209 return result; 210 } 211 212 public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) 213 { 214 bool result = base.UpdateSubTreeMasking(source, maskingBounds); 215 216 // We can accurately compute intersections - the scheduled reset is no longer required. 217 isIntersectingResetDelegate?.Cancel(); 218 isIntersectingResetDelegate = null; 219 220 if (!isIntersectingCache.IsValid) 221 { 222 if (!optimisingContainerCache.IsValid) 223 { 224 OptimisingContainer = FindParentOptimisingContainer(); 225 optimisingContainerCache.Validate(); 226 } 227 228 // The first condition is an intersection against the hierarchy, including any parents that may be masking this wrapper. 229 // It is the same calculation as Drawable.IsMaskedAway, however IsMaskedAway is optimised out for some CompositeDrawables (which this wrapper is). 230 // The second condition is an exact intersection against the optimising container, which further optimises rotated AABBs where the wrapper content is not visible. 231 IsIntersecting = maskingBounds.IntersectsWith(ScreenSpaceDrawQuad.AABBFloat) 232 && OptimisingContainer?.ScreenSpaceDrawQuad.Intersects(ScreenSpaceDrawQuad) != false; 233 234 isIntersectingCache.Validate(); 235 } 236 237 return result; 238 } 239 240 /// <summary> 241 /// A container which acts as a masking parent for on-screen delayed load optimisations. 242 /// </summary> 243 internal interface IOnScreenOptimisingContainer : IDrawable 244 { 245 } 246 } 247}