1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 05:27:23 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs

231 lines
9.4 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;
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;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
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 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;
private GridContainer pointGrid;
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
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()
{
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 CircularContainer
2020-06-15 20:48:59 +08:00
{
2020-06-16 15:31:02 +08:00
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
2020-06-15 20:48:59 +08:00
RelativeSizeAxes = Axes.Both,
2020-06-16 15:31:02 +08:00
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = 2f,
BorderColour = Color4.White,
Child = new Box
2020-06-15 20:48:59 +08:00
{
2020-06-16 15:31:02 +08:00
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
2020-06-15 20:48:59 +08:00
{
2020-06-16 15:31:02 +08:00
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = 1f,
Rotation = -rotation,
Alpha = 0.3f,
},
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = 1f,
Rotation = rotation
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 10,
Height = 2f,
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Y = -1,
Width = 2f,
Height = 10,
}
2020-06-15 20:48:59 +08:00
}
2020-06-16 15:31:02 +08:00
},
pointGrid = new GridContainer
2020-06-16 15:31:02 +08:00
{
RelativeSizeAxes = Axes.Both
2020-06-15 20:48:59 +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
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++)
{
HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius
? HitPointType.Hit
: HitPointType.Miss;
2020-06-15 20:48:59 +08:00
var point = new HitPoint(pointType)
2020-06-15 20:48:59 +08:00
{
Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
};
points[r][c] = point;
2020-06-15 20:48:59 +08:00
}
}
pointGrid.Content = points;
2020-06-19 18:58:35 +08:00
if (score.HitEvents == null || score.HitEvents.Count == 0)
return;
2020-06-19 18:58:35 +08:00
// Todo: This should probably not be done like this.
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
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
{
if (pointGrid.Content.Length == 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.
var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
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 = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
2020-06-15 20:48:59 +08:00
((HitPoint)pointGrid.Content[r][c]).Increment();
2020-06-15 20:48:59 +08:00
}
private class HitPoint : Circle
{
private readonly HitPointType pointType;
public HitPoint(HitPointType pointType)
2020-06-15 20:48:59 +08:00
{
this.pointType = pointType;
RelativeSizeAxes = Axes.Both;
2020-06-15 20:48:59 +08:00
Alpha = 0;
}
public void Increment()
{
if (Alpha < 1)
Alpha += 0.1f;
else if (pointType == HitPointType.Hit)
Colour = ((Color4)Colour).Lighten(0.1f);
}
}
private enum HitPointType
{
Hit,
Miss
}
}
}