From 3e1002fbf36650b9a91a6a04df9d15c0d220f8f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:06:08 +0900 Subject: [PATCH 1/5] Improve osu!catch caught fruit placement algorithm --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index d045dcf16a..8edc5d0eb2 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; + /// + /// The amount by which caught fruit should be scaled down to fit on the plate. + /// + private const float caught_fruit_scale_adjust = 0.5f; + [NotNull] private readonly Container trailsTarget; @@ -240,7 +245,7 @@ namespace osu.Game.Rulesets.Catch.UI if (result.IsHit) { - var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); + var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X); if (CatchFruitOnPlate) placeCaughtObject(palpableObject, positionInStack); @@ -470,7 +475,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.CopyStateFrom(drawableObject); caughtObject.Anchor = Anchor.TopCentre; caughtObject.Position = position; - caughtObject.Scale /= 2; + caughtObject.Scale *= caught_fruit_scale_adjust; caughtObjectContainer.Add(caughtObject); @@ -480,19 +485,21 @@ namespace osu.Game.Rulesets.Catch.UI private Vector2 computePositionInStack(Vector2 position, float displayRadius) { - const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; - const float allowance = 10; + // this is taken from osu-stable (lenience should be 10 * 10 at standard scale). + const float lenience_adjust = 10 / CatchHitObject.OBJECT_RADIUS; - while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2))) + float adjustedRadius = displayRadius * lenience_adjust; + float checkDistance = MathF.Pow(adjustedRadius, 2); + + // offset fruit vertically to better place "above" the plate. + position.Y -= 5; + + while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance)) { - float diff = (displayRadius + radius_div_2) / allowance; - - position.X += (RNG.NextSingle() - 0.5f) * diff * 2; - position.Y -= RNG.NextSingle() * diff; + position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius); + position.Y -= RNG.NextSingle(0, 5); } - position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); - return position; } From 84a713822306f5a6dd4897b58e3894f6b669190f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 16:56:23 +0900 Subject: [PATCH 2/5] Update tests to better support stack regeneration cases --- .../TestSceneCatcher.cs | 33 ++++++++++++++----- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 +++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 48efd73222..f8e278a486 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; @@ -170,16 +171,25 @@ namespace osu.Game.Rulesets.Catch.Tests } [Test] - public void TestCatcherStacking() + public void TestCatcherRandomStacking() + { + AddStep("catch more fruits", () => attemptCatch(() => new Fruit + { + X = (RNG.NextSingle() - 0.5f) * CatcherArea.CATCHER_SIZE + }, 50)); + } + + [Test] + public void TestCatcherStackingSameCaughtPosition() { AddStep("catch fruit", () => attemptCatch(new Fruit())); checkPlate(1); - AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9)); checkPlate(10); AddAssert("caught objects are stacked", () => - catcher.CaughtObjects.All(obj => obj.Y <= 0) && - catcher.CaughtObjects.Any(obj => obj.Y == 0) && - catcher.CaughtObjects.Any(obj => obj.Y < -20)); + catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && + catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && + catcher.CaughtObjects.Any(obj => obj.Y < -25)); } [Test] @@ -189,11 +199,11 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1); AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); - AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9)); AddStep("explode", () => catcher.Explode()); AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); - AddStep("catch fruits", () => attemptCatch(new Fruit(), 10)); + AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10)); AddStep("drop", () => catcher.Drop()); AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); } @@ -222,10 +232,15 @@ namespace osu.Game.Rulesets.Catch.Tests private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state); - private void attemptCatch(CatchHitObject hitObject, int count = 1) + private void attemptCatch(CatchHitObject hitObject) + { + attemptCatch(() => hitObject, 1); + } + + private void attemptCatch(Func hitObject, int count) { for (var i = 0; i < count; i++) - attemptCatch(hitObject, out _, out _); + attemptCatch(hitObject(), out _, out _); } private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8edc5d0eb2..4fc9e32a73 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; + /// + /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught". + /// + public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5; + /// /// The amount by which caught fruit should be scaled down to fit on the plate. /// @@ -492,7 +497,7 @@ namespace osu.Game.Rulesets.Catch.UI float checkDistance = MathF.Pow(adjustedRadius, 2); // offset fruit vertically to better place "above" the plate. - position.Y -= 5; + position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET; while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance)) { From dc2bc462b8c68b44b6e927ec84bbf87fc0586df3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:27:23 +0900 Subject: [PATCH 3/5] Expose internal catcher width calculation methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 4fc9e32a73..0d6a577d1e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -212,13 +212,13 @@ namespace osu.Game.Rulesets.Catch.UI /// Calculates the width of the area used for attempting catches in gameplay. /// /// The scale of the catcher. - internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; + public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; /// /// Calculates the width of the area used for attempting catches in gameplay. /// /// The beatmap difficulty. - internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); + public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Determine if this catcher can catch a in the current position. From 2203552e9e23528e79c63024d80186e4f5034bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:32:24 +0900 Subject: [PATCH 4/5] Add stacking test logic to `TestSceneCatcherArea` for skinned testing --- .../TestSceneCatcher.cs | 3 +- .../TestSceneCatcherArea.cs | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index f8e278a486..517027a9fc 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -175,7 +176,7 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch more fruits", () => attemptCatch(() => new Fruit { - X = (RNG.NextSingle() - 0.5f) * CatcherArea.CATCHER_SIZE + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One) }, 50)); } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 1cbfa6338e..3e4ee5ffa9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -8,6 +8,8 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; @@ -31,12 +33,32 @@ namespace osu.Game.Rulesets.Catch.Tests private float circleSize; + private ScheduledDelegate addManyFruit; + + private BeatmapDifficulty beatmapDifficulty; + public TestSceneCatcherArea() { AddSliderStep("circle size", 0, 8, 5, createCatcher); AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t))); - AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddStep("catch centered fruit", () => attemptCatch(new Fruit())); + AddStep("catch many random fruit", () => + { + int count = 50; + + addManyFruit?.Cancel(); + addManyFruit = Scheduler.AddDelayed(() => + { + attemptCatch(new Fruit + { + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty), + }); + + if (count-- == 0) + addManyFruit?.Cancel(); + }, 50, true); + }); AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true })); AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit())); AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true })); @@ -45,10 +67,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(Fruit fruit) { fruit.X = fruit.OriginalX + catcher.X; - fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty - { - CircleSize = circleSize - }); + fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty); foreach (var area in this.ChildrenOfType()) { @@ -71,6 +90,11 @@ namespace osu.Game.Rulesets.Catch.Tests { circleSize = size; + beatmapDifficulty = new BeatmapDifficulty + { + CircleSize = circleSize + }; + SetContents(() => { var droppedObjectContainer = new Container @@ -84,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Tests Children = new Drawable[] { droppedObjectContainer, - new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size }) + new TestCatcherArea(droppedObjectContainer, beatmapDifficulty) { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, From bdf07ad59a7cfc0abb796133001da52bd83f314b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:47:03 +0900 Subject: [PATCH 5/5] Limit catching towards the centre of the plate (to emulate actual gameplay) --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 3e4ee5ffa9..ad404e1f63 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Tests { attemptCatch(new Fruit { - X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty), + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f, }); if (count-- == 0)