diff --git a/osu.Android.props b/osu.Android.props index 4657896fac..6dab6edc5e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 6c077eb214..fe67b63252 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests { addMultipleObjectsStep(); - AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); + AddStep("move hitobject", () => + { + var manualClock = new ManualClock(); + followPointRenderer.Clock = new FramedClock(manualClock); + + manualClock.CurrentTime = getObject(1).HitObject.StartTime; + followPointRenderer.UpdateSubTree(); + + getObject(2).HitObject.Position = new Vector2(300, 100); + }); assertGroups(); + assertDirections(); } [TestCase(0, 0)] // Start -> Start @@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertGroups() { - AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); + AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count); AddAssert("group endpoints are correct", () => { for (int i = 0; i < hitObjectContainer.Count; i++) @@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests DrawableOsuHitObject expectedStart = getObject(i); DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; - if (getGroup(i).Start != expectedStart.HitObject) + if (getEntry(i).Start != expectedStart.HitObject) throw new AssertionException($"Object {i} expected to be the start of group {i}."); - if (getGroup(i).End != expectedEnd?.HitObject) + if (getEntry(i).End != expectedEnd?.HitObject) throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); } @@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests if (expectedEnd == null) continue; + var manualClock = new ManualClock(); + followPointRenderer.Clock = new FramedClock(manualClock); + + manualClock.CurrentTime = expectedStart.HitObject.StartTime; + followPointRenderer.UpdateSubTree(); + var points = getGroup(i).ChildrenOfType().ToArray(); if (points.Length == 0) continue; @@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; - private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; + private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index]; + + private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType().Single(c => c.Entry == getEntry(index)); private class TestHitObjectContainer : Container { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 596bc06c68..1278a0ff2d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -1,17 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osuTK; -using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests } private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + { + var drawable = createSingle(circleSize, auto, timeOffset, positionOffset); + + var playfield = new TestOsuPlayfield(); + playfield.Add(drawable); + return playfield; + } + + private Drawable testStream(float circleSize, bool auto = false) + { + var playfield = new TestOsuPlayfield(); + + Vector2 pos = new Vector2(-250, 0); + + for (int i = 0; i <= 1000; i += 100) + { + playfield.Add(createSingle(circleSize, auto, i, pos)); + pos.X += 50; + } + + return playfield; + } + + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset) { positionOffset ??= Vector2.Zero; var circle = new HitCircle { StartTime = Time.Current + 1000 + timeOffset, - Position = positionOffset.Value, + Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value, }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); @@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - return drawable; } protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) { - Anchor = Anchor.Centre, Depth = depthIndex++ }; - private Drawable testStream(float circleSize, bool auto = false) - { - var container = new Container { RelativeSizeAxes = Axes.Both }; - - Vector2 pos = new Vector2(-250, 0); - - for (int i = 0; i <= 1000; i += 100) - { - container.Add(testSingle(circleSize, auto, i, pos)); - pos.X += 50; - } - - return container; - } - protected class TestDrawableHitCircle : DrawableHitCircle { private readonly bool auto; @@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests base.CheckForResult(userTriggered, timeOffset); } } + + protected class TestOsuPlayfield : OsuPlayfield + { + public TestOsuPlayfield() + { + RelativeSizeAxes = Axes.Both; + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index d692be89b2..7e973d0971 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Diagnostics; +using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneShaking : TestSceneHitCircle { + private readonly List scheduledTasks = new List(); + + protected override IBeatmap CreateBeatmapForSkinProvider() + { + // best way to run cleanup before a new step is run + foreach (var task in scheduledTasks) + task.Cancel(); + + scheduledTasks.Clear(); + + return base.CreateBeatmapForSkinProvider(); + } + protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) { var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); @@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests Debug.Assert(drawableHitObject.HitObject.HitWindows != null); double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current; - Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay); + scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay)); return drawableHitObject; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 7375c0e981..ce5dc4855e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. + internal readonly Container Pieces; internal readonly Container Connections; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index a981648444..3e2ab65bb2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Skinning; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container, IAnimationTimeReference + public class FollowPoint : PoolableDrawable, IAnimationTimeReference { private const float width = 8; @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { Origin = Anchor.Centre; - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer + InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer { Masking = true, AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3a9e19b361..700d96eff3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; -using JetBrains.Annotations; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; using osuTK; @@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// Visualises the s between two s. /// - public class FollowPointConnection : CompositeDrawable + public class FollowPointConnection : PoolableDrawable { // Todo: These shouldn't be constants - private const int spacing = 32; - private const double preempt = 800; + public const int SPACING = 32; + public const double PREEMPT = 800; - public override bool RemoveWhenNotAlive => false; + public FollowPointLifetimeEntry Entry; + public DrawablePool Pool; - /// - /// The start time of . - /// - public readonly Bindable StartTime = new BindableDouble(); - - /// - /// The which s will exit from. - /// - [NotNull] - public readonly OsuHitObject Start; - - /// - /// Creates a new . - /// - /// The which s will exit from. - public FollowPointConnection([NotNull] OsuHitObject start) + protected override void PrepareForUse() { - Start = start; + base.PrepareForUse(); - RelativeSizeAxes = Axes.Both; + Entry.Invalidated += onEntryInvalidated; - StartTime.BindTo(start.StartTimeBindable); + refreshPoints(); } - protected override void LoadComplete() + protected override void FreeAfterUse() { - base.LoadComplete(); - bindEvents(Start); + base.FreeAfterUse(); + + Entry.Invalidated -= onEntryInvalidated; + + // Return points to the pool. + ClearInternal(false); + + Entry = null; } - private OsuHitObject end; + private void onEntryInvalidated() => refreshPoints(); - /// - /// The which s will enter. - /// - [CanBeNull] - public OsuHitObject End + private void refreshPoints() { - get => end; - set - { - end = value; + ClearInternal(false); - if (end != null) - bindEvents(end); + OsuHitObject start = Entry.Start; + OsuHitObject end = Entry.End; - if (IsLoaded) - scheduleRefresh(); - else - refresh(); - } - } + double startTime = start.GetEndTime(); - private void bindEvents(OsuHitObject obj) - { - obj.PositionBindable.BindValueChanged(_ => scheduleRefresh()); - obj.DefaultsApplied += _ => scheduleRefresh(); - } - - private void scheduleRefresh() - { - Scheduler.AddOnce(refresh); - } - - private void refresh() - { - double startTime = Start.GetEndTime(); - - LifetimeStart = startTime; - - if (End == null || End.NewCombo || Start is Spinner || End is Spinner) - { - // ensure we always set a lifetime for full LifetimeManagementContainer benefits - LifetimeEnd = LifetimeStart; - return; - } - - Vector2 startPosition = Start.StackedEndPosition; - Vector2 endPosition = End.StackedPosition; - double endTime = End.StartTime; + Vector2 startPosition = start.StackedEndPosition; + Vector2 endPosition = end.StackedPosition; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); - double duration = endTime - startTime; - double? firstTransformStartTime = null; double finalTransformEndTime = startTime; - int point = 0; - - ClearInternal(); - - for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) + for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING) { float fraction = (float)d / distance; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector; - double fadeOutTime = startTime + fraction * duration; - double fadeInTime = fadeOutTime - preempt; + + GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime); FollowPoint fp; - AddInternal(fp = new FollowPoint()); - - Debug.Assert(End != null); + AddInternal(fp = Pool.Get()); + fp.ClearTransforms(); fp.Position = pointStartPosition; fp.Rotation = rotation; fp.Alpha = 0; - fp.Scale = new Vector2(1.5f * End.Scale); - - firstTransformStartTime ??= fadeInTime; + fp.Scale = new Vector2(1.5f * end.Scale); fp.AnimationStartTime = fadeInTime; using (fp.BeginAbsoluteSequence(fadeInTime)) { - fp.FadeIn(End.TimeFadeIn); - fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); - fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); + fp.FadeIn(end.TimeFadeIn); + fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); + fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); - finalTransformEndTime = fadeOutTime + End.TimeFadeIn; + finalTransformEndTime = fadeOutTime + end.TimeFadeIn; } - - point++; } - int excessPoints = InternalChildren.Count - point; - for (int i = 0; i < excessPoints; i++) - RemoveInternal(InternalChildren[^1]); - // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. - LifetimeStart = firstTransformStartTime ?? startTime; - LifetimeEnd = finalTransformEndTime; + Entry.LifetimeEnd = finalTransformEndTime; + } + + /// + /// Computes the fade time of follow point positioned between two hitobjects. + /// + /// The first , where follow points should originate from. + /// The second , which follow points should target. + /// The fractional distance along and at which the follow point is to be located. + /// The fade-in time of the follow point/ + /// The fade-out time of the follow point. + public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime) + { + double startTime = start.GetEndTime(); + double duration = end.StartTime - startTime; + + fadeOutTime = startTime + fraction * duration; + fadeInTime = fadeOutTime - PREEMPT; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs new file mode 100644 index 0000000000..a167cb2f0f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections +{ + public class FollowPointLifetimeEntry : LifetimeEntry + { + public event Action Invalidated; + public readonly OsuHitObject Start; + + public FollowPointLifetimeEntry(OsuHitObject start) + { + Start = start; + LifetimeStart = Start.StartTime; + + bindEvents(); + } + + private OsuHitObject end; + + public OsuHitObject End + { + get => end; + set + { + UnbindEvents(); + + end = value; + + bindEvents(); + + refreshLifetimes(); + } + } + + private void bindEvents() + { + UnbindEvents(); + + // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. + Start.DefaultsApplied += onDefaultsApplied; + Start.PositionBindable.ValueChanged += onPositionChanged; + + if (End != null) + { + End.DefaultsApplied += onDefaultsApplied; + End.PositionBindable.ValueChanged += onPositionChanged; + } + } + + public void UnbindEvents() + { + if (Start != null) + { + Start.DefaultsApplied -= onDefaultsApplied; + Start.PositionBindable.ValueChanged -= onPositionChanged; + } + + if (End != null) + { + End.DefaultsApplied -= onDefaultsApplied; + End.PositionBindable.ValueChanged -= onPositionChanged; + } + } + + private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); + + private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes(); + + private void refreshLifetimes() + { + if (End == null || End.NewCombo || Start is Spinner || End is Spinner) + { + LifetimeEnd = LifetimeStart; + return; + } + + Vector2 startPosition = Start.StackedEndPosition; + Vector2 endPosition = End.StackedPosition; + Vector2 distanceVector = endPosition - startPosition; + + // The lifetime start will match the fade-in time of the first follow point. + float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; + FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _); + + LifetimeStart = fadeInTime; + LifetimeEnd = double.MaxValue; // This will be set by the connection. + + Invalidated?.Invoke(); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index be1392d7c3..3e85e528e8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -2,53 +2,74 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { /// /// Visualises connections between s. /// - public class FollowPointRenderer : LifetimeManagementContainer + public class FollowPointRenderer : CompositeDrawable { - /// - /// All the s contained by this . - /// - internal IReadOnlyList Connections => connections; - - private readonly List connections = new List(); - public override bool RemoveCompletedTransforms => false; - /// - /// Adds the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to add s for. - public void AddFollowPoints(OsuHitObject hitObject) - => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g)))); + public IReadOnlyList Entries => lifetimeEntries; - /// - /// Removes the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to remove s for. - public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject)); + private DrawablePool connectionPool; + private DrawablePool pointPool; - /// - /// Adds a to this . - /// - /// The to add. - /// The index of in . - private void addConnection(FollowPointConnection connection) + private readonly List lifetimeEntries = new List(); + private readonly Dictionary connectionsInUse = new Dictionary(); + private readonly Dictionary startTimeMap = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + + public FollowPointRenderer() { - // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections - int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => + lifetimeManager.EntryBecameAlive += onEntryBecameAlive; + lifetimeManager.EntryBecameDead += onEntryBecameDead; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] { - int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); + connectionPool = new DrawablePoolNoLifetime(1, 200), + pointPool = new DrawablePoolNoLifetime(50, 1000) + }; + } + + public void AddFollowPoints(OsuHitObject hitObject) + { + addEntry(hitObject); + + var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy(); + startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject); + startTimeMap[hitObject] = startTimeBindable; + } + + public void RemoveFollowPoints(OsuHitObject hitObject) + { + removeEntry(hitObject); + + startTimeMap[hitObject].UnbindAll(); + startTimeMap.Remove(hitObject); + } + + private void addEntry(OsuHitObject hitObject) + { + var newEntry = new FollowPointLifetimeEntry(hitObject); + + var index = lifetimeEntries.AddInPlace(newEntry, Comparer.Create((e1, e2) => + { + int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime); if (comp != 0) return comp; @@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections return -1; })); - if (index < connections.Count - 1) + if (index < lifetimeEntries.Count - 1) { // Update the connection's end point to the next connection's start point // h1 -> -> -> h2 // connection nextGroup - FollowPointConnection nextConnection = connections[index + 1]; - connection.End = nextConnection.Start; + FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1]; + newEntry.End = nextEntry.Start; } else { // The end point may be non-null during re-ordering - connection.End = null; + newEntry.End = null; } if (index > 0) @@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 // prevGroup connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.Start; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = newEntry.Start; } - AddInternal(connection); + lifetimeManager.AddEntry(newEntry); } - /// - /// Removes a from this . - /// - /// The to remove. - /// Whether was removed. - private void removeGroup(FollowPointConnection connection) + private void removeEntry(OsuHitObject hitObject) { - RemoveInternal(connection); + int index = lifetimeEntries.FindIndex(e => e.Start == hitObject); - int index = connections.IndexOf(connection); + var entry = lifetimeEntries[index]; + entry.UnbindEvents(); + + lifetimeEntries.RemoveAt(index); + lifetimeManager.RemoveEntry(entry); if (index > 0) { @@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 -> -> -> h3 // prevGroup connection nextGroup // The current connection's end point is used since there may not be a next connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.End; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = entry.End; } - - connections.Remove(connection); } - private void onStartTimeChanged(FollowPointConnection connection) + protected override bool CheckChildrenLife() { - // Naive but can be improved if performance becomes an issue - removeGroup(connection); - addConnection(connection); + bool anyAliveChanged = base.CheckChildrenLife(); + anyAliveChanged |= lifetimeManager.Update(Time.Current); + return anyAliveChanged; + } + + private void onEntryBecameAlive(LifetimeEntry entry) + { + var connection = connectionPool.Get(c => + { + c.Entry = (FollowPointLifetimeEntry)entry; + c.Pool = pointPool; + }); + + connectionsInUse[entry] = connection; + + AddInternal(connection); + } + + private void onEntryBecameDead(LifetimeEntry entry) + { + RemoveInternal(connectionsInUse[entry]); + connectionsInUse.Remove(entry); + } + + private void onStartTimeChanged(OsuHitObject hitObject) + { + removeEntry(hitObject); + addEntry(hitObject); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + foreach (var entry in lifetimeEntries) + entry.UnbindEvents(); + lifetimeEntries.Clear(); + } + + private class DrawablePoolNoLifetime : DrawablePool + where T : PoolableDrawable, new() + { + public override bool RemoveWhenNotAlive => false; + + public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index c455c66e8d..d0e1055dce 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class CirclePiece : CompositeDrawable { + [Resolved] + private DrawableHitObject drawableObject { get; set; } + + private TrianglesPiece triangles; + public CirclePiece() { Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(TextureStore textures, DrawableHitObject drawableHitObject) + private void load(TextureStore textures) { InternalChildren = new Drawable[] { @@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece(drawableHitObject.GetHashCode()) + triangles = new TrianglesPiece { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, Alpha = 0.5f, } }; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + if (obj.HitObject == null) + return; + + triangles.Reset((int)obj.HitObject.StartTime); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableObject != null) + drawableObject.HitObjectApplied -= onHitObjectApplied; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs index 6381ddca69..09299a3622 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs @@ -1,14 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ExplodePiece : Container { + [Resolved] + private DrawableHitObject drawableObject { get; set; } + + private TrianglesPiece triangles; + public ExplodePiece() { Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Blending = BlendingParameters.Additive; Alpha = 0; + } - Child = new TrianglesPiece + [BackgroundDependencyLoader] + private void load() + { + Child = triangles = new TrianglesPiece { Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, Alpha = 0.2f, }; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + if (obj.HitObject == null) + return; + + triangles.Reset((int)obj.HitObject.StartTime); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableObject != null) + drawableObject.HitObjectApplied -= onHitObjectApplied; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index 6cdb0d3df3..53dc7ecea3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class TrianglesPiece : Triangles { - protected override bool ExpireOffScreenTriangles => false; protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c816502d61..0e98a1d439 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -20,7 +19,6 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -40,44 +38,18 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly Bindable playfieldBorderStyle = new BindableBool(); - private readonly IDictionary> poolDictionary = new Dictionary>(); public OsuPlayfield() { InternalChildren = new Drawable[] { - playfieldBorder = new PlayfieldBorder - { - RelativeSizeAxes = Axes.Both, - Depth = 3 - }, - spinnerProxies = new ProxyContainer - { - RelativeSizeAxes = Axes.Both - }, - followPoints = new FollowPointRenderer - { - RelativeSizeAxes = Axes.Both, - Depth = 2, - }, - judgementLayer = new JudgementContainer - { - RelativeSizeAxes = Axes.Both, - Depth = 1, - }, - // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal - // Todo: Remove when hitobjects are properly pooled - new SkinProvidingContainer(null) - { - Child = HitObjectContainer, - }, - approachCircles = new ProxyContainer - { - RelativeSizeAxes = Axes.Both, - Depth = -1, - }, + playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, + spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, + followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, + judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, + HitObjectContainer, + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 5b0fa44444..0e9382279a 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -60,6 +60,7 @@ namespace osu.Game.Graphics.Backgrounds /// /// Whether we want to expire triangles as they exit our draw area completely. /// + [Obsolete("Unused.")] // Can be removed 20210518 protected virtual bool ExpireOffScreenTriangles => true; /// @@ -86,12 +87,9 @@ namespace osu.Game.Graphics.Backgrounds /// public float Velocity = 1; - private readonly Random stableRandom; - - private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); - private readonly SortedList parts = new SortedList(Comparer.Default); + private Random stableRandom; private IShader shader; private readonly Texture texture; @@ -172,7 +170,20 @@ namespace osu.Game.Graphics.Backgrounds } } - protected int AimCount; + /// + /// Clears and re-initialises triangles according to a given seed. + /// + /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time. + public void Reset(int? seed = null) + { + if (seed != null) + stableRandom = new Random(seed.Value); + + parts.Clear(); + addTriangles(true); + } + + protected int AimCount { get; private set; } private void addTriangles(bool randomY) { @@ -226,6 +237,8 @@ namespace osu.Game.Graphics.Backgrounds } } + private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); + protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this); private class TrianglesDrawNode : DrawNode diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index d9948aa23c..46d5eb40b4 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Compose composer = ruleset?.CreateHitObjectComposer(); // make the composer available to the timeline and other components in this screen. - dependencies.CacheAs(composer); + if (composer != null) + dependencies.CacheAs(composer); return dependencies; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index f8bdf0140c..ce3e618889 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; @@ -149,7 +150,12 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - ScorePanelList.AddScore(Score, true); + { + // only show flair / animation when arriving after watching a play that isn't autoplay. + bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay); + + ScorePanelList.AddScore(Score, shouldFlair); + } if (player != null && allowRetry) { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 704ac5a611..54f3fcede6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 346bd892b0..692dac909a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +