1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 06:02:55 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

358 lines
15 KiB
C#
Raw Normal View History

2020-06-15 20:48:59 +08:00
// 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.Diagnostics;
2020-06-19 19:31:52 +08:00
using System.Linq;
2020-06-15 20:48:59 +08:00
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
2024-02-06 21:23:30 +08:00
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
2023-07-07 17:16:55 +08:00
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Objects;
2020-06-19 18:58:35 +08:00
using osu.Game.Scoring;
2020-06-15 20:48:59 +08:00
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Statistics
{
public partial class AccuracyHeatmap : CompositeDrawable
2020-06-15 20:48:59 +08:00
{
/// <summary>
/// Size of the inner circle containing the "hit" points, relative to the size of this <see cref="AccuracyHeatmap"/>.
2020-06-15 20:48:59 +08:00
/// All other points outside of the inner circle are "miss" points.
/// </summary>
private const float inner_portion = 0.8f;
/// <summary>
/// Number of rows/columns of points.
/// ~4px per point @ 128x128 size (the contents of the <see cref="AccuracyHeatmap"/> are always square). 1089 total points.
/// </summary>
private const int points_per_dimension = 33;
2020-06-15 20:48:59 +08:00
private const float rotation = 45;
2023-05-17 14:03:59 +08:00
private BufferedContainer bufferedGrid = null!;
private GridContainer pointGrid = null!;
2020-06-15 20:48:59 +08:00
2020-06-19 18:58:35 +08:00
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
2020-06-16 15:31:02 +08:00
2020-06-25 17:44:04 +08:00
private const float line_thickness = 2;
/// <summary>
/// The highest count of any point currently being displayed.
/// </summary>
protected float PeakValue { get; private set; }
public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
2020-06-15 20:48:59 +08:00
{
2020-06-19 18:58:35 +08:00
this.score = score;
this.playableBeatmap = playableBeatmap;
2020-06-15 20:48:59 +08:00
}
[BackgroundDependencyLoader]
private void load()
{
2023-05-23 15:21:56 +08:00
const float line_extension = 0.2f;
2020-06-16 15:31:02 +08:00
InternalChild = new Container
2020-06-15 20:48:59 +08:00
{
2020-06-16 15:31:02 +08:00
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Children = new Drawable[]
2020-06-15 20:48:59 +08:00
{
2020-06-16 15:31:02 +08:00
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
2020-06-15 20:48:59 +08:00
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
2020-06-22 20:09:47 +08:00
new Container
2020-06-16 15:31:02 +08:00
{
2020-06-22 20:09:47 +08:00
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Rotation = rotation,
2020-06-22 20:09:47 +08:00
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
2023-05-23 15:21:56 +08:00
new Circle
2020-06-22 20:09:47 +08:00
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
2023-05-23 15:21:56 +08:00
Width = line_thickness,
Height = inner_portion + line_extension,
Rotation = -rotation * 2,
2023-05-23 15:21:56 +08:00
Alpha = 0.6f,
2020-06-22 20:09:47 +08:00
},
2023-05-23 15:21:56 +08:00
new Circle
2020-06-22 20:09:47 +08:00
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
2023-05-23 15:21:56 +08:00
Width = line_thickness,
Height = inner_portion + line_extension,
},
new OsuSpriteText
{
2023-05-23 15:21:56 +08:00
Text = "Overshoot",
2023-07-07 17:16:55 +08:00
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
2023-07-07 17:16:55 +08:00
Origin = Anchor.BottomLeft,
Padding = new MarginPadding(2),
Rotation = -rotation,
RelativePositionAxes = Axes.Both,
2023-05-23 15:21:56 +08:00
Y = -(inner_portion + line_extension) / 2,
},
new OsuSpriteText
{
2023-05-23 15:21:56 +08:00
Text = "Undershoot",
2023-07-07 17:16:55 +08:00
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
2023-07-07 17:16:55 +08:00
Origin = Anchor.TopRight,
Rotation = -rotation,
Padding = new MarginPadding(2),
RelativePositionAxes = Axes.Both,
2023-05-23 15:21:56 +08:00
Y = (inner_portion + line_extension) / 2,
},
2023-05-23 15:21:56 +08:00
new Circle
{
Anchor = Anchor.Centre,
2023-05-23 15:21:56 +08:00
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
2023-05-23 15:21:56 +08:00
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = 45,
},
2023-05-23 15:21:56 +08:00
new Circle
{
Anchor = Anchor.Centre,
2023-05-23 15:21:56 +08:00
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.Both,
2023-05-23 15:21:56 +08:00
Y = -(inner_portion + line_extension) / 2,
Margin = new MarginPadding(-line_thickness / 2),
Width = line_thickness,
Height = 10,
Rotation = -45,
}
2020-06-22 20:09:47 +08:00
}
},
2020-06-16 15:31:02 +08:00
},
2020-06-15 20:48:59 +08:00
}
2020-06-16 15:31:02 +08:00
},
bufferedGrid = new BufferedContainer(cachedFrameBuffer: true)
2020-06-16 15:31:02 +08:00
{
RelativeSizeAxes = Axes.Both,
BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0),
Child = pointGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both
}
},
2020-06-16 15:31:02 +08:00
}
2020-06-15 20:48:59 +08:00
};
2020-06-16 15:31:02 +08:00
Vector2 centre = new Vector2(points_per_dimension) / 2;
float innerRadius = centre.X * inner_portion;
2020-06-15 20:48:59 +08:00
Drawable[][] points = new Drawable[points_per_dimension][];
2020-06-15 20:48:59 +08:00
for (int r = 0; r < points_per_dimension; r++)
2020-06-15 20:48:59 +08:00
{
points[r] = new Drawable[points_per_dimension];
2020-06-15 20:48:59 +08:00
for (int c = 0; c < points_per_dimension; c++)
{
2024-02-06 21:23:30 +08:00
bool isHit = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius;
2020-06-15 20:48:59 +08:00
2024-02-06 21:23:30 +08:00
if (isHit)
2020-06-15 20:48:59 +08:00
{
2024-02-06 21:23:30 +08:00
points[r][c] = new HitPoint(this)
{
BaseColour = new Color4(102, 255, 204, 255)
};
}
else
{
points[r][c] = new MissPoint
{
BaseColour = new Color4(255, 102, 102, 255)
};
}
2020-06-15 20:48:59 +08:00
}
}
pointGrid.Content = points;
2022-01-12 21:34:07 +08:00
if (score.HitEvents.Count == 0)
2020-06-19 18:58:35 +08:00
return;
float radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true);
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
2020-06-19 18:58:35 +08:00
{
2020-06-22 18:04:51 +08:00
if (e.LastHitObject == null || e.Position == null)
2020-06-19 18:58:35 +08:00
continue;
2020-06-22 18:04:51 +08:00
AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);
}
2020-06-15 20:48:59 +08:00
}
2020-06-16 15:31:02 +08:00
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
2020-06-15 20:48:59 +08:00
{
2020-09-10 17:09:03 +08:00
if (pointGrid.Content.Count == 0)
2020-06-16 15:31:02 +08:00
return;
2020-06-15 20:48:59 +08:00
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.
float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
2020-06-22 17:05:21 +08:00
// Consider two objects placed horizontally, with the start on the left and the end on the right.
// The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
// +pi | 0
// O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi)
// -pi | 0
// E.g. If the hit point was directly above end, it would have an angle pi/2.
//
// It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form:
// 0 | pi
// O --------- O ----->
// 2pi | pi
//
// However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted.
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
//
// We also need to apply the anti-clockwise rotation.
2024-03-06 10:42:20 +08:00
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
2020-06-22 17:05:21 +08:00
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance;
2020-06-22 17:05:21 +08:00
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
2020-06-16 15:31:02 +08:00
2020-06-15 20:48:59 +08:00
// Find the most relevant hit point.
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
2020-06-15 20:48:59 +08:00
2024-02-06 21:23:30 +08:00
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
2020-06-15 20:48:59 +08:00
}
2024-02-06 21:23:30 +08:00
private abstract partial class GridPoint : CompositeDrawable
2020-06-15 20:48:59 +08:00
{
/// <summary>
/// The base colour which will be lightened/darkened depending on the value of this <see cref="HitPoint"/>.
/// </summary>
public Color4 BaseColour;
2024-02-06 21:23:30 +08:00
public override bool IsPresent => Count > 0;
2024-02-06 21:23:30 +08:00
protected int Count { get; private set; }
2020-06-15 20:48:59 +08:00
2024-02-06 21:23:30 +08:00
/// <summary>
/// Increment the value of this point by one.
/// </summary>
/// <returns>The value after incrementing.</returns>
public int Increment()
2020-06-15 20:48:59 +08:00
{
2024-02-06 21:23:30 +08:00
return ++Count;
}
}
2020-06-15 20:48:59 +08:00
2024-02-06 21:23:30 +08:00
private partial class MissPoint : GridPoint
{
public MissPoint()
{
RelativeSizeAxes = Axes.Both;
2024-02-06 21:23:30 +08:00
InternalChild = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.Times
};
}
protected override void Update()
{
Alpha = 0.8f;
Colour = BaseColour;
}
2024-02-06 21:23:30 +08:00
}
2024-02-06 21:23:30 +08:00
private partial class HitPoint : GridPoint
{
private readonly AccuracyHeatmap heatmap;
2024-02-06 21:23:30 +08:00
public HitPoint(AccuracyHeatmap heatmap)
{
2024-02-06 21:23:30 +08:00
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
InternalChild = new Circle { RelativeSizeAxes = Axes.Both };
2020-06-15 20:48:59 +08:00
}
protected override void Update()
2020-06-15 20:48:59 +08:00
{
base.Update();
2024-02-06 21:23:30 +08:00
// the point at which alpha is saturated and we begin to adjust colour lightness.
const float lighten_cutoff = 0.95f;
2024-02-06 21:23:30 +08:00
// the amount of lightness to attribute regardless of relative value to peak point.
const float non_relative_portion = 0.2f;
2024-02-06 21:23:30 +08:00
float amount = 0;
2024-02-06 21:23:30 +08:00
// give some amount of alpha regardless of relative count
amount += non_relative_portion * Math.Min(1, Count / 10f);
2024-02-06 21:23:30 +08:00
// add relative portion
amount += (1 - non_relative_portion) * (Count / heatmap.PeakValue);
2024-02-06 21:23:30 +08:00
// apply easing
amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
2024-02-06 21:23:30 +08:00
Debug.Assert(amount <= 1);
2024-02-06 21:23:30 +08:00
Alpha = Math.Min(amount / lighten_cutoff, 1);
Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
2020-06-15 20:48:59 +08:00
}
}
}
}