diff --git a/osu.Game.Rulesets.Osu/Skinning/AimErrorMeter.cs b/osu.Game.Rulesets.Osu/Skinning/AimErrorMeter.cs index 70ec88a11e..144fa63b10 100644 --- a/osu.Game.Rulesets.Osu/Skinning/AimErrorMeter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/AimErrorMeter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -17,6 +18,8 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osuTK; using osuTK.Graphics; @@ -35,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning }; [SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.JudgementStyle), nameof(AimErrorMeterStrings.JudgementStyleDescription))] - public Bindable JudgementStyle { get; } = new Bindable(); + public Bindable JudgementStyle { get; } = new Bindable(); [SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageSize), nameof(AimErrorMeterStrings.AverageSizeDescription))] public BindableNumber AverageSize { get; } = new BindableNumber(12f) @@ -46,16 +49,32 @@ namespace osu.Game.Rulesets.Osu.Skinning }; [SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageStyle), nameof(AimErrorMeterStrings.AverageStyleDescription))] - public Bindable AverageStyle { get; } = new Bindable(HitPositionStyle.Plus); + public Bindable AverageStyle { get; } = new Bindable(HitStyle.Plus); + + [SettingSource("Position Style")] + public Bindable HitPositionStyle { get; } = new Bindable(); + + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; private Container averagePositionContainer = null!; + private Container averagePositionRotateContainer = null!; private Vector2 averagePosition; private readonly DrawablePool hitPositionPool = new DrawablePool(20); private Container hitPositionsContainer = null!; + private Container arrowBackgroundContainer = null!; + private UprightAspectMaintainingContainer rotateFixedContainer = null!; + private Container mainContainer = null!; + private float objectRadius; + private const int max_concurrent_judgements = 30; + + private const float line_thickness = 2; + private const float inner_portion = 0.85f; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -75,12 +94,29 @@ namespace osu.Game.Rulesets.Osu.Skinning Children = new Drawable[] { hitPositionPool, + rotateFixedContainer = new UprightAspectMaintainingContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }; + + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { new CircularContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, BorderColour = Colour4.White, Masking = true, BorderThickness = 2, RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), Child = new Box { Colour = Colour4.Gray, @@ -88,38 +124,136 @@ namespace osu.Game.Rulesets.Osu.Skinning RelativeSizeAxes = Axes.Both }, }, - hitPositionsContainer = new UprightAspectMaintainingContainer + arrowBackgroundContainer = new Container { - RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + Name = "Arrow Background", + RelativeSizeAxes = Axes.Both, + Rotation = 45, + Alpha = 0f, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = inner_portion + 0.2f, + Width = line_thickness / 2, + }, + new Circle + { + Height = 5f, + Width = line_thickness / 2, + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(-line_thickness / 4), + RelativePositionAxes = Axes.Both, + Y = -(inner_portion + 0.2f) / 2, + Rotation = -45 + }, + new Circle + { + Height = 5f, + Width = line_thickness / 2, + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding(-line_thickness / 4), + RelativePositionAxes = Axes.Both, + Y = -(inner_portion + 0.2f) / 2, + Rotation = 45 + } + } }, - new UprightAspectMaintainingContainer + new Container + { + Name = "Cross Background", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.5f, + Width = line_thickness, + Height = inner_portion * 0.9f + }, + new Circle + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.5f, + Width = line_thickness, + Height = inner_portion * 0.9f, + Rotation = 90 + }, + new Circle + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + Width = line_thickness / 2, + Height = inner_portion * 0.9f, + Rotation = 45 + }, + new Circle + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.2f, + Width = line_thickness / 2, + Height = inner_portion * 0.9f, + Rotation = 135 + }, + } + }, + new Container { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = averagePositionContainer = new Container + Children = new Drawable[] { - RelativePositionAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + hitPositionsContainer = new Container { - new Circle + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + averagePositionContainer = new UprightAspectMaintainingContainer + { + RelativePositionAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = averagePositionRotateContainer = new Container { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 0.25f, - }, - new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.25f, - Rotation = 90 + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.25f, + }, + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.25f, + Rotation = 90 + } + } } } } @@ -130,7 +264,29 @@ namespace osu.Game.Rulesets.Osu.Skinning objectRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Value.Beatmap.Difficulty.CircleSize, true); AverageSize.BindValueChanged(size => averagePositionContainer.Size = new Vector2(size.NewValue), true); - AverageStyle.BindValueChanged(style => averagePositionContainer.Rotation = style.NewValue == HitPositionStyle.Plus ? 0 : 45, true); + AverageStyle.BindValueChanged(style => averagePositionRotateContainer.Rotation = style.NewValue == HitStyle.Plus ? 0 : 45, true); + + HitPositionStyle.BindValueChanged(s => + { + foreach (var hit in hitPositionsContainer) + { + hit.FadeOut(300).Expire(); + averagePositionContainer.MoveTo(averagePosition = Vector2.Zero, 800, Easing.OutQuint); + } + + if (s.NewValue == PositionStyle.Relative) + { + arrowBackgroundContainer.FadeIn(100); + rotateFixedContainer.Remove(mainContainer, false); + AddInternal(mainContainer); + } + else + { + arrowBackgroundContainer.FadeOut(100); + RemoveInternal(mainContainer, false); + rotateFixedContainer.Add(mainContainer); + } + }, true); } protected override void OnNewJudgement(JudgementResult judgement) @@ -139,31 +295,80 @@ namespace osu.Game.Rulesets.Osu.Skinning if (circleJudgement.CursorPositionAtHit == null) return; - var relativeHitPosition = (circleJudgement.CursorPositionAtHit.Value - ((OsuHitObject)circleJudgement.HitObject).StackedPosition) / objectRadius / 2; + if (hitPositionsContainer.Count > max_concurrent_judgements) + { + const double quick_fade_time = 300; + + // check with a bit of lenience to avoid precision error in comparison. + var old = hitPositionsContainer.FirstOrDefault(j => j.LifetimeEnd > Clock.CurrentTime + quick_fade_time * 1.1); + + if (old != null) + { + old.ClearTransforms(); + old.FadeOut(quick_fade_time).Expire(); + } + } + + Vector2 hitPosition; + + if (HitPositionStyle.Value == PositionStyle.Relative && scoreProcessor.HitEvents.LastOrDefault().LastHitObject != null) + { + var currentHitEvent = scoreProcessor.HitEvents.Last(); + + hitPosition = AccuracyHeatmap.FindRelativeHitPosition(((OsuHitObject)currentHitEvent.LastHitObject).StackedEndPosition, ((OsuHitObject)currentHitEvent.HitObject).StackedEndPosition, + circleJudgement.CursorPositionAtHit.Value, objectRadius, new Vector2(0.5f), inner_portion, 45) - new Vector2(0.5f); + } + else + { + hitPosition = roundPosition((circleJudgement.CursorPositionAtHit.Value - ((OsuHitObject)circleJudgement.HitObject).StackedPosition) / objectRadius / 2 * inner_portion); + } hitPositionPool.Get(drawableHit => { - drawableHit.X = relativeHitPosition.X; - drawableHit.Y = relativeHitPosition.Y; - drawableHit.Colour = getColourForPosition(relativeHitPosition); + drawableHit.X = hitPosition.X; + drawableHit.Y = hitPosition.Y; + drawableHit.Colour = getColourForPosition(hitPosition); hitPositionsContainer.Add(drawableHit); }); - averagePositionContainer.MoveTo(averagePosition = (relativeHitPosition + averagePosition) / 2, 800, Easing.OutQuint); + averagePositionContainer.MoveTo(averagePosition = (hitPosition + averagePosition) / 2, 800, Easing.OutQuint); + } + + private static Vector2 roundPosition(Vector2 position) + { + if (position.X > 0.5f) + { + position.X = 0.5f; + } + else if (position.X < -0.5f) + { + position.X = -0.5f; + } + + if (position.Y > 0.5f) + { + position.Y = 0.5f; + } + else if (position.Y < -0.5f) + { + position.Y = -0.5f; + } + + return position; } private Color4 getColourForPosition(Vector2 position) { switch (Vector2.Distance(position, Vector2.Zero)) { - case >= 0.5f: + case >= 0.5f * inner_portion: return colours.Red; - case >= 0.35f: + case >= 0.35f * inner_portion: return colours.Yellow; - case >= 0.2f: + case >= 0.2f * inner_portion: return colours.Green; default: @@ -189,7 +394,9 @@ namespace osu.Game.Rulesets.Osu.Skinning public readonly BindableNumber JudgementSize = new BindableFloat(); - public readonly Bindable JudgementStyle = new Bindable(); + public readonly Bindable JudgementStyle = new Bindable(); + + private readonly Container content; public HitPosition() { @@ -198,23 +405,35 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.Centre; Origin = Anchor.Centre; - InternalChildren = new Drawable[] + InternalChild = new UprightAspectMaintainingContainer { - new Circle + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = content = new Container { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 0.25f, - Rotation = -45 - }, - new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.25f, - Rotation = 45 + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.25f, + Rotation = -45 + }, + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.25f, + Rotation = 45 + } + } } }; } @@ -224,9 +443,9 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); JudgementSize.BindTo(aimErrorMeter.JudgementSize); - JudgementSize.BindValueChanged(size => Size = new Vector2(size.NewValue)); + JudgementSize.BindValueChanged(size => Size = new Vector2(size.NewValue), true); JudgementStyle.BindTo(aimErrorMeter.JudgementStyle); - JudgementStyle.BindValueChanged(style => Rotation = style.NewValue == HitPositionStyle.X ? 0 : 45); + JudgementStyle.BindValueChanged(style => content.Rotation = style.NewValue == HitStyle.X ? 0 : 45, true); } protected override void PrepareForUse() @@ -246,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } } - public enum HitPositionStyle + public enum HitStyle { [LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.StyleX))] X, @@ -254,5 +473,11 @@ namespace osu.Game.Rulesets.Osu.Skinning [LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.StylePlus))] Plus } + + public enum PositionStyle + { + Absolute, + Relative, + } } }