From 49ddc4526fbe4eaa1340af7c02146a9aff5cf5d8 Mon Sep 17 00:00:00 2001 From: nathen Date: Tue, 23 Jan 2024 20:40:32 -0500 Subject: [PATCH] Approximate miss error using the rayleigh distribution --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Osu/Statistics/AimError.cs | 43 ++++++++----------- .../Statistics/AverageAimError.cs | 16 +++---- 3 files changed, 27 insertions(+), 34 deletions(-) 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"; } }