mirror of
https://github.com/ppy/osu
synced 2025-01-19 04:20:59 +00:00
Merge pull request #20143 from acid-chicken/feat/stats/colored-td
Show judgement colours in hit distribution graph
This commit is contained in:
commit
096d1c3ff3
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
|
||||
{
|
||||
private HitEventTimingDistributionGraph graph;
|
||||
private HitEventTimingDistributionGraph graph = null!;
|
||||
|
||||
private static readonly HitObject placeholder_object = new HitCircle();
|
||||
|
||||
@ -43,6 +41,65 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSparse()
|
||||
{
|
||||
createTest(new List<HitEvent>
|
||||
{
|
||||
new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVariousTypesOfHitResult()
|
||||
{
|
||||
createTest(CreateDistributedHitEvents(0, 50).Select(h =>
|
||||
{
|
||||
double offset = Math.Abs(h.TimeOffset);
|
||||
HitResult result = offset > 36 ? HitResult.Miss
|
||||
: offset > 32 ? HitResult.Meh
|
||||
: offset > 24 ? HitResult.Ok
|
||||
: offset > 16 ? HitResult.Good
|
||||
: offset > 8 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleWindowsOfHitResult()
|
||||
{
|
||||
var wide = CreateDistributedHitEvents(0, 50).Select(h =>
|
||||
{
|
||||
double offset = Math.Abs(h.TimeOffset);
|
||||
HitResult result = offset > 36 ? HitResult.Miss
|
||||
: offset > 32 ? HitResult.Meh
|
||||
: offset > 24 ? HitResult.Ok
|
||||
: offset > 16 ? HitResult.Good
|
||||
: offset > 8 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
});
|
||||
var narrow = CreateDistributedHitEvents(0, 50).Select(h =>
|
||||
{
|
||||
double offset = Math.Abs(h.TimeOffset);
|
||||
HitResult result = offset > 25 ? HitResult.Miss
|
||||
: offset > 20 ? HitResult.Meh
|
||||
: offset > 15 ? HitResult.Ok
|
||||
: offset > 10 ? HitResult.Good
|
||||
: offset > 5 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
});
|
||||
createTest(wide.Concat(narrow).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroTimeOffset()
|
||||
{
|
||||
|
@ -102,26 +102,31 @@ namespace osu.Game.Graphics
|
||||
/// <summary>
|
||||
/// Retrieves the colour for a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
public Color4 ForHitResult(HitResult judgement)
|
||||
public Color4 ForHitResult(HitResult result)
|
||||
{
|
||||
switch (judgement)
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
case HitResult.Great:
|
||||
return Blue;
|
||||
|
||||
case HitResult.Ok:
|
||||
case HitResult.Good:
|
||||
return Green;
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Miss:
|
||||
return Red;
|
||||
|
||||
case HitResult.Meh:
|
||||
return Yellow;
|
||||
|
||||
case HitResult.Miss:
|
||||
return Red;
|
||||
case HitResult.Ok:
|
||||
return Green;
|
||||
|
||||
case HitResult.Good:
|
||||
return GreenLight;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.Great:
|
||||
return Blue;
|
||||
|
||||
default:
|
||||
return Color4.White;
|
||||
return BlueLight;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
#pragma warning disable CS0618
|
||||
public static class HitResultExtensions
|
||||
{
|
||||
private static readonly IList<HitResult> order = EnumExtensions.GetValuesInOrder<HitResult>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases the combo.
|
||||
/// </summary>
|
||||
@ -282,6 +284,13 @@ namespace osu.Game.Rulesets.Scoring
|
||||
Debug.Assert(minResult <= maxResult);
|
||||
return result > minResult && result < maxResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordered index of a <see cref="HitResult"/>. Used for consistent order when displaying hit results to the user.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
|
||||
/// <returns>The index of <paramref name="result"/>.</returns>
|
||||
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
@ -59,30 +59,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
protected Color4 GetColourForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Miss:
|
||||
return colours.Red;
|
||||
|
||||
case HitResult.Meh:
|
||||
return colours.Yellow;
|
||||
|
||||
case HitResult.Ok:
|
||||
return colours.Green;
|
||||
|
||||
case HitResult.Good:
|
||||
return colours.GreenLight;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.Great:
|
||||
return colours.Blue;
|
||||
|
||||
default:
|
||||
return colours.BlueLight;
|
||||
}
|
||||
return colours.ForHitResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -7,7 +7,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -57,7 +56,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
|
||||
}
|
||||
|
||||
private int[] bins;
|
||||
private IDictionary<HitResult, int>[] bins;
|
||||
private double binSize;
|
||||
private double hitOffset;
|
||||
|
||||
@ -69,7 +68,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
if (hitEvents == null || hitEvents.Count == 0)
|
||||
return;
|
||||
|
||||
bins = new int[total_timing_distribution_bins];
|
||||
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
|
||||
|
||||
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
|
||||
|
||||
@ -89,7 +88,8 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
bool roundUp = true;
|
||||
|
||||
Array.Clear(bins, 0, bins.Length);
|
||||
foreach (var bin in bins)
|
||||
bin.Clear();
|
||||
|
||||
foreach (var e in hitEvents)
|
||||
{
|
||||
@ -110,23 +110,23 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
// may be out of range when applying an offset. for such cases we can just drop the results.
|
||||
if (index >= 0 && index < bins.Length)
|
||||
bins[index]++;
|
||||
{
|
||||
bins[index].TryGetValue(e.Result, out int value);
|
||||
bins[index][e.Result] = ++value;
|
||||
}
|
||||
}
|
||||
|
||||
if (barDrawables != null)
|
||||
{
|
||||
for (int i = 0; i < barDrawables.Length; i++)
|
||||
{
|
||||
barDrawables[i].UpdateOffset(bins[i]);
|
||||
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int maxCount = bins.Max();
|
||||
barDrawables = new Bar[total_timing_distribution_bins];
|
||||
|
||||
for (int i = 0; i < barDrawables.Length; i++)
|
||||
barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index);
|
||||
int maxCount = bins.Max(b => b.Values.Sum());
|
||||
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
|
||||
|
||||
Container axisFlow;
|
||||
|
||||
@ -209,50 +209,97 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
|
||||
private class Bar : CompositeDrawable
|
||||
{
|
||||
private readonly float value;
|
||||
private readonly float maxValue;
|
||||
private float totalValue => values.Sum(v => v.Value);
|
||||
private float basalHeight => BoundingBox.Width / BoundingBox.Height;
|
||||
private float availableHeight => 1 - basalHeight;
|
||||
|
||||
private readonly Circle boxOriginal;
|
||||
private readonly IReadOnlyList<KeyValuePair<HitResult, int>> values;
|
||||
private readonly float maxValue;
|
||||
private readonly bool isCentre;
|
||||
|
||||
private Circle[] boxOriginals;
|
||||
private Circle boxAdjustment;
|
||||
|
||||
private const float minimum_height = 0.05f;
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public Bar(float value, float maxValue, bool isCentre)
|
||||
public Bar(IDictionary<HitResult, int> values, float maxValue, bool isCentre)
|
||||
{
|
||||
this.value = value;
|
||||
this.values = values.OrderBy(v => v.Key.GetIndexForOrderedDisplay()).ToList();
|
||||
this.maxValue = maxValue;
|
||||
this.isCentre = isCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (values.Any())
|
||||
{
|
||||
boxOriginal = new Circle
|
||||
boxOriginals = values.Select((v, i) => new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"),
|
||||
Height = minimum_height,
|
||||
},
|
||||
};
|
||||
Colour = isCentre && i == 0 ? Color4.White : colours.ForHitResult(v.Key),
|
||||
Height = 0,
|
||||
}).ToArray();
|
||||
// The bars of the stacked bar graph will be processed (stacked) from the bottom, which is the base position,
|
||||
// to the top, and the bottom bar should be drawn more toward the front by design,
|
||||
// while the drawing order is from the back to the front, so the order passed to `InternalChildren` is the opposite.
|
||||
InternalChildren = boxOriginals.Reverse().ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// A bin with no value draws a grey dot instead.
|
||||
InternalChildren = boxOriginals = new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = isCentre ? Color4.White : Color4.Gray,
|
||||
Height = 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private const double duration = 300;
|
||||
|
||||
private float offsetForValue(float value)
|
||||
{
|
||||
return availableHeight * value / maxValue;
|
||||
}
|
||||
|
||||
private float heightForValue(float value)
|
||||
{
|
||||
return basalHeight + offsetForValue(value);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
float height = Math.Clamp(value / maxValue, minimum_height, 1);
|
||||
foreach (var boxOriginal in boxOriginals)
|
||||
boxOriginal.Height = basalHeight;
|
||||
|
||||
if (height > minimum_height)
|
||||
boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint);
|
||||
float offsetValue = 0;
|
||||
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
|
||||
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
|
||||
offsetValue -= values[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateOffset(float adjustment)
|
||||
{
|
||||
bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height;
|
||||
bool hasAdjustment = adjustment != totalValue;
|
||||
|
||||
if (boxAdjustment == null)
|
||||
{
|
||||
@ -271,7 +318,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
||||
});
|
||||
}
|
||||
|
||||
boxAdjustment.ResizeHeightTo(Math.Clamp(adjustment / maxValue, minimum_height, 1), duration, Easing.OutQuint);
|
||||
boxAdjustment.ResizeHeightTo(heightForValue(adjustment), duration, Easing.OutQuint);
|
||||
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user