diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 23500f5da6..79ff222a89 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods 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 diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index da73c2addb..266f7d1251 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils 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 @@ namespace osu.Game.Rulesets.Osu.Utils 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)); @@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils 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, 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; + + // 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); + + 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, measured in radians, to rotate the vector by. + /// The rotated vector. + private static Vector2 rotateVector(Vector2 vector, float rotation) + { + float angle = MathF.Atan2(vector.Y, vector.X) + rotation; + float length = vector.Length; + return new Vector2( + 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 d1bc3b45df..a77d1f8b0f 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; 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; @@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils foreach (OsuHitObject hitObject in hitObjects) { Vector2 relativePosition = hitObject.Position - previousPosition; - float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - positionInfos.Add(new ObjectPositionInfo(hitObject) + ObjectPositionInfo positionInfo; + positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length }); + if (hitObject is Slider slider) + { + float absoluteRotation = getSliderRotation(slider); + positionInfo.Rotation = absoluteRotation - absoluteAngle; + absoluteAngle = absoluteRotation; + } + previousPosition = hitObject.EndPosition; previousAngle = absoluteAngle; } @@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (hitObject is Spinner) { - previous = null; + previous = current; continue; } @@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils if (previous != null) { - Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; - Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + if (previous.HitObject is Slider s) + { + previousAbsoluteAngle = getSliderRotation(s); + } + else + { + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); + } } float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; var posRelativeToPrev = new Vector2( - current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), - current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) + current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle), + current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; @@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionModified = lastEndPosition + posRelativeToPrev; + + if (!(current.HitObject is Slider slider)) + return; + + absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + + Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); + Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider)); + centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); + + float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); + if (!Precision.AlmostEquals(relativeRotation, 0)) + RotateSlider(slider, relativeRotation); } /// @@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils 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); @@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils ); } + /// + /// 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) + { + 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 += sample_step) + { + sum += slider.Path.PositionAt(i / pathDistance); + count++; + } + + return sum / count; + } + + /// + /// 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.Path.PositionAt(1); + return MathF.Atan2(endPositionVector.Y, endPositionVector.X); + } + public class ObjectPositionInfo { /// @@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils /// 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 the end of its path, 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 . ///