1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 08:22:56 +08:00

Merge pull request #15774 from smoogipoo/refactor-osu-difficulty-hit-object

Refactor OsuDifficultyHitObject for readability/understandability
This commit is contained in:
Dean Herbert 2021-12-09 18:59:24 +09:00 committed by GitHub
commit 603ba61b3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 58 deletions

View File

@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; private const int min_delta_time = 25;
private const float maximum_slider_radius = normalized_radius * 2.4f; private const float maximum_slider_radius = normalised_radius * 2.4f;
private const float assumed_slider_radius = normalized_radius * 1.8f; private const float assumed_slider_radius = normalised_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary> /// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>. /// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary> /// </summary>
public double JumpDistance { get; private set; } public readonly double StrainTime;
/// <summary> /// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>. /// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// <para>
/// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
/// </para>
/// </summary> /// </summary>
public double MovementDistance { get; private set; } public double LazyJumpDistance { get; private set; }
/// <summary> /// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>. /// Normalised shortest distance to consider for a jump between the previous <see cref="OsuDifficultyHitObject"/> and this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
/// <remarks>
/// This is bounded from above by <see cref="LazyJumpDistance"/>, and is smaller than the former if a more natural path is able to be taken through the previous <see cref="OsuDifficultyHitObject"/>.
/// </remarks>
/// <example>
/// Suppose a linear slider - circle pattern.
/// <br />
/// Following the slider lazily (see: <see cref="LazyJumpDistance"/>) will result in underestimating the true end position of the slider as being closer towards the start position.
/// As a result, <see cref="LazyJumpDistance"/> overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
/// such that the jump is felt as only starting from the slider's true end position.
/// <br />
/// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
/// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
/// </example>
public double MinimumJumpDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="MinimumJumpDistance"/>, with a minimum value of 25ms.
/// </summary>
public double MinimumJumpTime { get; private set; }
/// <summary>
/// Normalised distance between the start and end position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
/// </summary>
public double TravelTime { get; private set; }
/// <summary> /// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>. /// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current). /// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary> /// </summary>
public double? Angle { get; private set; } public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject; private readonly OsuHitObject lastObject;
@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances(double clockRate) private void setDistances(double clockRate)
{ {
if (BaseObject is Slider currentSlider)
{
computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance;
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
}
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner) if (BaseObject is Spinner || lastObject is Spinner)
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius; float scalingFactor = normalised_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30) if (BaseObject.Radius < 30)
{ {
@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
} }
Vector2 lastCursorPosition = getEndCursorPosition(lastObject); Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
MinimumJumpTime = StrainTime;
MinimumJumpDistance = LazyJumpDistance;
if (lastObject is Slider lastSlider) if (lastObject is Slider lastSlider)
{ {
computeSliderCursorPosition(lastSlider); double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
TravelDistance = lastSlider.LazyTravelDistance; MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time); //
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
//
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
//
// <======o==> ← slider
// | ← most natural jump path
// o ← a follow-up hitcircle
//
// In this case the most natural jump path is approximated by LazyJumpDistance.
//
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
//
// <======o==>---o
// ↑
// most natural jump path
//
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
//
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
//
// Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
// For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
// such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
// In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
// Additional distance is removed based on position of jump relative to slider follow circle radius.
// JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible.
MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
else
{
MovementTime = StrainTime;
MovementDistance = JumpDistance;
} }
if (lastLastObject != null && !(lastLastObject is Spinner)) if (lastLastObject != null && !(lastLastObject is Spinner))
@ -139,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++) for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{ {
@ -167,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalized_radius; requiredMovement = normalised_radius;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)

View File

@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1]; var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object. // But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider && withSliders) if (osuLastObj.BaseObject is Slider && withSliders)
{ {
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end. double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
} }
// As above, do the same for the previous hitobject. // As above, do the same for the previous hitobject.
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime; double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider && withSliders) if (osuLastLastObj.BaseObject is Slider && withSliders)
{ {
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
} }
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
} }
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Math.Max(prevVelocity, currVelocity) != 0) if (Math.Max(prevVelocity, currVelocity) != 0)
{ {
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities. // We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime; prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime; currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
// Scale with ratio of difference compared to 0.5 * max dist. // Scale with ratio of difference compared to 0.5 * max dist.
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// Reward for % distance slowed down compared to previous, paying attention to not award overlap // Reward for % distance slowed down compared to previous, paying attention to not award overlap
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity) double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap // do not award overlap
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2); * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
// Choose the largest bonus, multiplied by ratio. // Choose the largest bonus, multiplied by ratio.
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio; velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
} }
if (osuCurrObj.TravelTime != 0) if (osuLastObj.TravelTime != 0)
{ {
// Reward sliders based on velocity. // Reward sliders based on velocity.
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
} }
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for. // We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0); double stackNerf = Math.Min(1.0, (osuPrevious.LazyJumpDistance / scalingFactor) / 25.0);
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime; result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
} }

View File

@ -154,7 +154,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (strainTime < min_speed_bonus) if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.MovementDistance); double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
} }