mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 01:23:44 +08:00
Refactor, use miss locations instead of estimating miss variance
This commit is contained in:
parent
4cc2e9124c
commit
d87932a875
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
88
osu.Game.Rulesets.Osu/Scoring/OsuHitEventExtensions.cs
Normal file
88
osu.Game.Rulesets.Osu/Scoring/OsuHitEventExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user