diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 487d81f35b..be73e915bc 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -333,7 +333,7 @@ namespace osu.Game.Rulesets.Osu
new StatisticItem("Aim Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
{
new AverageAimError(timedHitEvents),
- new AimError(timedHitEvents)
+ new AimError(timedHitEvents, playableBeatmap)
}), true),
};
}
diff --git a/osu.Game.Rulesets.Osu/Statistics/AimError.cs b/osu.Game.Rulesets.Osu/Statistics/AimError.cs
index 0d91fff1ad..238279319f 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AimError.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AimError.cs
@@ -4,6 +4,8 @@
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.Scoring;
using osu.Game.Screens.Ranking.Statistics;
@@ -21,47 +23,38 @@ namespace osu.Game.Rulesets.Osu.Statistics
///
/// Creates and computes an statistic.
///
- public AimError(IEnumerable hitEvents)
+ public AimError(IEnumerable hitEvents, IBeatmap playableBeatmap)
: base("Aim Error")
{
- Value = calculateAimError(hitEvents);
+ Value = calculateAimError(hitEvents, playableBeatmap);
}
- private double? calculateAimError(IEnumerable hitEvents)
+ private double? calculateAimError(IEnumerable hitEvents, IBeatmap playableBeatmap)
{
- IEnumerable rawHitPositions = hitEvents.Where(affectsAimError);
+ IEnumerable hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle));
- if (!rawHitPositions.Any())
+ double nonMissCount = hitCircleEvents.Count(e => e.Result.IsHit());
+ double missCount = hitCircleEvents.Count() - nonMissCount;
+
+ if (nonMissCount == 0)
return null;
- foreach (var e in hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
+ foreach (var e in hitCircleEvents)
{
- if (e.LastHitObject == null || e.Position == null)
+ if (e.Position == null)
continue;
- addAngleAdjustedPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value);
+ hitPoints.Add((e.Position - ((OsuHitObject)e.HitObject).StackedEndPosition).Value);
}
- Vector2 averagePosition = new Vector2(hitPoints.Sum(x => x[0]), hitPoints.Sum(x => x[1])) / hitEvents.Where(affectsAimError).Count();
+ double radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true);
- return Math.Sqrt(hitPoints.Average(x => (x - averagePosition).LengthSquared)) * 10;
+ // We don't get data for miss locations, so we estimate the total variance using the Rayleigh distribution.
+ double variance = (missCount * Math.Pow(radius, 2) + hitPoints.Aggregate(0.0, (current, point) => current + point.LengthSquared)) / (2 * nonMissCount);
+
+ return Math.Sqrt(variance) * 10;
}
- 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.Sin(finalAngle) * distanceFromCenter), (float)(Math.Cos(finalAngle) * distanceFromCenter));
-
- hitPoints.Add(angleAdjustedPoint);
- }
-
- private bool affectsAimError(HitEvent hitEvent) => hitEvent.HitObject is HitCircle && !(hitEvent.HitObject is SliderTailCircle) && hitEvent.Result.IsHit();
-
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");
}
}
diff --git a/osu.Game.Rulesets.Osu/Statistics/AverageAimError.cs b/osu.Game.Rulesets.Osu/Statistics/AverageAimError.cs
index e149c5f3b1..3d846337e3 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AverageAimError.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AverageAimError.cs
@@ -23,19 +23,21 @@ namespace osu.Game.Rulesets.Osu.Statistics
///
/// Sequence of s to calculate the unstable rate based on.
public AverageAimError(IEnumerable hitEvents)
- : base("Average Hit Error")
+ : base("Average Aim Error")
{
Value = calculateAverageAimError(hitEvents);
}
private double? calculateAverageAimError(IEnumerable hitEvents)
{
- IEnumerable rawHitPositions = hitEvents.Where(affectsAimError);
+ IEnumerable hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle) && e.Result.IsHit());
- if (!rawHitPositions.Any())
+ double nonMissCount = hitCircleEvents.Count(e => e.Result.IsHit());
+
+ if (nonMissCount == 0)
return null;
- foreach (var e in rawHitPositions)
+ foreach (var e in hitCircleEvents)
{
if (e.LastHitObject == null || e.Position == null)
continue;
@@ -43,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
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])) / rawHitPositions.Count();
+ Vector2 averagePosition = new Vector2(hitPoints.Sum(x => x[0]), hitPoints.Sum(x => x[1])) / (float)nonMissCount;
return averagePosition.Length;
}
@@ -61,8 +63,6 @@ namespace osu.Game.Rulesets.Osu.Statistics
hitPoints.Add(angleAdjustedPoint);
}
- private bool affectsAimError(HitEvent hitEvent) => hitEvent.HitObject is HitCircle && !(hitEvent.HitObject is SliderTailCircle) && hitEvent.Result.IsHit();
-
- protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} osu!px from center";
+ protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} osu! pixels from center";
}
}