1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-28 20:47:22 +08:00

Merge pull request #9988 from bdach/unstable-rate

Add basic hit unstable rate display to results screen
This commit is contained in:
Dan Balasescu 2020-08-28 18:00:03 +09:00 committed by GitHub
commit 65759e1d1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 156 additions and 64 deletions

View File

@ -326,6 +326,16 @@ namespace osu.Game.Rulesets.Mania
Height = 250 Height = 250
}), }),
} }
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(score.HitEvents)
}))
}
} }
}; };
} }

View File

@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution",
Height = 250 new HitEventTimingDistributionGraph(timedHitEvents)
}), {
} RelativeSizeAxes = Axes.X,
}, Height = 250
new StatisticRow }),
{ }
Columns = new[] },
new StatisticRow
{ {
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View File

@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View File

@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneSimpleStatisticRow : OsuTestScene public class TestSceneSimpleStatisticTable : OsuTestScene
{ {
private Container container; private Container container;
@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking
public void TestEmpty() public void TestEmpty()
{ {
AddStep("create with no items", AddStep("create with no items",
() => container.Add(new SimpleStatisticRow(2, Enumerable.Empty<SimpleStatisticItem>()))); () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty<SimpleStatisticItem>())));
} }
[Test] [Test]
@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking
Value = RNG.Next(100) Value = RNG.Next(100)
}); });
container.Add(new SimpleStatisticRow(columnCount, items)); container.Add(new SimpleStatisticTable(columnCount, items));
}); });
} }
} }

View File

@ -41,13 +41,14 @@ namespace osu.Game.Screens.Ranking.Statistics
{ {
Text = Name, Text = Name,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14)
}, },
value = new OsuSpriteText value = new OsuSpriteText
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Font = OsuFont.Torus.With(weight: FontWeight.Bold) Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)
} }
}); });
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,10 +14,10 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Ranking.Statistics namespace osu.Game.Screens.Ranking.Statistics
{ {
/// <summary> /// <summary>
/// Represents a statistic row with simple statistics (ones that only need textual display). /// Represents a table with simple statistics (ones that only need textual display).
/// Richer visualisations should be done with <see cref="StatisticRow"/>s and <see cref="StatisticItem"/>s. /// Richer visualisations should be done with <see cref="StatisticRow"/>s and <see cref="StatisticItem"/>s.
/// </summary> /// </summary>
public class SimpleStatisticRow : CompositeDrawable public class SimpleStatisticTable : CompositeDrawable
{ {
private readonly SimpleStatisticItem[] items; private readonly SimpleStatisticItem[] items;
private readonly int columnCount; private readonly int columnCount;
@ -28,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary> /// </summary>
/// <param name="columnCount">The number of columns to layout the <paramref name="items"/> into.</param> /// <param name="columnCount">The number of columns to layout the <paramref name="items"/> into.</param>
/// <param name="items">The <see cref="SimpleStatisticItem"/>s to display in this row.</param> /// <param name="items">The <see cref="SimpleStatisticItem"/>s to display in this row.</param>
public SimpleStatisticRow(int columnCount, IEnumerable<SimpleStatisticItem> items) public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable<SimpleStatisticItem> items)
{ {
if (columnCount < 1) if (columnCount < 1)
throw new ArgumentOutOfRangeException(nameof(columnCount)); throw new ArgumentOutOfRangeException(nameof(columnCount));

View File

@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Content = new[] Content = new[]
{ {
new Drawable[] new[]
{ {
new FillFlowContainer createHeader(item)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 9,
Width = 4,
Colour = Color4Extensions.FromHex("#00FFAA")
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = item.Name,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
}
}
}
}, },
new Drawable[] new Drawable[]
{ {
@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics
} }
}; };
} }
private static Drawable createHeader(StatisticItem item)
{
if (string.IsNullOrEmpty(item.Name))
return Empty();
return new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 9,
Width = 4,
Colour = Color4Extensions.FromHex("#00FFAA")
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = item.Name,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
}
}
};
}
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <summary> /// <summary>
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen. /// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
/// </summary> /// </summary>
/// <param name="name">The name of the item.</param> /// <param name="name">The name of the item. Can be <see cref="string.Empty"/> to hide the item header.</param>
/// <param name="content">The <see cref="Drawable"/> content to be displayed.</param> /// <param name="content">The <see cref="Drawable"/> content to be displayed.</param>
/// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param> /// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param>
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)

View File

@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 15), Spacing = new Vector2(30, 15),
Alpha = 0
}; };
foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap))
{ {
rows.Add(new GridContainer rows.Add(new GridContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Content = new[] Content = new[]
@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Hide(); spinner.Hide();
content.Add(d); content.Add(d);
d.FadeIn(250, Easing.OutQuint);
}, localCancellationSource.Token); }, localCancellationSource.Token);
}), localCancellationSource.Token); }), localCancellationSource.Token);
} }

View File

@ -0,0 +1,39 @@
// 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.Scoring;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Displays the unstable rate statistic for a given play.
/// </summary>
public class UnstableRate : SimpleStatisticItem<double>
{
/// <summary>
/// Creates and computes an <see cref="UnstableRate"/> statistic.
/// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public UnstableRate(IEnumerable<HitEvent> hitEvents)
: base("Unstable Rate")
{
var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray();
Value = 10 * standardDeviation(timeOffsets);
}
private static double standardDeviation(double[] timeOffsets)
{
if (timeOffsets.Length == 0)
return double.NaN;
var mean = timeOffsets.Average();
var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum();
return Math.Sqrt(squares / timeOffsets.Length);
}
protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2");
}
}