1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 15:17:27 +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();
beatmap.Difficulty.CircleSize = 0;
var aimError = new AimError(events, beatmap);
var aimError = new AimError(events);
Assert.IsNotNull(aimError.Value);
Assert.AreEqual(Math.Sqrt(57) * 10, aimError.Value!.Value);
@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Tests
IBeatmap beatmap = new Beatmap();
beatmap.Difficulty.CircleSize = 0;
var aimErrorWithMiss = new AimError(eventsWithMiss, beatmap);
var aimErrorWithoutMiss = new AimError(eventsWithoutMiss, beatmap);
var aimErrorWithMiss = new AimError(eventsWithMiss);
var aimErrorWithoutMiss = new AimError(eventsWithoutMiss);
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();
beatmap.Difficulty.CircleSize = 0;
var aimError = new AimError(events, beatmap);
var aimError = new AimError(events);
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 AverageAimError(timedHitEvents),
new AimError(timedHitEvents, playableBeatmap)
new AimError(timedHitEvents)
}), 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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
namespace osu.Game.Rulesets.Osu.Statistics
{
@ -18,45 +13,14 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// </summary>
public partial class AimError : SimpleStatisticItem<double?>
{
private readonly List<Vector2> hitPoints = new List<Vector2>();
/// <summary>
/// Creates and computes an <see cref="AimError"/> statistic.
/// </summary>
/// <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, IBeatmap playableBeatmap)
public AimError(IEnumerable<HitEvent> hitEvents)
: base("Aim Error")
{
Value = calculateAimError(hitEvents, playableBeatmap);
}
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;
Value = hitEvents.CalculateAimError();
}
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");

View File

@ -3,8 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
@ -16,8 +15,6 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// </summary>
public partial class AverageAimError : SimpleStatisticItem<double?>
{
private readonly List<Vector2> hitPoints = new List<Vector2>();
/// <summary>
/// Creates and computes an <see cref="AverageHitError"/> statistic.
/// </summary>
@ -25,42 +22,9 @@ namespace osu.Game.Rulesets.Osu.Statistics
public AverageAimError(IEnumerable<HitEvent> hitEvents)
: base("Average Aim Error")
{
Value = calculateAverageAimError(hitEvents);
}
Vector2? offsetVector = hitEvents.CalculateAverageAimError();
private double? calculateAverageAimError(IEnumerable<HitEvent> hitEvents)
{
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);
Value = offsetVector?.Length;
}
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} osu! pixels from center";