diff --git a/osu.Android.props b/osu.Android.props
index 6e3d5eec1f..6dab6edc5e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b17611f23f..0feab9a717 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -139,7 +139,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
- desktopWindow.CursorState.Value |= CursorState.Hidden;
+ desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 385d8ed7fa..89063319d6 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -18,71 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests
base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
+ AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
+ AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
- private Drawable createDrawableTinyDroplet()
+ private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
+ setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
+
+ private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
+
+ private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
+
+ private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
{
- var droplet = new TestCatchTinyDroplet
- {
- Scale = 1.5f,
- };
+ var hitObject = d.HitObject;
+ hitObject.StartTime = 1000000000000;
+ hitObject.Scale = 1.5f;
- return new DrawableTinyDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
+ if (hyperdash)
+ hitObject.HyperDashTarget = new Banana();
- private Drawable createDrawableDroplet(bool hyperdash = false)
- {
- var droplet = new TestCatchDroplet
+ d.Anchor = Anchor.Centre;
+ d.RelativePositionAxes = Axes.None;
+ d.Position = Vector2.Zero;
+ d.HitObjectApplied += _ =>
{
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
- {
- Fruit fruit = new TestCatchFruit(rep)
- {
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableFruit(fruit)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
+ d.LifetimeStart = double.NegativeInfinity;
+ d.LifetimeEnd = double.PositiveInfinity;
};
+ return d;
}
public class TestCatchFruit : Fruit
@@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
- StartTime = 1000000000000;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
-
- public class TestCatchDroplet : Droplet
- {
- public TestCatchDroplet()
- {
- StartTime = 1000000000000;
- }
- }
-
- public class TestCatchTinyDroplet : TinyDroplet
- {
- public TestCatchTinyDroplet()
- {
- StartTime = 1000000000000;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index d24c81dac6..96444fd316 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -1,7 +1,7 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
///
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
SetContents(() => new FillFlowContainer
{
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
},
}
});
- }
+ });
protected abstract DrawableManiaHitObject CreateHitObject();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index a4d4ec50f8..dcb25f21ba 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (hitWindows.IsHitResultAllowed(result))
{
AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
+ {
+ Type = result
+ }, null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
index 9c4c2b3d5b..e88ff8e2ac 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
@@ -1,6 +1,7 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
@@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
});
}
+ [Test]
+ public void TestFadeOnMiss()
+ {
+ AddStep("miss tick", () =>
+ {
+ foreach (var holdNote in holdNotes)
+ holdNote.ChildrenOfType().First().MissForcefully();
+ });
+ }
+
+ private IEnumerable holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType());
+
protected override DrawableManiaHitObject CreateHitObject()
{
var note = new HoldNote { Duration = 1000 };
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 59899637f9..a64cc6dc67 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
///
- /// Whether the hold note has been released too early and shouldn't give full score for the release.
+ /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
///
- public bool HasBroken { get; private set; }
+ public double? HoldBrokenTime { get; private set; }
///
/// Whether the hold note has been released potentially without having caused a break.
@@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
if (Tail.Judged && !Tail.IsHit)
- HasBroken = true;
+ HoldBrokenTime = Time.Current;
}
public bool OnPressed(ManiaAction action)
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
- HasBroken = true;
+ HoldBrokenTime = Time.Current;
releaseTime = Time.Current;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index c780c0836e..a4029e7893 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
- if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
+ if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
result = HitResult.Meh;
r.Type = result;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index f265419aa0..98931dceed 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteTick : DrawableManiaHitObject
{
- public override bool DisplayResult => false;
-
///
/// References the time at which the user started holding the hold note.
///
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index c0f0fcb4af..8902d82f33 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyBodyPiece : LegacyManiaColumnElement
{
+ private DrawableHoldNote holdNote;
+
private readonly IBindable direction = new Bindable();
private readonly IBindable isHitting = new Bindable();
+ ///
+ /// Stores the start time of the fade animation that plays when any of the nested
+ /// hitobjects of the hold note are missed.
+ ///
+ private readonly Bindable missFadeTime = new Bindable();
+
[CanBeNull]
private Drawable bodySprite;
@@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{
+ holdNote = (DrawableHoldNote)drawableObject;
+
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
@@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(onDirectionChanged, true);
-
- var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHitting);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true);
+ missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
+
+ holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
+ applyCustomUpdateState(holdNote, holdNote.State.Value);
+ }
+
+ private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
+ {
+ // ensure that the hold note is also faded out when the head/tail/any tick is missed.
+ if (state == ArmedState.Miss)
+ missFadeTime.Value ??= hitObject.HitStateUpdateTime;
}
private void onIsHittingChanged(ValueChangedEvent isHitting)
@@ -158,10 +183,38 @@ namespace osu.Game.Rulesets.Mania.Skinning
}
}
+ private void onMissFadeTimeChanged(ValueChangedEvent missFadeTimeChange)
+ {
+ if (missFadeTimeChange.NewValue == null)
+ return;
+
+ // this update could come from any nested object of the hold note (or even from an input).
+ // make sure the transforms are consistent across all affected parts.
+ using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
+ {
+ // colour and duration matches stable
+ // transforms not applied to entire hold note in order to not affect hit lighting
+ const double fade_duration = 60;
+
+ holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
+ holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
+ bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ missFadeTime.Value ??= holdNote.HoldBrokenTime;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ if (holdNote != null)
+ holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
+
lightContainer?.Expire();
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index c28a1c13d8..9aabcc6699 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
- if (!result.IsHit || !DisplayJudgements.Value)
+ if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index d99f6cb8d3..a3dcd0e57f 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -1,10 +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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -19,22 +20,52 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
- [BackgroundDependencyLoader]
- private void load()
+ protected override void ApplyMissAnimations()
{
- if (JudgementText != null)
- JudgementText.Font = JudgementText.Font.With(size: 25);
- }
+ if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
+ {
+ // this is temporary logic until mania's skin transformer returns IAnimatableJudgements
+ JudgementBody.ScaleTo(1.6f);
+ JudgementBody.ScaleTo(1, 100, Easing.In);
- protected override double FadeInDuration => 50;
+ JudgementBody.MoveTo(Vector2.Zero);
+ JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ JudgementBody.RotateTo(0);
+ JudgementBody.RotateTo(40, 800, Easing.InQuint);
+ JudgementBody.FadeOutFromOne(800);
+
+ LifetimeEnd = JudgementBody.LatestTransformEndTime;
+ }
+
+ base.ApplyMissAnimations();
+ }
protected override void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
- JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
- this.Delay(FadeInDuration).FadeOut(200);
+ JudgementBody.Delay(50)
+ .ScaleTo(0.75f, 250)
+ .FadeOut(200);
+ }
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
+
+ private class DefaultManiaJudgementPiece : DefaultJudgementPiece
+ {
+ public DefaultManiaJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ JudgementText.Font = JudgementText.Font.With(size: 25);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index e7a2de266d..3d7960ffe3 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
+ // Tick judgements should not display text.
+ if (judgedObject is DrawableHoldNoteTick)
+ return;
+
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 646f12f710..e4158d8f07 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1));
- AddAssert("hit lighting hidden",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
+ AddAssert("hit lighting has no transforms", () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any()));
+ AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
}
[Test]
@@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body not immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
- AddAssert("hit lighting shown",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0));
+ AddUntilStep("hit lighting shown", () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Alpha > 0));
}
private void showResult(HitResult result)
@@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Children = new Drawable[]
{
pool,
- pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
+ pool.Get(j => j.Apply(new JudgementResult(new HitObject
+ {
+ StartTime = Time.Current
+ }, new Judgement())
+ {
+ Type = result,
+ }, null)).With(j =>
{
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestDrawableOsuJudgement : DrawableOsuJudgement
{
public new SkinnableSprite Lighting => base.Lighting;
- public new Container JudgementBody => base.JudgementBody;
+ public new SkinnableDrawable JudgementBody => base.JudgementBody;
}
}
}
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/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
index ba1d35c35c..eb7011e8b0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Path path;
private readonly Slider slider;
- private readonly int controlPointIndex;
+ public int ControlPointIndex { get; set; }
private IBindable sliderPosition;
private IBindable pathVersion;
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
- this.controlPointIndex = controlPointIndex;
+ ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
- int nextIndex = controlPointIndex + 1;
+ int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;
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 17541866ec..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;
@@ -66,6 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ // If inserting in the path (not appending),
+ // update indices of existing connections after insert location
+ if (e.NewStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.NewStartingIndex)
+ connection.ControlPointIndex += e.NewItems.Count;
+ }
+ }
+
for (int i = 0; i < e.NewItems.Count; i++)
{
var point = (PathControlPoint)e.NewItems[i];
@@ -88,6 +102,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Connections.RemoveAll(c => c.ControlPoint == point);
}
+ // If removing before the end of the path,
+ // update indices of connections after remove location
+ if (e.OldStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.OldStartingIndex)
+ connection.ControlPointIndex -= e.OldItems.Count;
+ }
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 7ae4f387ca..d592e129d9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
+ private readonly BindableList controlPoints = new BindableList();
+ private readonly IBindable pathVersion = new Bindable();
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
}
- private IBindable pathVersion;
-
protected override void LoadComplete()
{
base.LoadComplete();
- pathVersion = HitObject.Path.Version.GetBoundCopy();
+ controlPoints.BindTo(HitObject.Path.ControlPoints);
+
+ pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
@@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
- private BindableList controlPoints => HitObject.Path.ControlPoints;
-
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
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/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 2e63160d36..d1ceca6d8f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.Delay(800).FadeOut();
break;
}
+
+ Expire();
}
public Drawable ProxiedLayer => ApproachCircle;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index bcaf73d34f..a26db06ede 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindTo(HitObject.PositionBindable);
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
+
+ // Manually set to reduce the number of future alive objects to a bare minimum.
+ LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
+
+ // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
+ // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
+ LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
}
protected override void OnFree(HitObject hitObject)
@@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // Manually set to reduce the number of future alive objects to a bare minimum.
- LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
- }
-
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 98898ce1b4..13f5960bd4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -4,9 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
-using osuTK;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved]
private OsuConfigManager config { get; set; }
- public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
- : base(result, judgedObject)
- {
- }
-
- public DrawableOsuJudgement()
- {
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -39,48 +30,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
- public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
- {
- base.Apply(result, judgedObject);
-
- if (judgedObject?.HitObject is OsuHitObject osuObject)
- {
- Position = osuObject.StackedPosition;
- Scale = new Vector2(osuObject.Scale);
- }
- }
-
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
- }
- private double fadeOutDelay;
- protected override double FadeOutDelay => fadeOutDelay;
+ if (JudgedObject?.HitObject is OsuHitObject osuObject)
+ {
+ Position = osuObject.StackedPosition;
+ Scale = new Vector2(osuObject.Scale);
+ }
+ }
protected override void ApplyHitAnimations()
{
bool hitLightingEnabled = config.Get(OsuSetting.HitLighting);
- if (hitLightingEnabled)
- {
- JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
+ Lighting.Alpha = 0;
+ if (hitLightingEnabled && Lighting.Drawable != null)
+ {
+ // todo: this animation changes slightly based on new/old legacy skin versions.
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
- }
- else
- {
- JudgementBody.Alpha = 1;
+
+ // extend the lifetime to cover lighting fade
+ LifetimeEnd = Lighting.LatestTransformEndTime;
}
- fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
-
- JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
}
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
+
+ private class OsuJudgementPiece : DefaultJudgementPiece
+ {
+ public OsuJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ public override void PlayAnimation()
+ {
+ base.PlayAnimation();
+
+ if (Result != HitResult.Miss)
+ JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index f7b1894058..14c494d909 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -193,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- Body.FadeInFromZero(HitObject.TimeFadeIn);
- }
-
public readonly Bindable Tracking = new Bindable();
protected override void Update()
@@ -273,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples();
}
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
@@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
- this.FadeOut(fade_out_time, Easing.OutQuint);
+ this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 87c7146a64..2a14a7c975 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- this.FadeOut(160);
+ this.FadeOut(160).Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();
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..3bd150c4d3 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,21 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
- private readonly Bindable playfieldBorderStyle = new BindableBool();
-
private readonly IDictionary> poolDictionary = new Dictionary>();
+ private readonly Container judgementAboveHitObjectLayer;
+
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,
+ judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
+ approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
@@ -86,13 +61,18 @@ namespace osu.Game.Rulesets.Osu.UI
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
- poolDictionary.Add(result, new DrawableJudgementPool(result));
+ poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
AddRangeInternal(poolDictionary.Values);
NewResult += onNewResult;
}
+ private void onJudgmentLoaded(DrawableOsuJudgement judgement)
+ {
+ judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
+ }
+
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
@@ -178,11 +158,13 @@ namespace osu.Game.Rulesets.Osu.UI
private class DrawableJudgementPool : DrawablePool
{
private readonly HitResult result;
+ private readonly Action onLoaded;
- public DrawableJudgementPool(HitResult result)
+ public DrawableJudgementPool(HitResult result, Action onLoaded)
: base(10)
{
this.result = result;
+ this.onLoaded = onLoaded;
}
protected override DrawableOsuJudgement CreateNewDrawable()
@@ -192,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.UI
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
+ onLoaded?.Invoke(judgement);
+
return judgement;
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index cbfc5a8628..b5e35f88b5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Judgements;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- switch (Result.Type)
- {
- case HitResult.Ok:
- JudgementBody.Colour = colours.GreenLight;
- break;
-
- case HitResult.Great:
- JudgementBody.Colour = colours.BlueLight;
- break;
- }
- }
-
protected override void ApplyHitAnimations()
{
this.MoveToY(-100, 500);
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 9ef9649f77..5323f58a66 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background
private class FadeAccessibleResults : ResultsScreen
{
public FadeAccessibleResults(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
new file mode 100644
index 0000000000..82095cb809
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [TestFixture]
+ public class TestSceneParticleExplosion : OsuTestScene
+ {
+ private ParticleExplosion explosion;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddStep("create initial", () =>
+ {
+ Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(400)
+ };
+ });
+
+ AddWaitStep("wait for playback", 5);
+
+ AddRepeatStep(@"restart animation", () =>
+ {
+ explosion.Restart();
+ }, 10);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index ff96a999ec..b2be7cdf88 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
}
@@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public UnrankedSoloResultsScreen(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
Score.Beatmap.OnlineBeatmapID = 0;
Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
similarity index 95%
rename from osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
index 2e9f919cfd..cd226662d7 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
@@ -11,12 +11,12 @@ using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestScenePaginatedContainerHeader : OsuTestScene
+ public class TestSceneProfileSubsectionHeader : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- private PaginatedContainerHeader header;
+ private ProfileSubsectionHeader header;
[Test]
public void TestHiddenCounter()
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
{
Clear();
- Add(header = new PaginatedContainerHeader(text, state)
+ Add(header = new ProfileSubsectionHeader(text, state)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index c85ad6d651..05d6930992 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -1,7 +1,6 @@
// 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 System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -15,103 +14,93 @@ namespace osu.Game.Database
{
public class UserLookupCache : MemoryCachingComponent
{
- private readonly HashSet nextTaskIDs = new HashSet();
-
[Resolved]
private IAPIProvider api { get; set; }
- private readonly object taskAssignmentLock = new object();
-
- private Task> pendingRequest;
-
- ///
- /// Whether has already grabbed its IDs.
- ///
- private bool pendingRequestConsumedIDs;
-
public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
- {
- var users = await getQueryTaskForUser(lookup);
- return users.FirstOrDefault(u => u.Id == lookup);
- }
+ => await queryUser(lookup);
- ///
- /// Return the task responsible for fetching the provided user.
- /// This may be part of a larger batch lookup to reduce web requests.
- ///
- /// The user to lookup.
- /// The task responsible for the lookup.
- private Task> getQueryTaskForUser(int userId)
+ private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>();
+ private Task pendingRequestTask;
+ private readonly object taskAssignmentLock = new object();
+
+ private Task queryUser(int userId)
{
lock (taskAssignmentLock)
{
- nextTaskIDs.Add(userId);
+ var tcs = new TaskCompletionSource();
- // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it.
- if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50)
- return pendingRequest;
+ // Add to the queue.
+ pendingUserTasks.Enqueue((userId, tcs));
- return queueNextTask(nextLookup);
+ // Create a request task if there's not already one.
+ if (pendingRequestTask == null)
+ createNewTask();
+
+ return tcs.Task;
}
+ }
- List nextLookup()
+ private void performLookup()
+ {
+ // contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
+ var userTasks = new Dictionary>>();
+
+ // Grab at most 50 unique user IDs from the queue.
+ lock (taskAssignmentLock)
{
- int[] lookupItems;
-
- lock (taskAssignmentLock)
+ while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
{
- pendingRequestConsumedIDs = true;
- lookupItems = nextTaskIDs.ToArray();
- nextTaskIDs.Clear();
+ (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue();
- if (lookupItems.Length == 0)
+ // Perform a secondary check for existence, in case the user was queried in a previous batch.
+ if (CheckExists(next.id, out var existing))
+ next.task.SetResult(existing);
+ else
{
- queueNextTask(null);
- return new List();
+ if (userTasks.TryGetValue(next.id, out var tasks))
+ tasks.Add(next.task);
+ else
+ userTasks[next.id] = new List> { next.task };
}
}
-
- var request = new GetUsersRequest(lookupItems);
-
- // rather than queueing, we maintain our own single-threaded request stream.
- api.Perform(request);
-
- return request.Result?.Users;
}
- }
- ///
- /// Queues new work at the end of the current work tasks.
- /// Ensures the provided work is eventually run.
- ///
- /// The work to run. Can be null to signify the end of available work.
- /// The task tracking this work.
- private Task> queueNextTask(Func> work)
- {
+ // Query the users.
+ var request = new GetUsersRequest(userTasks.Keys.ToArray());
+
+ // rather than queueing, we maintain our own single-threaded request stream.
+ api.Perform(request);
+
+ // Create a new request task if there's still more users to query.
lock (taskAssignmentLock)
{
- if (work == null)
- {
- pendingRequest = null;
- pendingRequestConsumedIDs = false;
- }
- else if (pendingRequest == null)
- {
- // special case for the first request ever.
- pendingRequest = Task.Run(work);
- pendingRequestConsumedIDs = false;
- }
- else
- {
- // append the new request on to the last to be executed.
- pendingRequest = pendingRequest.ContinueWith(_ => work());
- pendingRequestConsumedIDs = false;
- }
+ pendingRequestTask = null;
+ if (pendingUserTasks.Count > 0)
+ createNewTask();
+ }
- return pendingRequest;
+ foreach (var user in request.Result.Users)
+ {
+ if (userTasks.TryGetValue(user.Id, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(user);
+
+ userTasks.Remove(user.Id);
+ }
+ }
+
+ // if any tasks remain which were not satisfied, return null.
+ foreach (var tasks in userTasks.Values)
+ {
+ foreach (var task in tasks)
+ task.SetResult(null);
}
}
+
+ private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
}
}
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/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs
new file mode 100644
index 0000000000..e0d2b50c55
--- /dev/null
+++ b/osu.Game/Graphics/ParticleExplosion.cs
@@ -0,0 +1,144 @@
+// 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 System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
+
+namespace osu.Game.Graphics
+{
+ ///
+ /// An explosion of textured particles based on how osu-stable randomises the explosion pattern.
+ ///
+ public class ParticleExplosion : Sprite
+ {
+ private readonly int particleCount;
+ private readonly double duration;
+ private double startTime;
+
+ private readonly List parts = new List();
+
+ public ParticleExplosion(Texture texture, int particleCount, double duration)
+ {
+ Texture = texture;
+ this.particleCount = particleCount;
+ this.duration = duration;
+ Blending = BlendingParameters.Additive;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Restart();
+ }
+
+ ///
+ /// Restart the animation from the current point in time.
+ /// Supports transform time offset chaining.
+ ///
+ public void Restart()
+ {
+ startTime = TransformStartTime;
+ this.FadeOutFromOne(duration);
+
+ parts.Clear();
+ for (int i = 0; i < particleCount; i++)
+ parts.Add(new ParticlePart(duration));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this);
+
+ private class ParticleExplosionDrawNode : SpriteDrawNode
+ {
+ private readonly List parts = new List();
+
+ private ParticleExplosion source => (ParticleExplosion)Source;
+
+ private double startTime;
+ private double currentTime;
+ private Vector2 sourceSize;
+
+ public ParticleExplosionDrawNode(Sprite source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ parts.Clear();
+ parts.AddRange(source.parts);
+
+ sourceSize = source.Size;
+ startTime = source.startTime;
+ currentTime = source.Time.Current;
+ }
+
+ protected override void Blit(Action vertexAction)
+ {
+ var time = currentTime - startTime;
+
+ foreach (var p in parts)
+ {
+ Vector2 pos = p.PositionAtTime(time);
+ float alpha = p.AlphaAtTime(time);
+
+ var rect = new RectangleF(
+ pos.X * sourceSize.X - Texture.DisplayWidth / 2,
+ pos.Y * sourceSize.Y - Texture.DisplayHeight / 2,
+ Texture.DisplayWidth,
+ Texture.DisplayHeight);
+
+ // convert to screen space.
+ var quad = new Quad(
+ Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
+ );
+
+ DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
+ new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
+ null, TextureCoords);
+ }
+ }
+ }
+
+ private readonly struct ParticlePart
+ {
+ private readonly double duration;
+ private readonly float direction;
+ private readonly float distance;
+
+ public ParticlePart(double availableDuration)
+ {
+ distance = RNG.NextSingle(0.5f);
+ duration = RNG.NextDouble(availableDuration / 3, availableDuration);
+ direction = RNG.NextSingle(0, MathF.PI * 2);
+ }
+
+ public float AlphaAtTime(double time) => 1 - progressAtTime(time);
+
+ public Vector2 PositionAtTime(double time)
+ {
+ var travelledDistance = distance * progressAtTime(time);
+ return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction));
+ }
+
+ private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1);
+ }
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index cb0e2cfa8e..bb638bcf3a 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -420,7 +420,7 @@ namespace osu.Game
break;
case ScorePresentType.Results:
- screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo));
+ screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
}, validScreens: new[] { typeof(PlaySongSelect) });
diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index 4b7de8de90..780d7ea986 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{
- public class PaginatedBeatmapContainer : PaginatedContainer
+ public class PaginatedBeatmapContainer : PaginatedProfileSubsection
{
private const float panel_padding = 10f;
private readonly BeatmapSetType type;
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index 556f3139dd..e5bb1f8008 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -13,7 +13,7 @@ using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
- public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer
+ public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection
{
public PaginatedMostPlayedBeatmapContainer(Bindable user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
index 1b8bd23eb4..008d89d881 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
- public class PaginatedKudosuHistoryContainer : PaginatedContainer
+ public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection
{
public PaginatedKudosuHistoryContainer(Bindable user)
: base(user, missingText: "This user hasn't received any kudosu!")
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
similarity index 72%
rename from osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
rename to osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
index c1107ce907..51e5622f68 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
@@ -6,62 +6,51 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
-using osu.Game.Rulesets;
using osu.Game.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
- public abstract class PaginatedContainer : FillFlowContainer
+ public abstract class PaginatedProfileSubsection : ProfileSubsection
{
[Resolved]
private IAPIProvider api { get; set; }
+ [Resolved]
+ protected RulesetStore Rulesets { get; private set; }
+
protected int VisiblePages;
protected int ItemsPerPage;
- protected readonly Bindable User = new Bindable();
- protected FillFlowContainer ItemsContainer;
- protected RulesetStore Rulesets;
+ protected FillFlowContainer ItemsContainer { get; private set; }
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
- private readonly string missingText;
private ShowMoreButton moreButton;
private OsuSpriteText missing;
- private PaginatedContainerHeader header;
+ private readonly string missingText;
- private readonly string headerText;
- private readonly CounterVisibilityState counterVisibilityState;
-
- protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ : base(user, headerText, counterVisibilityState)
{
- this.headerText = headerText;
this.missingText = missingText;
- this.counterVisibilityState = counterVisibilityState;
- User.BindTo(user);
}
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
+ protected override Drawable CreateContent() => new FillFlowContainer
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- Direction = FillDirection.Vertical;
-
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- header = new PaginatedContainerHeader(headerText, counterVisibilityState)
- {
- Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
- },
ItemsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections
Font = OsuFont.GetFont(size: 15),
Text = missingText,
Alpha = 0,
- },
- };
+ }
+ }
+ };
- Rulesets = rulesets;
-
- User.ValueChanged += onUserChanged;
- User.TriggerChange();
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent e)
@@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide();
moreButton.IsLoading = false;
- if (!string.IsNullOrEmpty(missing.Text))
+ if (!string.IsNullOrEmpty(missingText))
missing.Show();
return;
@@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections
protected virtual int GetCount(User user) => 0;
- protected void SetCount(int value) => header.Current.Value = value;
-
protected virtual void OnItemsReceived(List items)
{
}
@@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections
protected override void Dispose(bool isDisposing)
{
- base.Dispose(isDisposing);
retrievalRequest?.Cancel();
+ loadCancellation?.Cancel();
+ base.Dispose(isDisposing);
}
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
new file mode 100644
index 0000000000..3e331f85e9
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
@@ -0,0 +1,51 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Users;
+using JetBrains.Annotations;
+
+namespace osu.Game.Overlays.Profile.Sections
+{
+ public abstract class ProfileSubsection : FillFlowContainer
+ {
+ protected readonly Bindable User = new Bindable();
+
+ private readonly string headerText;
+ private readonly CounterVisibilityState counterVisibilityState;
+
+ private ProfileSubsectionHeader header;
+
+ protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ {
+ this.headerText = headerText;
+ this.counterVisibilityState = counterVisibilityState;
+ User.BindTo(user);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Direction = FillDirection.Vertical;
+
+ Children = new[]
+ {
+ header = new ProfileSubsectionHeader(headerText, counterVisibilityState)
+ {
+ Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
+ },
+ CreateContent()
+ };
+ }
+
+ [NotNull]
+ protected abstract Drawable CreateContent();
+
+ protected void SetCount(int value) => header.Current.Value = value;
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
similarity index 95%
rename from osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs
rename to osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
index 8c617e5fbd..5858cebe89 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs
+++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
@@ -14,7 +14,7 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
- public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue
+ public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue
{
private readonly BindableWithCurrent current = new BindableWithCurrent();
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections
private CounterPill counterPill;
- public PaginatedContainerHeader(string text, CounterVisibilityState counterState)
+ public ProfileSubsectionHeader(string text, CounterVisibilityState counterState)
{
this.text = text;
this.counterState = counterState;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 1ce3079d52..53f6d375ca 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -14,7 +14,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
- public class PaginatedScoreContainer : PaginatedContainer
+ public class PaginatedScoreContainer : PaginatedProfileSubsection
{
private readonly ScoreType type;
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
index 08f39c6272..d7101a8147 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
@@ -13,7 +13,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
- public class PaginatedRecentActivityContainer : PaginatedContainer
+ public class PaginatedRecentActivityContainer : PaginatedProfileSubsection
{
public PaginatedRecentActivityContainer(Bindable user)
: base(user, missingText: "This user hasn't done anything notable recently!")
diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
new file mode 100644
index 0000000000..d94346cb72
--- /dev/null
+++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
@@ -0,0 +1,75 @@
+// 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.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Judgements
+{
+ public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public DefaultJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Font = OsuFont.Numeric.With(size: 20),
+ Scale = new Vector2(0.85f, 1),
+ }
+ };
+ }
+
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+
+ break;
+
+ default:
+ this.ScaleTo(0.9f);
+ this.ScaleTo(1, 500, Easing.OutElastic);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => null;
+ }
+}
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index d24c81536e..3063656aaf 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -1,20 +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 System;
using System.Diagnostics;
using JetBrains.Annotations;
-using osuTK;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Judgements
{
@@ -25,25 +22,29 @@ namespace osu.Game.Rulesets.Judgements
{
private const float judgement_size = 128;
- [Resolved]
- private OsuColour colours { get; set; }
-
public JudgementResult Result { get; private set; }
+
public DrawableHitObject JudgedObject { get; private set; }
- protected Container JudgementBody { get; private set; }
- protected SpriteText JudgementText { get; private set; }
+ public override bool RemoveCompletedTransforms => false;
- private SkinnableDrawable bodyDrawable;
+ protected SkinnableDrawable JudgementBody { get; private set; }
+
+ private readonly Container aboveHitObjectsContent;
+
+ [Resolved]
+ private ISkinSource skinSource { get; set; }
///
/// Duration of initial fade in.
///
+ [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeInDuration => 100;
///
/// Duration to wait until fade out begins. Defaults to .
///
+ [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeOutDelay => FadeInDuration;
///
@@ -61,6 +62,12 @@ namespace osu.Game.Rulesets.Judgements
{
Size = new Vector2(judgement_size);
Origin = Anchor.Centre;
+
+ AddInternal(aboveHitObjectsContent = new Container
+ {
+ Depth = float.MinValue,
+ RelativeSizeAxes = Axes.Both
+ });
}
[BackgroundDependencyLoader]
@@ -69,15 +76,61 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
}
- protected virtual void ApplyHitAnimations()
- {
- JudgementBody.ScaleTo(0.9f);
- JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
+ public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
- this.Delay(FadeOutDelay).FadeOut(400);
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ skinSource.SourceChanged += onSkinChanged;
}
- public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
+ private void onSkinChanged()
+ {
+ // on a skin change, the child component will update but not get correctly triggered to play its animation.
+ // we need to trigger a reinitialisation to make things right.
+ currentDrawableType = null;
+
+ PrepareForUse();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (skinSource != null)
+ skinSource.SourceChanged -= onSkinChanged;
+ }
+
+ ///
+ /// Apply top-level animations to the current judgement when successfully hit.
+ /// If displaying components which require lifetime extensions, manually adjusting is required.
+ ///
+ ///
+ /// For animating the actual "default skin" judgement itself, it is recommended to use .
+ /// This allows applying animations which don't affect custom skins.
+ ///
+ protected virtual void ApplyHitAnimations()
+ {
+ }
+
+ ///
+ /// Apply top-level animations to the current judgement when missed.
+ /// If displaying components which require lifetime extensions, manually adjusting is required.
+ ///
+ ///
+ /// For animating the actual "default skin" judgement itself, it is recommended to use .
+ /// This allows applying animations which don't affect custom skins.
+ ///
+ protected virtual void ApplyMissAnimations()
+ {
+ }
+
+ ///
+ /// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool.
+ ///
+ /// The applicable judgement.
+ /// The drawable object.
+ public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
{
Result = result;
JudgedObject = judgedObject;
@@ -91,34 +144,46 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
- bodyDrawable.ResetAnimation();
+ runAnimation();
+ }
- this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
- JudgementBody.ScaleTo(1);
- JudgementBody.RotateTo(0);
- JudgementBody.MoveTo(Vector2.Zero);
+ private void runAnimation()
+ {
+ ClearTransforms(true);
+ LifetimeStart = Result.TimeAbsolute;
- switch (Result.Type)
+ using (BeginAbsoluteSequence(Result.TimeAbsolute, true))
{
- case HitResult.None:
- break;
+ // not sure if this should remain going forward.
+ JudgementBody.ResetAnimation();
- case HitResult.Miss:
- JudgementBody.ScaleTo(1.6f);
- JudgementBody.ScaleTo(1, 100, Easing.In);
+ switch (Result.Type)
+ {
+ case HitResult.None:
+ break;
- JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
- JudgementBody.RotateTo(40, 800, Easing.InQuint);
+ case HitResult.Miss:
+ ApplyMissAnimations();
+ break;
- this.Delay(600).FadeOut(200);
- break;
+ default:
+ ApplyHitAnimations();
+ break;
+ }
- default:
- ApplyHitAnimations();
- break;
+ if (JudgementBody.Drawable is IAnimatableJudgement animatable)
+ {
+ var drawableAnimation = (Drawable)animatable;
+
+ animatable.PlayAnimation();
+
+ // a derived version of DrawableJudgement may be proposing a lifetime.
+ // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
+ double lastTransformTime = drawableAnimation.LatestTransformEndTime;
+ if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
+ LifetimeEnd = lastTransformTime;
+ }
}
-
- Expire(true);
}
private HitResult? currentDrawableType;
@@ -127,6 +192,7 @@ namespace osu.Game.Rulesets.Judgements
{
var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset
+ // todo: this should be removed once judgements are always pooled.
if (type == currentDrawableType)
return;
@@ -134,21 +200,24 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null)
RemoveInternal(JudgementBody);
- AddInternal(JudgementBody = new Container
+ aboveHitObjectsContent.Clear();
+ AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ =>
+ CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText
- {
- Text = type.GetDescription().ToUpperInvariant(),
- Font = OsuFont.Numeric.With(size: 20),
- Colour = colours.ForHitResult(type),
- Scale = new Vector2(0.85f, 1),
- }, confineMode: ConfineMode.NoScaling)
});
+ if (JudgementBody.Drawable is IAnimatableJudgement animatable)
+ {
+ var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
+ if (proxiedContent != null)
+ aboveHitObjectsContent.Add(proxiedContent);
+ }
+
currentDrawableType = type;
}
+
+ protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);
}
}
diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
new file mode 100644
index 0000000000..b38b83b534
--- /dev/null
+++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Judgements
+{
+ ///
+ /// A skinnable judgement element which supports playing an animation from the current point in time.
+ ///
+ public interface IAnimatableJudgement : IDrawable
+ {
+ ///
+ /// Start the animation for this judgement from the current point in time.
+ ///
+ void PlayAnimation();
+
+ ///
+ /// Get proxied content which should be displayed above all hitobjects.
+ ///
+ [CanBeNull]
+ Drawable GetAboveHitObjectsProxiedContent();
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
index d8c6da86f9..ba38c7f77d 100644
--- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
+++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
@@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
- [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
- // ReSharper disable once RedundantOverload.Global
- public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
- double? legacyLastTickOffset)
- {
- return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
- }
-
// ReSharper disable once MethodOverloadWithOptionalParameter
public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default)
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 53b6e14940..df9cadebfc 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
- protected virtual Container CreateSelectionBlueprintContainer() =>
- new Container { RelativeSizeAxes = Axes.Both };
+ protected virtual Container CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
///
/// Creates a which outlines s and handles movement of selections.
@@ -338,7 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Whether a selection was performed.
private bool beginClickSelection(MouseButtonEvent e)
{
- foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
+ // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
+ foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
{
if (!blueprint.IsHovered) continue;
diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
new file mode 100644
index 0000000000..9e95fe4fa1
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
@@ -0,0 +1,77 @@
+// 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 osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Edit.Compose.Components
+{
+ ///
+ /// A container for ordered by their start times.
+ ///
+ public sealed class HitObjectOrderedSelectionContainer : Container
+ {
+ public override void Add(SelectionBlueprint drawable)
+ {
+ base.Add(drawable);
+ bindStartTime(drawable);
+ }
+
+ public override bool Remove(SelectionBlueprint drawable)
+ {
+ if (!base.Remove(drawable))
+ return false;
+
+ unbindStartTime(drawable);
+ return true;
+ }
+
+ public override void Clear(bool disposeChildren)
+ {
+ base.Clear(disposeChildren);
+ unbindAllStartTimes();
+ }
+
+ private readonly Dictionary startTimeMap = new Dictionary();
+
+ private void bindStartTime(SelectionBlueprint blueprint)
+ {
+ var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
+
+ bindable.BindValueChanged(_ =>
+ {
+ if (LoadState >= LoadState.Ready)
+ SortInternal();
+ });
+
+ startTimeMap[blueprint] = bindable;
+ }
+
+ private void unbindStartTime(SelectionBlueprint blueprint)
+ {
+ startTimeMap[blueprint].UnbindAll();
+ startTimeMap.Remove(blueprint);
+ }
+
+ private void unbindAllStartTimes()
+ {
+ foreach (var kvp in startTimeMap)
+ kvp.Value.UnbindAll();
+ startTimeMap.Clear();
+ }
+
+ protected override int Compare(Drawable x, Drawable y)
+ {
+ var xObj = (SelectionBlueprint)x;
+ var yObj = (SelectionBlueprint)y;
+
+ // Put earlier blueprints towards the end of the list, so they handle input first
+ int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
+ return i == 0 ? CompareReverseChildID(x, y) : i;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index eef02e61a6..2f14c607c2 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public TimelineSelectionBlueprintContainer()
{
- AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
+ AddInternal(new TimelinePart(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}
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/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index 04da943a10..0efa9c5196 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Screens.Multi.Play
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(roomId.Value != null);
- return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem);
+ return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
}
protected override ScoreInfo CreateScore()
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index 8da6a530a8..3623208fa7 100644
--- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
+++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking
[Resolved]
private IAPIProvider api { get; set; }
- public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry)
: base(score, allowRetry)
{
this.roomId = roomId;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index b94f0a5062..d0a83e3c22 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -545,7 +545,7 @@ namespace osu.Game.Screens.Play
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
- protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score);
+ protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
#region Fail Logic
diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
index 56ccfd2253..dabdf0a139 100644
--- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs
+++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play
public class SpectatorResultsScreen : SoloResultsScreen
{
public SpectatorResultsScreen(ScoreInfo score)
- : base(score)
+ : base(score, false)
{
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index f8bdf0140c..887e7ec8a9 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;
@@ -57,7 +58,7 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry;
- protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
+ protected ResultsScreen(ScoreInfo score, bool allowRetry)
{
Score = score;
this.allowRetry = allowRetry;
@@ -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/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 9cf2e6757a..76b549da1a 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private RulesetStore rulesets { get; set; }
- public SoloResultsScreen(ScoreInfo score, bool allowRetry = true)
+ public SoloResultsScreen(ScoreInfo score, bool allowRetry)
: base(score, allowRetry)
{
}
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index ee8825640c..50a61ed4c2 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
}
protected void PresentScore(ScoreInfo score) =>
- FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score)));
+ FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs
new file mode 100644
index 0000000000..ca25efaa01
--- /dev/null
+++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs
@@ -0,0 +1,127 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement
+ {
+ private readonly HitResult result;
+
+ private readonly LegacyJudgementPieceOld temporaryOldStyle;
+
+ private readonly Drawable mainPiece;
+
+ private readonly ParticleExplosion particles;
+
+ public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Texture particleTexture)
+ {
+ this.result = result;
+
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new[]
+ {
+ mainPiece = createMainDrawable().With(d =>
+ {
+ d.Anchor = Anchor.Centre;
+ d.Origin = Anchor.Centre;
+ })
+ };
+
+ if (particleTexture != null)
+ {
+ AddInternal(particles = new ParticleExplosion(particleTexture, 150, 1600)
+ {
+ Size = new Vector2(140),
+ Depth = float.MaxValue,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+
+ if (result != HitResult.Miss)
+ {
+ //new judgement shows old as a temporary effect
+ AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f)
+ {
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ }
+ }
+
+ public void PlayAnimation()
+ {
+ var animation = mainPiece as IFramedAnimation;
+
+ animation?.GotoFrame(0);
+
+ if (particles != null)
+ {
+ // start the particles already some way into their animation to break cluster away from centre.
+ using (particles.BeginDelayedSequence(-100, true))
+ particles.Restart();
+ }
+
+ const double fade_in_length = 120;
+ const double fade_out_delay = 500;
+ const double fade_out_length = 600;
+
+ this.FadeInFromZero(fade_in_length);
+ this.Delay(fade_out_delay).FadeOut(fade_out_length);
+
+ // new style non-miss judgements show the original style temporarily, with additive colour.
+ if (temporaryOldStyle != null)
+ {
+ temporaryOldStyle.PlayAnimation();
+
+ temporaryOldStyle.Hide();
+ temporaryOldStyle.Delay(-16)
+ .FadeTo(0.5f, 56, Easing.Out).Then()
+ .FadeOut(300);
+ }
+
+ // legacy judgements don't play any transforms if they are an animation.
+ if (animation?.FrameCount > 1)
+ return;
+
+ switch (result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ //todo: this only applies to osu! ruleset apparently.
+ this.MoveTo(new Vector2(0, -2));
+ this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In);
+
+ float rotation = RNG.NextSingle(-8.6f, 8.6f);
+
+ this.RotateTo(0);
+ this.RotateTo(rotation, fade_in_length)
+ .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
+ break;
+
+ default:
+ mainPiece.ScaleTo(0.9f);
+ mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length);
+ break;
+ }
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects.
+ }
+}
diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
new file mode 100644
index 0000000000..5d74ab9ae3
--- /dev/null
+++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs
@@ -0,0 +1,75 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Skinning
+{
+ public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement
+ {
+ private readonly HitResult result;
+
+ private readonly float finalScale;
+
+ public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f)
+ {
+ this.result = result;
+ this.finalScale = finalScale;
+
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChild = createMainDrawable();
+ }
+
+ public virtual void PlayAnimation()
+ {
+ var animation = InternalChild as IFramedAnimation;
+
+ animation?.GotoFrame(0);
+
+ const double fade_in_length = 120;
+ const double fade_out_delay = 500;
+ const double fade_out_length = 600;
+
+ this.FadeInFromZero(fade_in_length);
+ this.Delay(fade_out_delay).FadeOut(fade_out_length);
+
+ // legacy judgements don't play any transforms if they are an animation.
+ if (animation?.FrameCount > 1)
+ return;
+
+ switch (result)
+ {
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ float rotation = RNG.NextSingle(-8.6f, 8.6f);
+
+ this.RotateTo(0);
+ this.RotateTo(rotation, fade_in_length)
+ .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
+ break;
+
+ default:
+
+ this.ScaleTo(0.6f).Then()
+ .ScaleTo(1.1f, fade_in_length * 0.8f).Then()
+ // this is actually correct to match stable; there were overlapping transforms.
+ .ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
+ .ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
+ .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
+ break;
+ }
+ }
+
+ public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index fb020f4e39..63a22eba62 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -371,19 +371,15 @@ namespace osu.Game.Skinning
}
case GameplaySkinComponent resultComponent:
- switch (resultComponent.Component)
+ Func createDrawable = () => getJudgementAnimation(resultComponent.Component);
+
+ // kind of wasteful that we throw this away, but should do for now.
+ if (createDrawable() != null)
{
- case HitResult.Miss:
- return this.GetAnimation("hit0", true, false);
-
- case HitResult.Meh:
- return this.GetAnimation("hit50", true, false);
-
- case HitResult.Ok:
- return this.GetAnimation("hit100", true, false);
-
- case HitResult.Great:
- return this.GetAnimation("hit300", true, false);
+ if (Configuration.LegacyVersion > 1)
+ return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component));
+ else
+ return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
break;
@@ -392,6 +388,43 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
+ private Texture getParticleTexture(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Meh:
+ return GetTexture("particle50");
+
+ case HitResult.Ok:
+ return GetTexture("particle100");
+
+ case HitResult.Great:
+ return GetTexture("particle300");
+ }
+
+ return null;
+ }
+
+ private Drawable getJudgementAnimation(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Miss:
+ return this.GetAnimation("hit0", true, false);
+
+ case HitResult.Meh:
+ return this.GetAnimation("hit50", true, false);
+
+ case HitResult.Ok:
+ return this.GetAnimation("hit100", true, false);
+
+ case HitResult.Great:
+ return this.GetAnimation("hit300", true, false);
+ }
+
+ return null;
+ }
+
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (var name in getFallbackNames(componentName))
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 5352928ec6..1340d1474c 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -24,7 +24,15 @@ namespace osu.Game.Skinning
{
}
- protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) };
+ protected override Drawable CreateDefault(ISkinComponent component)
+ {
+ var texture = textures.Get(component.LookupName);
+
+ if (texture == null)
+ return null;
+
+ return new Sprite { Texture = texture };
+ }
private class SpriteComponent : ISkinComponent
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 1850ee3488..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 2ac23f1503..692dac909a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+