mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 18:23:04 +08:00
Refactor to abstract out strain logic into StrainSkill class
While it is the case for the existing official Skills, Skill implementations shouldn't be required to conform to a strain based approach. There are other valid approaches to calculating skill difficulty that can be supported by abstracting the strain logic into its own StrainSkill class.
This commit is contained in:
parent
5b2dcea8a8
commit
85d2b1232a
@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||||
{
|
{
|
||||||
public class Movement : Skill
|
public class Movement : StrainSkill
|
||||||
{
|
{
|
||||||
private const float absolute_player_positioning_error = 16f;
|
private const float absolute_player_positioning_error = 16f;
|
||||||
private const float normalized_hitobject_radius = 41.0f;
|
private const float normalized_hitobject_radius = 41.0f;
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||||
{
|
{
|
||||||
public class Strain : Skill
|
public class Strain : StrainSkill
|
||||||
{
|
{
|
||||||
private const double individual_decay_base = 0.125;
|
private const double individual_decay_base = 0.125;
|
||||||
private const double overall_decay_base = 0.30;
|
private const double overall_decay_base = 0.30;
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Aim : Skill
|
public class Aim : StrainSkill
|
||||||
{
|
{
|
||||||
private const double angle_bonus_begin = Math.PI / 3;
|
private const double angle_bonus_begin = Math.PI / 3;
|
||||||
private const double timing_threshold = 107;
|
private const double timing_threshold = 107;
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : Skill
|
public class Speed : StrainSkill
|
||||||
{
|
{
|
||||||
private const double single_spacing_threshold = 125;
|
private const double single_spacing_threshold = 125;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the colour coefficient of taiko difficulty.
|
/// Calculates the colour coefficient of taiko difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Colour : Skill
|
public class Colour : StrainSkill
|
||||||
{
|
{
|
||||||
protected override double SkillMultiplier => 1;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.4;
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the rhythm coefficient of taiko difficulty.
|
/// Calculates the rhythm coefficient of taiko difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Rhythm : Skill
|
public class Rhythm : StrainSkill
|
||||||
{
|
{
|
||||||
protected override double SkillMultiplier => 10;
|
protected override double SkillMultiplier => 10;
|
||||||
protected override double StrainDecayBase => 0;
|
protected override double StrainDecayBase => 0;
|
||||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class Stamina : Skill
|
public class Stamina : StrainSkill
|
||||||
{
|
{
|
||||||
protected override double SkillMultiplier => 1;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.4;
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -11,140 +9,39 @@ using osu.Game.Rulesets.Mods;
|
|||||||
namespace osu.Game.Rulesets.Difficulty.Skills
|
namespace osu.Game.Rulesets.Difficulty.Skills
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
|
/// A bare minimal abstract skill for fully custom skill implementations.
|
||||||
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Skill
|
public abstract class Skill
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double SkillMultiplier { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines how quickly strain decays for the given skill.
|
|
||||||
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double StrainDecayBase { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The weight by which each strain value decays.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual double DecayWeight => 0.9;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
|
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current strain level.
|
|
||||||
/// </summary>
|
|
||||||
protected double CurrentStrain { get; private set; } = 1;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The length of each strain section.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual int SectionLength => 400;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mods for use in skill calculations.
|
/// Mods for use in skill calculations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected IReadOnlyList<Mod> Mods => mods;
|
protected IReadOnlyList<Mod> Mods => mods;
|
||||||
|
|
||||||
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
|
||||||
|
|
||||||
private double currentSectionEnd;
|
|
||||||
|
|
||||||
private readonly List<double> strainPeaks = new List<double>();
|
|
||||||
|
|
||||||
private readonly Mod[] mods;
|
private readonly Mod[] mods;
|
||||||
|
|
||||||
protected Skill(Mod[] mods)
|
protected Skill(Mod[] mods)
|
||||||
{
|
{
|
||||||
this.mods = mods;
|
this.mods = mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
|
/// Process a <see cref="DifficultyHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Process(DifficultyHitObject current)
|
/// <param name="current">The <see cref="DifficultyHitObject"/> to process.</param>
|
||||||
|
public virtual void Process(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
// The first object doesn't generate a strain, so we begin with an incremented section end
|
|
||||||
if (Previous.Count == 0)
|
|
||||||
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
|
||||||
|
|
||||||
while (current.StartTime > currentSectionEnd)
|
|
||||||
{
|
|
||||||
saveCurrentPeak();
|
|
||||||
startNewSectionFrom(currentSectionEnd);
|
|
||||||
currentSectionEnd += SectionLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentStrain *= strainDecay(current.DeltaTime);
|
|
||||||
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
|
|
||||||
|
|
||||||
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
|
|
||||||
|
|
||||||
Previous.Push(current);
|
Previous.Push(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
|
||||||
/// </summary>
|
|
||||||
private void saveCurrentPeak()
|
|
||||||
{
|
|
||||||
strainPeaks.Add(currentSectionPeak);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the initial strain level for a new section.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
|
||||||
private void startNewSectionFrom(double time)
|
|
||||||
{
|
|
||||||
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
|
|
||||||
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
|
||||||
currentSectionPeak = GetPeakStrain(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the peak strain at a point in time.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time">The time to retrieve the peak strain at, adjusted by clockrate.</param>
|
|
||||||
/// <returns>The peak strain.</returns>
|
|
||||||
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
|
|
||||||
/// including the peak of the current section.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<double> GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double DifficultyValue()
|
public abstract double DifficultyValue();
|
||||||
{
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
|
|
||||||
// Difficulty is the weighted sum of the highest strains from every section.
|
|
||||||
// We're sorting from highest to lowest strain.
|
|
||||||
foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
|
|
||||||
{
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= DecayWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract double StrainValueOf(DifficultyHitObject current);
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
137
osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
Normal file
137
osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// 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.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
|
||||||
|
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class StrainSkill : Skill
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract double SkillMultiplier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines how quickly strain decays for the given skill.
|
||||||
|
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract double StrainDecayBase { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The weight by which each strain value decays.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double DecayWeight => 0.9;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current strain level.
|
||||||
|
/// </summary>
|
||||||
|
protected double CurrentStrain { get; private set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of each strain section.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual int SectionLength => 400;
|
||||||
|
|
||||||
|
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
||||||
|
|
||||||
|
private double currentSectionEnd;
|
||||||
|
|
||||||
|
private readonly List<double> strainPeaks = new List<double>();
|
||||||
|
|
||||||
|
protected StrainSkill(Mod[] mods)
|
||||||
|
: base(mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
|
||||||
|
/// </summary>
|
||||||
|
public sealed override void Process(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
||||||
|
if (Previous.Count == 0)
|
||||||
|
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||||
|
|
||||||
|
while (current.StartTime > currentSectionEnd)
|
||||||
|
{
|
||||||
|
saveCurrentPeak();
|
||||||
|
startNewSectionFrom(currentSectionEnd);
|
||||||
|
currentSectionEnd += SectionLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentStrain *= strainDecay(current.DeltaTime);
|
||||||
|
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
|
||||||
|
|
||||||
|
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
|
||||||
|
|
||||||
|
base.Process(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
|
||||||
|
/// </summary>
|
||||||
|
private void saveCurrentPeak()
|
||||||
|
{
|
||||||
|
strainPeaks.Add(currentSectionPeak);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the initial strain level for a new section.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
||||||
|
private void startNewSectionFrom(double time)
|
||||||
|
{
|
||||||
|
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
|
||||||
|
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
||||||
|
currentSectionPeak = GetPeakStrain(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the peak strain at a point in time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time to retrieve the peak strain at.</param>
|
||||||
|
/// <returns>The peak strain.</returns>
|
||||||
|
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
|
||||||
|
/// including the peak of the current section.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<double> GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
||||||
|
/// </summary>
|
||||||
|
public sealed override double DifficultyValue()
|
||||||
|
{
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
// Difficulty is the weighted sum of the highest strains from every section.
|
||||||
|
// We're sorting from highest to lowest strain.
|
||||||
|
foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= DecayWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract double StrainValueOf(DifficultyHitObject current);
|
||||||
|
|
||||||
|
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user