mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:20:04 +08:00
Rework score processor to provide more generic events
This commit is contained in:
parent
6c8a24260b
commit
20db5b33ab
@ -29,6 +29,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
@ -200,7 +201,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 130,
|
||||
Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution"))
|
||||
Child = new TimingDistributionGraph(score.HitEvents.Cast<HitEvent>().ToList())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
@ -208,7 +209,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new StatisticContainer("Accuracy Heatmap")
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Heatmap((List<HitOffset>)score.ExtraStatistics.GetValueOrDefault("hit_offsets"))
|
||||
Child = new Heatmap(score.Beatmap, score.HitEvents.Cast<HitEvent>().ToList())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
|
@ -1,120 +1,52 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Game.Beatmaps;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreProcessor : ScoreProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of bins on each side of the timing distribution.
|
||||
/// </summary>
|
||||
private const int timing_distribution_bins = 25;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0.
|
||||
/// </summary>
|
||||
private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1;
|
||||
|
||||
/// <summary>
|
||||
/// The centre bin, with a timing distribution very close to/at 0.
|
||||
/// </summary>
|
||||
private const int timing_distribution_centre_bin_index = timing_distribution_bins;
|
||||
|
||||
private TimingDistribution timingDistribution;
|
||||
private readonly List<HitOffset> hitOffsets = new List<HitOffset>();
|
||||
|
||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var hitWindows = CreateHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
|
||||
timingDistribution = new TimingDistribution(total_timing_distribution_bins, hitWindows.WindowFor(hitWindows.LowestSuccessfulHitResult()) / timing_distribution_bins);
|
||||
|
||||
base.ApplyBeatmap(beatmap);
|
||||
}
|
||||
|
||||
private OsuHitCircleJudgementResult lastCircleResult;
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
private HitObject lastHitObject;
|
||||
|
||||
protected override void OnResultApplied(JudgementResult result)
|
||||
{
|
||||
base.OnResultApplied(result);
|
||||
|
||||
if (result.IsHit)
|
||||
{
|
||||
int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize);
|
||||
timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++;
|
||||
|
||||
addHitOffset(result);
|
||||
}
|
||||
hitEvents.Add(new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, (result as OsuHitCircleJudgementResult)?.HitPosition));
|
||||
lastHitObject = result.HitObject;
|
||||
}
|
||||
|
||||
protected override void OnResultReverted(JudgementResult result)
|
||||
{
|
||||
base.OnResultReverted(result);
|
||||
|
||||
if (result.IsHit)
|
||||
{
|
||||
int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize);
|
||||
timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--;
|
||||
|
||||
removeHitOffset(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void addHitOffset(JudgementResult result)
|
||||
{
|
||||
if (!(result is OsuHitCircleJudgementResult circleResult))
|
||||
return;
|
||||
|
||||
if (lastCircleResult == null)
|
||||
{
|
||||
lastCircleResult = circleResult;
|
||||
return;
|
||||
}
|
||||
|
||||
if (circleResult.HitPosition != null)
|
||||
{
|
||||
Debug.Assert(circleResult.Radius != null);
|
||||
hitOffsets.Add(new HitOffset(lastCircleResult.HitCircle.StackedEndPosition, circleResult.HitCircle.StackedEndPosition, circleResult.HitPosition.Value, circleResult.Radius.Value));
|
||||
}
|
||||
|
||||
lastCircleResult = circleResult;
|
||||
}
|
||||
|
||||
private void removeHitOffset(JudgementResult result)
|
||||
{
|
||||
if (!(result is OsuHitCircleJudgementResult circleResult))
|
||||
return;
|
||||
|
||||
if (hitOffsets.Count > 0 && circleResult.HitPosition != null)
|
||||
hitOffsets.RemoveAt(hitOffsets.Count - 1);
|
||||
hitEvents.RemoveAt(hitEvents.Count - 1);
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
timingDistribution.Bins.AsSpan().Clear();
|
||||
hitOffsets.Clear();
|
||||
hitEvents.Clear();
|
||||
lastHitObject = null;
|
||||
}
|
||||
|
||||
public override void PopulateScore(ScoreInfo score)
|
||||
{
|
||||
base.PopulateScore(score);
|
||||
|
||||
score.ExtraStatistics["timing_distribution"] = timingDistribution;
|
||||
score.ExtraStatistics["hit_offsets"] = hitOffsets;
|
||||
score.HitEvents.AddRange(hitEvents.Select(e => e).Cast<object>());
|
||||
}
|
||||
|
||||
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
|
||||
@ -131,4 +63,42 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
|
||||
public override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||
}
|
||||
|
||||
public readonly struct HitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The time offset from the end of <see cref="HitObject"/> at which the event occurred.
|
||||
/// </summary>
|
||||
public readonly double TimeOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The hit result.
|
||||
/// </summary>
|
||||
public readonly HitResult Result;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> on which the result occurred.
|
||||
/// </summary>
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> occurring prior to <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public readonly HitObject LastHitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The player's cursor position, if available, at the time of the event.
|
||||
/// </summary>
|
||||
public readonly Vector2? CursorPosition;
|
||||
|
||||
public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? cursorPosition)
|
||||
{
|
||||
TimeOffset = timeOffset;
|
||||
Result = result;
|
||||
HitObject = hitObject;
|
||||
LastHitObject = lastHitObject;
|
||||
CursorPosition = cursorPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -27,14 +29,16 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
private const float rotation = 45;
|
||||
private const float point_size = 4;
|
||||
|
||||
private readonly IReadOnlyList<HitOffset> offsets;
|
||||
private Container<HitPoint> allPoints;
|
||||
|
||||
private readonly BeatmapInfo beatmap;
|
||||
private readonly IReadOnlyList<HitEvent> hitEvents;
|
||||
private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
public Heatmap(IReadOnlyList<HitOffset> offsets)
|
||||
public Heatmap(BeatmapInfo beatmap, IReadOnlyList<HitEvent> hitEvents)
|
||||
{
|
||||
this.offsets = offsets;
|
||||
this.beatmap = beatmap;
|
||||
this.hitEvents = hitEvents;
|
||||
|
||||
AddLayout(sizeLayout);
|
||||
}
|
||||
@ -153,10 +157,18 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
}
|
||||
}
|
||||
|
||||
if (offsets?.Count > 0)
|
||||
if (hitEvents.Count > 0)
|
||||
{
|
||||
foreach (var o in offsets)
|
||||
AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius);
|
||||
// Todo: This should probably not be done like this.
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2;
|
||||
|
||||
foreach (var e in hitEvents)
|
||||
{
|
||||
if (e.LastHitObject == null || e.CursorPosition == null)
|
||||
continue;
|
||||
|
||||
AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius);
|
||||
}
|
||||
}
|
||||
|
||||
sizeLayout.Validate();
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -15,29 +17,52 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
{
|
||||
public class TimingDistributionGraph : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of bins on each side of the timing distribution.
|
||||
/// </summary>
|
||||
private const int timing_distribution_bins = 25;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0.
|
||||
/// </summary>
|
||||
private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1;
|
||||
|
||||
/// <summary>
|
||||
/// The centre bin, with a timing distribution very close to/at 0.
|
||||
/// </summary>
|
||||
private const int timing_distribution_centre_bin_index = timing_distribution_bins;
|
||||
|
||||
/// <summary>
|
||||
/// The number of data points shown on the axis below the graph.
|
||||
/// </summary>
|
||||
private const float axis_points = 5;
|
||||
|
||||
private readonly TimingDistribution distribution;
|
||||
private readonly List<HitEvent> hitEvents;
|
||||
|
||||
public TimingDistributionGraph(TimingDistribution distribution)
|
||||
public TimingDistributionGraph(List<HitEvent> hitEvents)
|
||||
{
|
||||
this.distribution = distribution;
|
||||
this.hitEvents = hitEvents;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (distribution?.Bins == null || distribution.Bins.Length == 0)
|
||||
if (hitEvents.Count == 0)
|
||||
return;
|
||||
|
||||
int maxCount = distribution.Bins.Max();
|
||||
int[] bins = new int[total_timing_distribution_bins];
|
||||
double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins;
|
||||
|
||||
var bars = new Drawable[distribution.Bins.Length];
|
||||
foreach (var e in hitEvents)
|
||||
{
|
||||
int binOffset = (int)(e.TimeOffset / binSize);
|
||||
bins[timing_distribution_centre_bin_index + binOffset]++;
|
||||
}
|
||||
|
||||
int maxCount = bins.Max();
|
||||
var bars = new Drawable[total_timing_distribution_bins];
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount };
|
||||
bars[i] = new Bar { Height = (float)bins[i] / maxCount };
|
||||
|
||||
Container axisFlow;
|
||||
|
||||
@ -71,10 +96,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
}
|
||||
};
|
||||
|
||||
// We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin.
|
||||
// So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
|
||||
int sideBins = (distribution.Bins.Length - 1) / 2;
|
||||
double maxValue = sideBins * distribution.BinSize;
|
||||
// Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
|
||||
double maxValue = timing_distribution_bins * binSize;
|
||||
double axisValueStep = maxValue / axis_points;
|
||||
|
||||
axisFlow.Add(new OsuSpriteText
|
||||
|
@ -8,8 +8,11 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
Position = new Vector2(500, 300),
|
||||
},
|
||||
heatmap = new TestHeatmap(new List<HitOffset>())
|
||||
heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List<HitEvent>())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -73,8 +76,8 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
private class TestHeatmap : Heatmap
|
||||
{
|
||||
public TestHeatmap(IReadOnlyList<HitOffset> offsets)
|
||||
: base(offsets)
|
||||
public TestHeatmap(BeatmapInfo beatmap, List<HitEvent> events)
|
||||
: base(beatmap, events)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
@ -17,11 +16,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
ExtraStatistics =
|
||||
{
|
||||
["timing_distribution"] = TestSceneTimingDistributionGraph.CreateNormalDistribution(),
|
||||
["hit_offsets"] = new List<HitOffset>()
|
||||
}
|
||||
HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents().Cast<object>().ToList(),
|
||||
};
|
||||
|
||||
loadPanel(score);
|
||||
|
@ -1,12 +1,15 @@
|
||||
// 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 osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Statistics;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
@ -22,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#333")
|
||||
},
|
||||
new TimingDistributionGraph(CreateNormalDistribution())
|
||||
new TimingDistributionGraph(CreateDistributedHitEvents())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -31,34 +34,19 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
};
|
||||
}
|
||||
|
||||
public static TimingDistribution CreateNormalDistribution()
|
||||
public static List<HitEvent> CreateDistributedHitEvents()
|
||||
{
|
||||
var distribution = new TimingDistribution(51, 5);
|
||||
var hitEvents = new List<HitEvent>();
|
||||
|
||||
// We create an approximately-normal distribution of 51 elements by using the 13th binomial row (14 initial elements) and subdividing the inner values twice.
|
||||
var row = new List<int> { 1 };
|
||||
for (int i = 0; i < 13; i++)
|
||||
row.Add(row[i] * (13 - i) / (i + 1));
|
||||
|
||||
// Each subdivision yields 2n-1 total elements, so first subdivision will contain 27 elements, and the second will contain 53 elements.
|
||||
for (int div = 0; div < 2; div++)
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var newRow = new List<int> { 1 };
|
||||
int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
|
||||
|
||||
for (int i = 0; i < row.Count - 1; i++)
|
||||
{
|
||||
newRow.Add((row[i] + row[i + 1]) / 2);
|
||||
newRow.Add(row[i + 1]);
|
||||
}
|
||||
|
||||
row = newRow;
|
||||
for (int j = 0; j < count; j++)
|
||||
hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
|
||||
}
|
||||
|
||||
// After the subdivisions take place, we're left with 53 values which we use the inner 51 of.
|
||||
for (int i = 1; i < row.Count - 1; i++)
|
||||
distribution.Bins[i - 1] = row[i];
|
||||
|
||||
return distribution;
|
||||
return hitEvents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,9 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object> ExtraStatistics = new Dictionary<string, object>();
|
||||
[NotMapped]
|
||||
[JsonIgnore]
|
||||
public List<object> HitEvents = new List<object>();
|
||||
|
||||
[JsonIgnore]
|
||||
public List<ScoreFileInfo> Files { get; set; }
|
||||
|
Loading…
Reference in New Issue
Block a user