From cabbc486e9d1940f4d95429c6691758ff7ee6b85 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:36:20 +0800 Subject: [PATCH 01/12] Rotate sliders in random mod --- .../Utils/OsuHitObjectGenerationUtils.cs | 35 +++++++++ .../OsuHitObjectGenerationUtils_Reposition.cs | 72 +++++++++++++++++-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index da73c2addb..19d3390f56 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -146,5 +146,40 @@ public static void ReflectVertically(OsuHitObject osuObject) slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); } + + /// + /// Rotate a slider about its start position by the specified angle. + /// + /// The slider to be rotated. + /// The angle to rotate the slider by. + public static void RotateSlider(Slider slider, float rotation) + { + void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + + var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(); + foreach (var point in controlPoints) + point.Position = rotateVector(point.Position, rotation); + + slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); + } + + /// + /// Rotate a vector by the specified angle. + /// + /// The vector to be rotated. + /// The angle to rotate the vector by. + /// The rotated vector. + private static Vector2 rotateVector(Vector2 vector, float rotation) + { + float angle = (float)Math.Atan2(vector.Y, vector.X) + rotation; + float length = vector.Length; + return new Vector2( + length * (float)Math.Cos(angle), + length * (float)Math.Sin(angle) + ); + } } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index d1bc3b45df..ef1c258a8d 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -40,12 +40,21 @@ public static List GeneratePositionInfos(IEnumerable @@ -287,6 +313,27 @@ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float paddi ); } + private static Vector2 calculateCentreOfMass(Slider slider) + { + int count = 0; + Vector2 sum = Vector2.Zero; + double pathDistance = slider.Distance; + + for (double i = 0; i < pathDistance; i++) + { + sum += slider.Path.PositionAt(i / pathDistance); + count++; + } + + return sum / count; + } + + private static float getSliderRotation(Slider slider) + { + var endPositionVector = slider.EndPosition - slider.Position; + return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + } + public class ObjectPositionInfo { /// @@ -309,6 +356,13 @@ public class ObjectPositionInfo /// public float DistanceFromPrevious { get; set; } + /// + /// The rotation of the hit object, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to its end position, relative to its jump angle. + /// For hit circles and spinners, this property is ignored. + /// + public float Rotation { get; set; } + /// /// The hit object associated with this . /// @@ -325,6 +379,7 @@ private class WorkingObject public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } + public float RotationOriginal { get; } public ObjectPositionInfo PositionInfo { get; } public OsuHitObject HitObject => PositionInfo.HitObject; @@ -334,6 +389,15 @@ public WorkingObject(ObjectPositionInfo positionInfo) PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; + + if (HitObject is Slider slider) + { + RotationOriginal = getSliderRotation(slider); + } + else + { + RotationOriginal = 0; + } } } } From 998df5a4fef7627a897c23afa4f8b57c01e2a6f7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:37:10 +0800 Subject: [PATCH 02/12] Fix large slider clamping --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index ef1c258a8d..9f308df985 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -198,13 +198,13 @@ private static Vector2 clampSliderToPlayfield(WorkingObject workingObject) var previousPosition = workingObject.PositionModified; // Clamp slider position to the placement area - // If the slider is larger than the playfield, force it to stay at the original position + // If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield float newX = possibleMovementBounds.Width < 0 - ? workingObject.PositionOriginal.X + ? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X) : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); float newY = possibleMovementBounds.Height < 0 - ? workingObject.PositionOriginal.Y + ? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y) : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); slider.Position = workingObject.PositionModified = new Vector2(newX, newY); From af3835083ccd246810758b0156adbca2b3117587 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:41:45 +0800 Subject: [PATCH 03/12] Fix slider relative rotation calculation --- .../OsuHitObjectGenerationUtils_Reposition.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 9f308df985..fe5841daac 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -47,10 +47,9 @@ public static List GeneratePositionInfos(IEnumerable PositionInfo.HitObject; @@ -389,15 +389,6 @@ public WorkingObject(ObjectPositionInfo positionInfo) PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; - - if (HitObject is Slider slider) - { - RotationOriginal = getSliderRotation(slider); - } - else - { - RotationOriginal = 0; - } } } } From c0a78924aa2e5fe9ea9f6fe671e7fa770e472cac Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:47:21 +0800 Subject: [PATCH 04/12] Fix generation for zero-length sliders --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index fe5841daac..5f3719146f 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; @@ -167,7 +168,8 @@ private static void computeModifiedPosition(WorkingObject current, WorkingObject centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); - RotateSlider(slider, relativeRotation); + if (!Precision.AlmostEquals(relativeRotation, 0)) + RotateSlider(slider, relativeRotation); } /// @@ -316,6 +318,8 @@ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float paddi private static Vector2 calculateCentreOfMass(Slider slider) { + if (slider.Distance < 1) return Vector2.Zero; + int count = 0; Vector2 sum = Vector2.Zero; double pathDistance = slider.Distance; From 0015f627b04a23f407ff514255650003fb610c0b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:49:27 +0800 Subject: [PATCH 05/12] Add xmldoc --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 5f3719146f..ccc2529768 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -316,6 +316,11 @@ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float paddi ); } + /// + /// Estimate the centre of mass of a slider relative to its start position. + /// + /// The slider to process. + /// The centre of mass of the slider. private static Vector2 calculateCentreOfMass(Slider slider) { if (slider.Distance < 1) return Vector2.Zero; @@ -333,6 +338,11 @@ private static Vector2 calculateCentreOfMass(Slider slider) return sum / count; } + /// + /// Get the absolute rotation of a slider, defined as the angle from its start position to its end position. + /// + /// The slider to process. + /// The angle in radians. private static float getSliderRotation(Slider slider) { var endPositionVector = slider.EndPosition - slider.Position; From 031a977009d466796eb90aa9386d24ce16410f6c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:50:30 +0800 Subject: [PATCH 06/12] Calculate slider rotation using end point of path instead of EndPosition --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index ccc2529768..45285e5e0c 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -339,13 +339,13 @@ private static Vector2 calculateCentreOfMass(Slider slider) } /// - /// Get the absolute rotation of a slider, defined as the angle from its start position to its end position. + /// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path. /// /// The slider to process. /// The angle in radians. private static float getSliderRotation(Slider slider) { - var endPositionVector = slider.EndPosition - slider.Position; + var endPositionVector = slider.Path.PositionAt(1); return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); } @@ -373,7 +373,7 @@ public class ObjectPositionInfo /// /// The rotation of the hit object, relative to its jump angle. - /// For sliders, this is defined as the angle from the slider's start position to its end position, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle. /// For hit circles and spinners, this property is ignored. /// public float Rotation { get; set; } From ee6567788425bd24b198493bdd7f565fd6f0b4a5 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:57:45 +0800 Subject: [PATCH 07/12] Use height of playfield instead of width when randomizing the first object This is the change discussed in #17194. The effect of this change is barely noticeable, but it makes more sense to generate the object within playfield from the start. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index fea9246035..ccc56bd64f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -48,7 +48,7 @@ public void ApplyToBeatmap(IBeatmap beatmap) if (positionInfo == positionInfos.First()) { - positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); + positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else From 3bebc88306c9abb2543686ba22fbbcbd51d2b0da Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:59:24 +0800 Subject: [PATCH 08/12] Consider spinners when calculating jump angles Spinners are considered in `GeneratePositionInfos`, so they should also be considered in `RepositionHitObjects` --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 45285e5e0c..664bfae35a 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -79,7 +79,7 @@ public static List RepositionHitObjects(IEnumerable Date: Mon, 11 Apr 2022 14:15:08 +0800 Subject: [PATCH 09/12] USe `MathF` in all applicable places --- .../Utils/OsuHitObjectGenerationUtils.cs | 6 +++--- .../OsuHitObjectGenerationUtils_Reposition.cs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 19d3390f56..6129e6bfc4 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -174,11 +174,11 @@ public static void RotateSlider(Slider slider, float rotation) /// The rotated vector. private static Vector2 rotateVector(Vector2 vector, float rotation) { - float angle = (float)Math.Atan2(vector.Y, vector.X) + rotation; + float angle = MathF.Atan2(vector.Y, vector.X) + rotation; float length = vector.Length; return new Vector2( - length * (float)Math.Cos(angle), - length * (float)Math.Sin(angle) + length * MathF.Cos(angle), + length * MathF.Sin(angle) ); } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 664bfae35a..2abbd61c59 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -38,7 +38,7 @@ public static List GeneratePositionInfos(IEnumerable Date: Sun, 17 Apr 2022 10:34:48 +0800 Subject: [PATCH 10/12] Use a bigger sample step to calculate slider center of mass --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 2abbd61c59..a77d1f8b0f 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -323,13 +323,19 @@ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float paddi /// The centre of mass of the slider. private static Vector2 calculateCentreOfMass(Slider slider) { - if (slider.Distance < 1) return Vector2.Zero; + const double sample_step = 50; + + // just sample the start and end positions if the slider is too short + if (slider.Distance <= sample_step) + { + return Vector2.Divide(slider.Path.PositionAt(1), 2); + } int count = 0; Vector2 sum = Vector2.Zero; double pathDistance = slider.Distance; - for (double i = 0; i < pathDistance; i++) + for (double i = 0; i < pathDistance; i += sample_step) { sum += slider.Path.PositionAt(i / pathDistance); count++; From 1d79266d422fe8dbe06eec2d259c2aab348df106 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 17 Apr 2022 10:40:43 +0800 Subject: [PATCH 11/12] Clarify in the xmldoc that angles are measured in radians --- osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 6129e6bfc4..7b0d061e9a 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -151,7 +151,7 @@ public static void ReflectVertically(OsuHitObject osuObject) /// Rotate a slider about its start position by the specified angle. /// /// The slider to be rotated. - /// The angle to rotate the slider by. + /// The angle, measured in radians, to rotate the slider by. public static void RotateSlider(Slider slider, float rotation) { void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; @@ -170,7 +170,7 @@ public static void RotateSlider(Slider slider, float rotation) /// Rotate a vector by the specified angle. /// /// The vector to be rotated. - /// The angle to rotate the vector by. + /// The angle, measured in radians, to rotate the vector by. /// The rotated vector. private static Vector2 rotateVector(Vector2 vector, float rotation) { From e5e196097584484bc9062328b887cd806232b960 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 18 Apr 2022 09:38:51 +0800 Subject: [PATCH 12/12] Add inline comments --- osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 7b0d061e9a..266f7d1251 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -116,6 +116,7 @@ public static void ReflectHorizontally(OsuHitObject osuObject) if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); @@ -137,6 +138,7 @@ public static void ReflectVertically(OsuHitObject osuObject) if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); @@ -156,6 +158,7 @@ public static void RotateSlider(Slider slider, float rotation) { void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);