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 @@
-
+