From 0ba3526956310c28309e7232cfe3164d8102b7bb Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 16 Dec 2024 21:57:33 +0800 Subject: [PATCH 1/5] Implement the gameplay accuracy heatmap --- .../Skinning/GameplayAccuracyHeatmap.cs | 75 +++++++++++++++++++ .../Statistics/AccuracyHeatmap.cs | 9 ++- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs new file mode 100644 index 000000000000..569ce48e0dc7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . 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 System.Linq; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Screens.Play; +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public partial class GameplayAccuracyHeatmap : Container, ISerialisableDrawable + { + [Resolved] + private IBindable 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; + } + + private void initHeatmap() + { + ScoreInfo scoreInfo = new ScoreInfo { BeatmapInfo = beatmap.Value.BeatmapInfo, HitEvents = (List)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) + { + var e = scoreProcessor.HitEvents.Last(); + if (e.Position != null && scoreProcessor.HitEvents.Count > 0) + heatmap.AddPoint(((OsuHitObject)j.HitObject).StackedEndPosition, ((OsuHitObject)j.HitObject).StackedEndPosition, e.Position.Value, radius); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 41620bc3d879..f85062bdc3b9 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -46,15 +46,18 @@ public partial class AccuracyHeatmap : CompositeDrawable private const float line_thickness = 2; + private readonly bool showsLabel; + /// /// The highest count of any point currently being displayed. /// 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] @@ -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, @@ -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, @@ -227,7 +232,7 @@ 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) return; From c8e9558e51f329a892396fa80fcec8ea07f1df0b Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 16 Dec 2024 22:14:48 +0800 Subject: [PATCH 2/5] Fix null instance reference --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f85062bdc3b9..966fc3d999dd 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -39,7 +39,7 @@ public partial class AccuracyHeatmap : CompositeDrawable private const float rotation = 45; private BufferedContainer bufferedGrid = null!; - private GridContainer pointGrid = null!; + private GridContainer? pointGrid = null!; private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; @@ -234,7 +234,7 @@ private void load() public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { - if (pointGrid.Content.Count == 0) + if (pointGrid == null || pointGrid.Content.Count == 0) return; double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. From a60ba30019d65084bf9fd7a4277866962f4da6c9 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 16 Dec 2024 22:22:29 +0800 Subject: [PATCH 3/5] Remove redundant initialization --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 966fc3d999dd..c6f6bc242f3f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -39,7 +39,7 @@ 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; From 7363d1a8801404898880ffa24f6dd92f24476768 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Tue, 17 Dec 2024 18:55:25 +0800 Subject: [PATCH 4/5] Optimize the position retrive logic --- .../Skinning/GameplayAccuracyHeatmap.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs index 569ce48e0dc7..485cc243ca5d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs @@ -12,10 +12,10 @@ using osu.Game.Rulesets.Scoring; using System.Collections.Generic; using osu.Game.Rulesets.Osu.Objects; -using System.Linq; 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 { @@ -67,9 +67,10 @@ private void initHeatmap() private void updateHeatmap(JudgementResult j) { - var e = scoreProcessor.HitEvents.Last(); - if (e.Position != null && scoreProcessor.HitEvents.Count > 0) - heatmap.AddPoint(((OsuHitObject)j.HitObject).StackedEndPosition, ((OsuHitObject)j.HitObject).StackedEndPosition, e.Position.Value, radius); + if (j is not OsuHitCircleJudgementResult circleJudgementResult || circleJudgementResult.CursorPositionAtHit == null) + return; + + heatmap.AddPoint(((OsuHitObject)j.HitObject).StackedEndPosition, ((OsuHitObject)j.HitObject).StackedEndPosition, circleJudgementResult.CursorPositionAtHit.Value, radius); } } } From 9002e540ab361b659847ebed203412ecfa44efd2 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Thu, 9 Jan 2025 14:46:27 +0800 Subject: [PATCH 5/5] Fix unnecessary null check and dangling delegate --- .../Skinning/GameplayAccuracyHeatmap.cs | 10 ++++++++++ osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs index 485cc243ca5d..10aaa1d9623a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Skinning/GameplayAccuracyHeatmap.cs @@ -51,6 +51,16 @@ protected override void LoadComplete() scoreProcessor.NewJudgement += updateHeatmap; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (gameplayClockContainer != null) + gameplayClockContainer.OnSeek -= initHeatmap; + + scoreProcessor.NewJudgement -= updateHeatmap; + } + private void initHeatmap() { ScoreInfo scoreInfo = new ScoreInfo { BeatmapInfo = beatmap.Value.BeatmapInfo, HitEvents = (List)scoreProcessor.HitEvents }; diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index c6f6bc242f3f..f85062bdc3b9 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -39,7 +39,7 @@ public partial class AccuracyHeatmap : CompositeDrawable private const float rotation = 45; private BufferedContainer bufferedGrid = null!; - private GridContainer? pointGrid; + private GridContainer pointGrid = null!; private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; @@ -234,7 +234,7 @@ private void load() public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { - if (pointGrid == null || pointGrid.Content.Count == 0) + if (pointGrid.Content.Count == 0) return; double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.