2022-03-09 20:36:31 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 osu.Framework.Graphics.Primitives ;
2022-04-01 11:47:21 +08:00
using osu.Framework.Utils ;
2022-03-09 20:36:31 +08:00
using osu.Game.Rulesets.Osu.Objects ;
using osu.Game.Rulesets.Osu.UI ;
using osuTK ;
#nullable enable
2022-03-09 21:52:15 +08:00
namespace osu.Game.Rulesets.Osu.Utils
2022-03-09 20:36:31 +08:00
{
2022-03-10 11:53:03 +08:00
public static partial class OsuHitObjectGenerationUtils
2022-03-09 20:36:31 +08:00
{
/// <summary>
/// Number of previous hitobjects to be shifted together when an object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10 ;
private static readonly Vector2 playfield_centre = OsuPlayfield . BASE_SIZE / 2 ;
/// <summary>
2022-03-10 12:02:25 +08:00
/// Generate a list of <see cref="ObjectPositionInfo"/>s containing information for how the given list of
2022-03-10 11:53:03 +08:00
/// <see cref="OsuHitObject"/>s are positioned.
2022-03-09 20:36:31 +08:00
/// </summary>
2022-03-10 11:53:03 +08:00
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param>
2022-03-10 12:02:25 +08:00
/// <returns>A list of <see cref="ObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns>
public static List < ObjectPositionInfo > GeneratePositionInfos ( IEnumerable < OsuHitObject > hitObjects )
2022-03-09 20:36:31 +08:00
{
2022-03-10 12:02:25 +08:00
var positionInfos = new List < ObjectPositionInfo > ( ) ;
2022-03-09 20:36:31 +08:00
Vector2 previousPosition = playfield_centre ;
float previousAngle = 0 ;
foreach ( OsuHitObject hitObject in hitObjects )
{
Vector2 relativePosition = hitObject . Position - previousPosition ;
2022-04-11 14:15:08 +08:00
float absoluteAngle = MathF . Atan2 ( relativePosition . Y , relativePosition . X ) ;
2022-03-09 20:36:31 +08:00
float relativeAngle = absoluteAngle - previousAngle ;
2022-04-01 11:36:20 +08:00
ObjectPositionInfo positionInfo ;
positionInfos . Add ( positionInfo = new ObjectPositionInfo ( hitObject )
2022-03-09 20:36:31 +08:00
{
RelativeAngle = relativeAngle ,
DistanceFromPrevious = relativePosition . Length
} ) ;
2022-04-01 11:41:45 +08:00
if ( hitObject is Slider slider )
2022-04-01 11:36:20 +08:00
{
2022-04-01 11:41:45 +08:00
float absoluteRotation = getSliderRotation ( slider ) ;
2022-04-01 11:36:20 +08:00
positionInfo . Rotation = absoluteRotation - absoluteAngle ;
absoluteAngle = absoluteRotation ;
}
2022-03-09 20:36:31 +08:00
previousPosition = hitObject . EndPosition ;
previousAngle = absoluteAngle ;
}
2022-03-10 11:53:03 +08:00
return positionInfos ;
2022-03-09 20:36:31 +08:00
}
/// <summary>
2022-03-10 11:53:03 +08:00
/// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>.
2022-03-09 20:36:31 +08:00
/// </summary>
2022-03-10 12:02:25 +08:00
/// <param name="objectPositionInfos">Position information for each hit object.</param>
2022-03-10 11:53:03 +08:00
/// <returns>The repositioned hit objects.</returns>
2022-03-10 12:02:25 +08:00
public static List < OsuHitObject > RepositionHitObjects ( IEnumerable < ObjectPositionInfo > objectPositionInfos )
2022-03-09 20:36:31 +08:00
{
2022-03-14 20:18:30 +08:00
List < WorkingObject > workingObjects = objectPositionInfos . Select ( o = > new WorkingObject ( o ) ) . ToList ( ) ;
WorkingObject ? previous = null ;
2022-03-09 20:36:31 +08:00
2022-03-14 20:18:30 +08:00
for ( int i = 0 ; i < workingObjects . Count ; i + + )
2022-03-09 20:36:31 +08:00
{
2022-03-14 20:18:30 +08:00
var current = workingObjects [ i ] ;
2022-03-10 11:23:52 +08:00
var hitObject = current . HitObject ;
2022-03-09 20:36:31 +08:00
if ( hitObject is Spinner )
{
2022-04-01 11:59:24 +08:00
previous = current ;
2022-03-09 20:36:31 +08:00
continue ;
}
2022-03-14 20:18:30 +08:00
computeModifiedPosition ( current , previous , i > 1 ? workingObjects [ i - 2 ] : null ) ;
2022-03-09 20:36:31 +08:00
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2 . Zero ;
switch ( hitObject )
{
2022-03-14 20:23:35 +08:00
case HitCircle _ :
shift = clampHitCircleToPlayfield ( current ) ;
2022-03-09 20:36:31 +08:00
break ;
2022-03-14 20:23:35 +08:00
case Slider _ :
shift = clampSliderToPlayfield ( current ) ;
2022-03-09 20:36:31 +08:00
break ;
}
if ( shift ! = Vector2 . Zero )
{
var toBeShifted = new List < OsuHitObject > ( ) ;
for ( int j = i - 1 ; j > = i - preceding_hitobjects_to_shift & & j > = 0 ; j - - )
{
// only shift hit circles
2022-03-14 20:18:30 +08:00
if ( ! ( workingObjects [ j ] . HitObject is HitCircle ) ) break ;
2022-03-09 20:36:31 +08:00
2022-03-14 20:18:30 +08:00
toBeShifted . Add ( workingObjects [ j ] . HitObject ) ;
2022-03-09 20:36:31 +08:00
}
if ( toBeShifted . Count > 0 )
applyDecreasingShift ( toBeShifted , shift ) ;
}
previous = current ;
}
2022-03-10 11:53:03 +08:00
2022-03-14 20:18:30 +08:00
return workingObjects . Select ( p = > p . HitObject ) . ToList ( ) ;
2022-03-09 20:36:31 +08:00
}
/// <summary>
2022-03-09 20:52:11 +08:00
/// Compute the modified position of a hit object while attempting to keep it inside the playfield.
2022-03-09 20:36:31 +08:00
/// </summary>
2022-03-14 20:18:30 +08:00
/// <param name="current">The <see cref="WorkingObject"/> representing the hit object to have the modified position computed for.</param>
/// <param name="previous">The <see cref="WorkingObject"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="WorkingObject"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private static void computeModifiedPosition ( WorkingObject current , WorkingObject ? previous , WorkingObject ? beforePrevious )
2022-03-09 20:36:31 +08:00
{
float previousAbsoluteAngle = 0f ;
if ( previous ! = null )
{
2022-04-01 11:36:20 +08:00
if ( previous . HitObject is Slider s )
{
previousAbsoluteAngle = getSliderRotation ( s ) ;
}
else
{
Vector2 earliestPosition = beforePrevious ? . HitObject . EndPosition ? ? playfield_centre ;
Vector2 relativePosition = previous . HitObject . Position - earliestPosition ;
2022-04-11 14:15:08 +08:00
previousAbsoluteAngle = MathF . Atan2 ( relativePosition . Y , relativePosition . X ) ;
2022-04-01 11:36:20 +08:00
}
2022-03-09 20:36:31 +08:00
}
2022-03-14 20:18:30 +08:00
float absoluteAngle = previousAbsoluteAngle + current . PositionInfo . RelativeAngle ;
2022-03-09 20:36:31 +08:00
var posRelativeToPrev = new Vector2 (
2022-04-11 14:15:08 +08:00
current . PositionInfo . DistanceFromPrevious * MathF . Cos ( absoluteAngle ) ,
current . PositionInfo . DistanceFromPrevious * MathF . Sin ( absoluteAngle )
2022-03-09 20:36:31 +08:00
) ;
2022-03-09 20:52:11 +08:00
Vector2 lastEndPosition = previous ? . EndPositionModified ? ? playfield_centre ;
2022-03-09 20:36:31 +08:00
2022-03-10 11:53:03 +08:00
posRelativeToPrev = RotateAwayFromEdge ( lastEndPosition , posRelativeToPrev ) ;
2022-03-09 20:36:31 +08:00
2022-03-09 20:52:11 +08:00
current . PositionModified = lastEndPosition + posRelativeToPrev ;
2022-04-01 11:36:20 +08:00
if ( ! ( current . HitObject is Slider slider ) )
return ;
2022-04-11 14:15:08 +08:00
absoluteAngle = MathF . Atan2 ( posRelativeToPrev . Y , posRelativeToPrev . X ) ;
2022-04-01 11:41:45 +08:00
2022-04-01 11:36:20 +08:00
Vector2 centreOfMassOriginal = calculateCentreOfMass ( slider ) ;
2022-04-01 11:41:45 +08:00
Vector2 centreOfMassModified = rotateVector ( centreOfMassOriginal , current . PositionInfo . Rotation + absoluteAngle - getSliderRotation ( slider ) ) ;
2022-04-01 11:36:20 +08:00
centreOfMassModified = RotateAwayFromEdge ( current . PositionModified , centreOfMassModified ) ;
2022-04-11 14:15:08 +08:00
float relativeRotation = MathF . Atan2 ( centreOfMassModified . Y , centreOfMassModified . X ) - MathF . Atan2 ( centreOfMassOriginal . Y , centreOfMassOriginal . X ) ;
2022-04-01 11:47:21 +08:00
if ( ! Precision . AlmostEquals ( relativeRotation , 0 ) )
RotateSlider ( slider , relativeRotation ) ;
2022-03-09 20:36:31 +08:00
}
/// <summary>
2022-03-14 20:23:35 +08:00
/// Move the modified position of a <see cref="HitCircle"/> so that it fits inside the playfield.
2022-03-09 20:36:31 +08:00
/// </summary>
2022-03-09 20:52:11 +08:00
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
2022-03-14 20:23:35 +08:00
private static Vector2 clampHitCircleToPlayfield ( WorkingObject workingObject )
2022-03-09 20:36:31 +08:00
{
2022-03-14 20:23:35 +08:00
var previousPosition = workingObject . PositionModified ;
workingObject . EndPositionModified = workingObject . PositionModified = clampToPlayfieldWithPadding (
workingObject . PositionModified ,
( float ) workingObject . HitObject . Radius
2022-03-09 20:36:31 +08:00
) ;
2022-03-14 20:23:35 +08:00
workingObject . HitObject . Position = workingObject . PositionModified ;
2022-03-09 20:36:31 +08:00
2022-03-14 20:23:35 +08:00
return workingObject . PositionModified - previousPosition ;
2022-03-09 20:36:31 +08:00
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
2022-03-09 20:52:11 +08:00
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
2022-03-14 20:23:35 +08:00
private static Vector2 clampSliderToPlayfield ( WorkingObject workingObject )
2022-03-09 20:36:31 +08:00
{
2022-03-14 20:23:35 +08:00
var slider = ( Slider ) workingObject . HitObject ;
2022-03-09 20:36:31 +08:00
var possibleMovementBounds = calculatePossibleMovementBounds ( slider ) ;
2022-03-14 20:23:35 +08:00
var previousPosition = workingObject . PositionModified ;
2022-03-09 20:36:31 +08:00
// Clamp slider position to the placement area
2022-04-01 11:37:10 +08:00
// If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
2022-03-09 20:36:31 +08:00
float newX = possibleMovementBounds . Width < 0
2022-04-01 11:37:10 +08:00
? Math . Clamp ( possibleMovementBounds . Left , 0 , OsuPlayfield . BASE_SIZE . X )
2022-03-09 20:36:31 +08:00
: Math . Clamp ( previousPosition . X , possibleMovementBounds . Left , possibleMovementBounds . Right ) ;
float newY = possibleMovementBounds . Height < 0
2022-04-01 11:37:10 +08:00
? Math . Clamp ( possibleMovementBounds . Top , 0 , OsuPlayfield . BASE_SIZE . Y )
2022-03-09 20:36:31 +08:00
: Math . Clamp ( previousPosition . Y , possibleMovementBounds . Top , possibleMovementBounds . Bottom ) ;
2022-03-14 20:23:35 +08:00
slider . Position = workingObject . PositionModified = new Vector2 ( newX , newY ) ;
workingObject . EndPositionModified = slider . EndPosition ;
2022-03-09 20:36:31 +08:00
2022-03-14 20:23:35 +08:00
shiftNestedObjects ( slider , workingObject . PositionModified - workingObject . PositionOriginal ) ;
2022-03-09 20:36:31 +08:00
2022-03-14 20:23:35 +08:00
return workingObject . PositionModified - previousPosition ;
2022-03-09 20:36:31 +08:00
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
2022-03-10 11:53:03 +08:00
private static void applyDecreasingShift ( IList < OsuHitObject > hitObjects , Vector2 shift )
2022-03-09 20:36:31 +08:00
{
for ( int i = 0 ; i < hitObjects . Count ; i + + )
{
var hitObject = hitObjects [ i ] ;
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject . Position + shift * ( ( hitObjects . Count - i ) / ( float ) ( hitObjects . Count + 1 ) ) ;
hitObject . Position = clampToPlayfieldWithPadding ( position , ( float ) hitObject . Radius ) ;
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
2022-03-10 11:53:03 +08:00
private static RectangleF calculatePossibleMovementBounds ( Slider slider )
2022-03-09 20:36:31 +08:00
{
var pathPositions = new List < Vector2 > ( ) ;
slider . Path . GetPathToProgress ( pathPositions , 0 , 1 ) ;
float minX = float . PositiveInfinity ;
float maxX = float . NegativeInfinity ;
float minY = float . PositiveInfinity ;
float maxY = float . NegativeInfinity ;
// Compute the bounding box of the slider.
foreach ( var pos in pathPositions )
{
minX = MathF . Min ( minX , pos . X ) ;
maxX = MathF . Max ( maxX , pos . X ) ;
minY = MathF . Min ( minY , pos . Y ) ;
maxY = MathF . Max ( maxY , pos . Y ) ;
}
// Take the circle radius into account.
float radius = ( float ) slider . Radius ;
minX - = radius ;
minY - = radius ;
maxX + = radius ;
maxY + = radius ;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = - minX ;
float right = OsuPlayfield . BASE_SIZE . X - maxX ;
float top = - minY ;
float bottom = OsuPlayfield . BASE_SIZE . Y - maxY ;
return new RectangleF ( left , top , right - left , bottom - top ) ;
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
2022-03-10 11:53:03 +08:00
private static void shiftNestedObjects ( Slider slider , Vector2 shift )
2022-03-09 20:36:31 +08:00
{
foreach ( var hitObject in slider . NestedHitObjects . Where ( o = > o is SliderTick | | o is SliderRepeat ) )
{
if ( ! ( hitObject is OsuHitObject osuHitObject ) )
continue ;
osuHitObject . Position + = shift ;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
2022-03-10 11:53:03 +08:00
private static Vector2 clampToPlayfieldWithPadding ( Vector2 position , float padding )
2022-03-09 20:36:31 +08:00
{
return new Vector2 (
Math . Clamp ( position . X , padding , OsuPlayfield . BASE_SIZE . X - padding ) ,
Math . Clamp ( position . Y , padding , OsuPlayfield . BASE_SIZE . Y - padding )
) ;
}
2022-04-01 11:49:27 +08:00
/// <summary>
/// Estimate the centre of mass of a slider relative to its start position.
/// </summary>
/// <param name="slider">The slider to process.</param>
/// <returns>The centre of mass of the slider.</returns>
2022-04-01 11:36:20 +08:00
private static Vector2 calculateCentreOfMass ( Slider slider )
{
2022-04-17 10:34:48 +08:00
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 ) ;
}
2022-04-01 11:47:21 +08:00
2022-04-01 11:36:20 +08:00
int count = 0 ;
Vector2 sum = Vector2 . Zero ;
double pathDistance = slider . Distance ;
2022-04-17 10:34:48 +08:00
for ( double i = 0 ; i < pathDistance ; i + = sample_step )
2022-04-01 11:36:20 +08:00
{
sum + = slider . Path . PositionAt ( i / pathDistance ) ;
count + + ;
}
return sum / count ;
}
2022-04-01 11:49:27 +08:00
/// <summary>
2022-04-01 11:50:30 +08:00
/// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
2022-04-01 11:49:27 +08:00
/// </summary>
/// <param name="slider">The slider to process.</param>
/// <returns>The angle in radians.</returns>
2022-04-01 11:36:20 +08:00
private static float getSliderRotation ( Slider slider )
{
2022-04-01 11:50:30 +08:00
var endPositionVector = slider . Path . PositionAt ( 1 ) ;
2022-04-11 14:15:08 +08:00
return MathF . Atan2 ( endPositionVector . Y , endPositionVector . X ) ;
2022-04-01 11:36:20 +08:00
}
2022-03-10 12:02:25 +08:00
public class ObjectPositionInfo
2022-03-09 20:36:31 +08:00
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
2022-03-10 12:02:25 +08:00
public float RelativeAngle { get ; set ; }
2022-03-09 20:36:31 +08:00
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
2022-03-10 12:02:25 +08:00
public float DistanceFromPrevious { get ; set ; }
2022-03-09 20:36:31 +08:00
2022-04-01 11:36:20 +08:00
/// <summary>
/// The rotation of the hit object, relative to its jump angle.
2022-04-01 11:50:30 +08:00
/// 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.
2022-04-01 11:36:20 +08:00
/// For hit circles and spinners, this property is ignored.
/// </summary>
public float Rotation { get ; set ; }
2022-03-09 20:36:31 +08:00
/// <summary>
2022-03-10 12:02:25 +08:00
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
2022-03-09 20:36:31 +08:00
/// </summary>
2022-03-10 12:02:25 +08:00
public OsuHitObject HitObject { get ; }
public ObjectPositionInfo ( OsuHitObject hitObject )
{
HitObject = hitObject ;
}
2022-03-09 20:36:31 +08:00
}
2022-03-14 20:18:30 +08:00
private class WorkingObject
2022-03-09 20:36:31 +08:00
{
public Vector2 PositionOriginal { get ; }
2022-03-09 20:52:11 +08:00
public Vector2 PositionModified { get ; set ; }
public Vector2 EndPositionModified { get ; set ; }
2022-03-09 20:36:31 +08:00
2022-03-14 20:18:30 +08:00
public ObjectPositionInfo PositionInfo { get ; }
public OsuHitObject HitObject = > PositionInfo . HitObject ;
public WorkingObject ( ObjectPositionInfo positionInfo )
2022-03-09 20:36:31 +08:00
{
2022-03-14 20:18:30 +08:00
PositionInfo = positionInfo ;
2022-03-10 12:02:25 +08:00
PositionModified = PositionOriginal = HitObject . Position ;
EndPositionModified = HitObject . EndPosition ;
2022-03-09 20:36:31 +08:00
}
}
}
}