1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 01:57:51 +08:00

Refactor, use miss locations instead of estimating miss variance

This commit is contained in:
Nathen 2024-02-22 10:57:18 -05:00
parent 4cc2e9124c
commit d87932a875
5 changed files with 99 additions and 83 deletions

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests
IBeatmap beatmap = new Beatmap(); IBeatmap beatmap = new Beatmap();
beatmap.Difficulty.CircleSize = 0; beatmap.Difficulty.CircleSize = 0;
var aimError = new AimError(events, beatmap); var aimError = new AimError(events);
Assert.IsNotNull(aimError.Value); Assert.IsNotNull(aimError.Value);
Assert.AreEqual(Math.Sqrt(57) * 10, aimError.Value!.Value); Assert.AreEqual(Math.Sqrt(57) * 10, aimError.Value!.Value);
@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Tests
IBeatmap beatmap = new Beatmap(); IBeatmap beatmap = new Beatmap();
beatmap.Difficulty.CircleSize = 0; beatmap.Difficulty.CircleSize = 0;
var aimErrorWithMiss = new AimError(eventsWithMiss, beatmap); var aimErrorWithMiss = new AimError(eventsWithMiss);
var aimErrorWithoutMiss = new AimError(eventsWithoutMiss, beatmap); var aimErrorWithoutMiss = new AimError(eventsWithoutMiss);
Assert.IsTrue(aimErrorWithMiss.Value != null && aimErrorWithoutMiss.Value != null && Precision.DefinitelyBigger(aimErrorWithMiss.Value.Value, aimErrorWithoutMiss.Value.Value)); Assert.IsTrue(aimErrorWithMiss.Value != null && aimErrorWithoutMiss.Value != null && Precision.DefinitelyBigger(aimErrorWithMiss.Value.Value, aimErrorWithoutMiss.Value.Value));
} }
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests
IBeatmap beatmap = new Beatmap(); IBeatmap beatmap = new Beatmap();
beatmap.Difficulty.CircleSize = 0; beatmap.Difficulty.CircleSize = 0;
var aimError = new AimError(events, beatmap); var aimError = new AimError(events);
Assert.IsNull(aimError.Value); Assert.IsNull(aimError.Value);
} }

View File

@ -337,7 +337,7 @@ namespace osu.Game.Rulesets.Osu
new StatisticItem("Aim Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] new StatisticItem("Aim Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
{ {
new AverageAimError(timedHitEvents), new AverageAimError(timedHitEvents),
new AimError(timedHitEvents, playableBeatmap) new AimError(timedHitEvents)
}), true), }), true),
}; };
} }

View File

@ -0,0 +1,88 @@
// 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.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Scoring
{
public static class OsuHitEventExtensions
{
public static double? CalculateAimError(this IEnumerable<HitEvent> hitEvents)
{
IEnumerable<HitEvent> hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
Vector2 averageHitError = hitCircleEvents.CalculateAverageAimError()!.Value;
int eventCount = 0;
double varianceSum = 0;
foreach (var e in hitCircleEvents)
{
if (e.Position == null)
continue;
eventCount += 1;
varianceSum += (e.CalcAngleAdjustedPoint() - averageHitError ?? new Vector2(0, 0)).LengthSquared;
}
if (eventCount == 0)
return null;
// We don't get data for miss locations, so we estimate the total variance using the Rayleigh distribution.
// Deriving the Rayleigh distribution in this form results in a 2 in the denominator,
// but it is removed to take the variance across both axes, instead of across just one.
double variance = varianceSum / eventCount;
return Math.Sqrt(variance) * 10;
}
public static Vector2? CalculateAverageAimError(this IEnumerable<HitEvent> hitEvents)
{
IEnumerable<HitEvent> hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
int eventCount = 0;
Vector2 sumOfPointVectors = new Vector2(0, 0);
foreach (var e in hitCircleEvents)
{
if (e.LastHitObject == null || e.Position == null)
continue;
eventCount += 1;
sumOfPointVectors += CalcAngleAdjustedPoint(e) ?? new Vector2(0, 0);
}
if (eventCount == 0)
return null;
Vector2 averagePosition = sumOfPointVectors / eventCount;
return averagePosition;
}
public static Vector2? CalcAngleAdjustedPoint(this HitEvent hitEvent)
{
if (hitEvent.LastHitObject is null || hitEvent.Position is null)
return null;
Vector2 start = ((OsuHitObject)hitEvent.LastHitObject!).StackedEndPosition;
Vector2 end = ((OsuHitObject)hitEvent.HitObject).StackedEndPosition;
Vector2 hitPoint = hitEvent.Position!.Value;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
double distanceFromCenter = (hitPoint - end).Length;
Vector2 angleAdjustedPoint = new Vector2((float)(Math.Cos(finalAngle) * distanceFromCenter), (float)(Math.Sin(finalAngle) * distanceFromCenter));
return angleAdjustedPoint;
}
}
}

View File

@ -1,15 +1,10 @@
// 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.Osu.Scoring;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osuTK;
namespace osu.Game.Rulesets.Osu.Statistics namespace osu.Game.Rulesets.Osu.Statistics
{ {
@ -18,45 +13,14 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// </summary> /// </summary>
public partial class AimError : SimpleStatisticItem<double?> public partial class AimError : SimpleStatisticItem<double?>
{ {
private readonly List<Vector2> hitPoints = new List<Vector2>();
/// <summary> /// <summary>
/// Creates and computes an <see cref="AimError"/> statistic. /// Creates and computes an <see cref="AimError"/> statistic.
/// </summary> /// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the aim error based on.</param> /// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the aim error based on.</param>
/// <param name="playableBeatmap">The <see cref="IBeatmap"/> containing the radii of the circles, used to compute the variance of misses.</param> public AimError(IEnumerable<HitEvent> hitEvents)
public AimError(IEnumerable<HitEvent> hitEvents, IBeatmap playableBeatmap)
: base("Aim Error") : base("Aim Error")
{ {
Value = calculateAimError(hitEvents, playableBeatmap); Value = hitEvents.CalculateAimError();
}
private double? calculateAimError(IEnumerable<HitEvent> hitEvents, IBeatmap playableBeatmap)
{
IEnumerable<HitEvent> hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
double nonMissCount = hitCircleEvents.Count(e => e.Result.IsHit());
double missCount = hitCircleEvents.Count() - nonMissCount;
if (nonMissCount == 0)
return null;
foreach (var e in hitCircleEvents)
{
if (e.Position == null)
continue;
hitPoints.Add((e.Position - ((OsuHitObject)e.HitObject).StackedEndPosition).Value);
}
double radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true);
// We don't get data for miss locations, so we estimate the total variance using the Rayleigh distribution.
// Deriving the Rayleigh distribution in this form results in a 2 in the denominator,
// but it is removed to take the variance across both axes, instead of across just one.
double variance = (missCount * Math.Pow(radius, 2) + hitPoints.Sum(point => point.LengthSquared)) / nonMissCount;
return Math.Sqrt(variance) * 10;
} }
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");

View File

@ -3,8 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osuTK; using osuTK;
@ -16,8 +15,6 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// </summary> /// </summary>
public partial class AverageAimError : SimpleStatisticItem<double?> public partial class AverageAimError : SimpleStatisticItem<double?>
{ {
private readonly List<Vector2> hitPoints = new List<Vector2>();
/// <summary> /// <summary>
/// Creates and computes an <see cref="AverageHitError"/> statistic. /// Creates and computes an <see cref="AverageHitError"/> statistic.
/// </summary> /// </summary>
@ -25,42 +22,9 @@ namespace osu.Game.Rulesets.Osu.Statistics
public AverageAimError(IEnumerable<HitEvent> hitEvents) public AverageAimError(IEnumerable<HitEvent> hitEvents)
: base("Average Aim Error") : base("Average Aim Error")
{ {
Value = calculateAverageAimError(hitEvents); Vector2? offsetVector = hitEvents.CalculateAverageAimError();
}
private double? calculateAverageAimError(IEnumerable<HitEvent> hitEvents) Value = offsetVector?.Length;
{
IEnumerable<HitEvent> hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle) && e.Result.IsHit()).ToList();
double nonMissCount = hitCircleEvents.Count(e => e.Result.IsHit());
if (nonMissCount == 0)
return null;
foreach (var e in hitCircleEvents)
{
if (e.LastHitObject == null || e.Position == null)
continue;
addAngleAdjustedPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value);
}
Vector2 averagePosition = new Vector2(hitPoints.Sum(x => x[0]), hitPoints.Sum(x => x[1])) / (float)nonMissCount;
return averagePosition.Length;
}
private void addAngleAdjustedPoint(Vector2 start, Vector2 end, Vector2 hitPoint)
{
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
double distanceFromCenter = (hitPoint - end).Length;
Vector2 angleAdjustedPoint = new Vector2((float)(Math.Cos(finalAngle) * distanceFromCenter), (float)(Math.Sin(finalAngle) * distanceFromCenter));
hitPoints.Add(angleAdjustedPoint);
} }
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} osu! pixels from center"; protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} osu! pixels from center";