Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Gameplay Accuracy Heatmap #31158

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Scoring;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Screens.Play;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;

namespace osu.Game.Rulesets.Osu.Skinning
{
public partial class GameplayAccuracyHeatmap : Container, ISerialisableDrawable
{
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;

[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;

[Resolved(canBeNull: true)]
private GameplayClockContainer? gameplayClockContainer { get; set; }

public bool UsesFixedAnchor { get; set; }

private float radius;
private AccuracyHeatmap heatmap = null!;

protected override void LoadComplete()
{
base.LoadComplete();

AutoSizeAxes = Axes.Both;

radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Value.Beatmap.Difficulty.CircleSize, true);

initHeatmap();

if (gameplayClockContainer != null)
gameplayClockContainer.OnSeek += initHeatmap;

scoreProcessor.NewJudgement += updateHeatmap;
Comment on lines +48 to +51
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drawable adding event subscriptions to other components that are not cleaned up on disposal => this will leak delegates and hold references to removed instances of this component forever

}

private void initHeatmap()
{
ScoreInfo scoreInfo = new ScoreInfo { BeatmapInfo = beatmap.Value.BeatmapInfo, HitEvents = (List<HitEvent>)scoreProcessor.HitEvents };
Child = new Container
{
Height = 200,
Width = 200,
Child = heatmap = new AccuracyHeatmap(scoreInfo, beatmap.Value.Beatmap, false)
{
RelativeSizeAxes = Axes.Both
}
};
}

private void updateHeatmap(JudgementResult j)
{
if (j is not OsuHitCircleJudgementResult circleJudgementResult || circleJudgementResult.CursorPositionAtHit == null)
return;

heatmap.AddPoint(((OsuHitObject)j.HitObject).StackedEndPosition, ((OsuHitObject)j.HitObject).StackedEndPosition, circleJudgementResult.CursorPositionAtHit.Value, radius);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this using CursorPositionAtHit rather than plain Position like the other existing usage of this?

AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);

}
}
}
13 changes: 9 additions & 4 deletions osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,25 @@ public partial class AccuracyHeatmap : CompositeDrawable
private const float rotation = 45;

private BufferedContainer bufferedGrid = null!;
private GridContainer pointGrid = null!;
private GridContainer? pointGrid;

private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;

private const float line_thickness = 2;

private readonly bool showsLabel;

/// <summary>
/// The highest count of any point currently being displayed.
/// </summary>
protected float PeakValue { get; private set; }

public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap, bool showsLabel = true)
{
this.score = score;
this.playableBeatmap = playableBeatmap;
this.showsLabel = showsLabel;
}

[BackgroundDependencyLoader]
Expand Down Expand Up @@ -123,6 +126,7 @@ private void load()
new OsuSpriteText
{
Text = "Overshoot",
Alpha = showsLabel ? 1 : 0,
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.BottomLeft,
Expand All @@ -134,6 +138,7 @@ private void load()
new OsuSpriteText
{
Text = "Undershoot",
Alpha = showsLabel ? 1 : 0,
Font = OsuFont.GetFont(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.TopRight,
Expand Down Expand Up @@ -227,9 +232,9 @@ private void load()
}
}

protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
{
if (pointGrid.Content.Count == 0)
if (pointGrid == null || pointGrid.Content.Count == 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is with the null check here? sounds to me like you're calling AddPoint() too early before the pointGrid has been created and trying to paper over that crack by doing this. this is not the correct way to do this, you must somehow make sure that the calls to AddPoint() that occur too early take place when everything else is ready. many ways to do this, including but not limited to scheduling or queueing the calls in a list until loaded

return;

double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
Expand Down
Loading