mirror of
https://github.com/ppy/osu.git
synced 2026-05-23 11:40:28 +08:00
Merge pull request #32601 from sineplusx/median-offset
Use median instead of mean for average hit error calculations
This commit is contained in:
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
// 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
List<HitEvent> hitEvents =
|
||||
[
|
||||
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
];
|
||||
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
|
||||
}
|
||||
|
||||
foreach (var ev in hitEvents)
|
||||
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ namespace osu.Game.Configuration
|
||||
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.Count < 10)
|
||||
if (newScore.HitEvents.Count < 50)
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
|
||||
if (newScore.HitEvents.CalculateMedianHitError() is not double medianError)
|
||||
return;
|
||||
|
||||
// keep a sane maximum number of entries.
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Configuration
|
||||
averageHitErrorHistory.RemoveAt(0);
|
||||
|
||||
double globalOffset = configManager.Get<double>(OsuSetting.AudioOffset);
|
||||
averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset));
|
||||
averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset));
|
||||
}
|
||||
|
||||
public void ClearHistory() => averageHitErrorHistory.Clear();
|
||||
|
||||
@@ -55,20 +55,23 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// Calculates the median hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
|
||||
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
|
||||
/// </returns>
|
||||
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
public static double? CalculateMedianHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray();
|
||||
|
||||
if (timeOffsets.Length == 0)
|
||||
return null;
|
||||
|
||||
return timeOffsets.Average();
|
||||
int center = timeOffsets.Length / 2;
|
||||
|
||||
// Use average of the 2 central values if length is even
|
||||
return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
|
||||
}
|
||||
|
||||
public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
|
||||
|
||||
@@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
private double lastPlayAverage;
|
||||
private double lastPlayMedian;
|
||||
private double lastPlayBeatmapOffset;
|
||||
private HitEventTimingDistributionGraph? lastPlayGraph;
|
||||
|
||||
private SettingsButton? useAverageButton;
|
||||
private SettingsButton? calibrateFromLastPlayButton;
|
||||
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
var hitEvents = score.NewValue.HitEvents;
|
||||
|
||||
if (!(hitEvents.CalculateAverageHitError() is double average))
|
||||
if (!(hitEvents.CalculateMedianHitError() is double median))
|
||||
return;
|
||||
|
||||
referenceScoreContainer.Children = new Drawable[]
|
||||
@@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
// affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event,
|
||||
// i.e. an user input that the user had to *time to the track*,
|
||||
// i.e. one that it *makes sense to use* when doing anything with timing and offsets.
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10)
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50)
|
||||
{
|
||||
referenceScoreContainer.AddRange(new Drawable[]
|
||||
{
|
||||
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
return;
|
||||
}
|
||||
|
||||
lastPlayAverage = average;
|
||||
lastPlayMedian = median;
|
||||
lastPlayBeatmapOffset = Current.Value;
|
||||
|
||||
LinkFlowContainer globalOffsetText;
|
||||
@@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
Height = 50,
|
||||
},
|
||||
new AverageHitError(hitEvents),
|
||||
useAverageButton = new SettingsButton
|
||||
calibrateFromLastPlayButton = new SettingsButton
|
||||
{
|
||||
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
|
||||
Action = () =>
|
||||
@@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
if (Current.Disabled)
|
||||
return;
|
||||
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayMedian;
|
||||
lastAppliedScore.Value = ReferenceScore.Value;
|
||||
},
|
||||
},
|
||||
@@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
bool allow = allowOffsetAdjust;
|
||||
|
||||
if (useAverageButton != null)
|
||||
useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
if (calibrateFromLastPlayButton != null)
|
||||
calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
|
||||
Current.Disabled = !allow;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring;
|
||||
namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the unstable rate statistic for a given play.
|
||||
/// Displays the average hit error statistic for a given play.
|
||||
/// </summary>
|
||||
public partial class AverageHitError : SimpleStatisticItem<double?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and computes an <see cref="AverageHitError"/> statistic.
|
||||
/// </summary>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the average hit error based on.</param>
|
||||
public AverageHitError(IEnumerable<HitEvent> hitEvents)
|
||||
: base("Average Hit Error")
|
||||
{
|
||||
Value = hitEvents.CalculateAverageHitError();
|
||||
Value = hitEvents.CalculateMedianHitError();
|
||||
}
|
||||
|
||||
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
|
||||
|
||||
Reference in New Issue
Block a user