From 16a2a96bcf64580ab889ee60e8e26d82967a93b3 Mon Sep 17 00:00:00 2001 From: Krzysztof Gutkowski Date: Tue, 14 Apr 2026 17:27:43 +0200 Subject: [PATCH] Add bottom 'ornament' overlay to ranked play (#37288) This commit adds an always present overlay to all ranked play screens, meant to indicate to the user that a ranked play session in currently in progress. This has been largely inspired by the pre-shader argon healthbar code. It shouldn't have the same performance concerns, however, since the paths are only calculated once when loading the drawable (and eventually when it is resized, if ever). `RankedPlayScreen`: image Gameplay: image Currently the drawable overlaps with some components, but it will be resolved in later pull requests. --------- Co-authored-by: Dean Herbert --- .../RankedPlay/Intro/VsSequence.cs | 9 +- .../RankedPlay/RankedPlayScreen.cs | 23 +++ .../Matchmaking/RankedPlayBottomOrnament.cs | 178 ++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/VsSequence.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/VsSequence.cs index 5b2656de90..0bd7d0e287 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/VsSequence.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/VsSequence.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro vsText.ScaleTo(0.4f, 1300, Easing.OutExpo); } - delay += 850; + delay += delay_first; impactDelay = delay; @@ -187,9 +187,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro this.Delay(3200).FadeOut(300).Expire(); } - delay += 3350; + delay += delay_final; } + private const double delay_first = 850; + private const double delay_final = 3350; + + public const double INTRO_LENGTH = delay_first + delay_final; + private partial class UserDisplay : CompositeDrawable { public UserDisplay(UserWithRating user, Anchor contentAnchor) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs index 1f127de542..fbc8eb319b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.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; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -67,6 +68,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay [Resolved] private IDialogOverlay dialogOverlay { get; set; } = null!; + [Resolved] + private IOverlayManager overlayManager { get; set; } = null!; + [Resolved] private AudioManager audio { get; set; } = null!; @@ -85,6 +89,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay private readonly Container screenContainer; private readonly RankedPlayChatDisplay chat; + private RankedPlayBottomOrnament ornament = null!; + private IDisposable? ornamentOverlayRegistration; + private IBindable stage = null!; private Sample? sampleStart; @@ -158,6 +165,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { stage = matchInfo.Stage.GetBoundCopy(); sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + LoadComponent(ornament = new RankedPlayBottomOrnament + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }); } protected override void LoadComplete() @@ -168,6 +181,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay client.UserStateChanged += onUserStateChanged; client.LoadRequested += onLoadRequested; + Scheduler.AddDelayed(() => ornament.Show(), VsSequence.INTRO_LENGTH); + int localUserId = api.LocalUser.Value.OnlineID; int opponentUserId = ((RankedPlayRoomState)client.Room!.MatchState!).Users.Keys.Single(it => it != localUserId); @@ -195,6 +210,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay }, ]); + ornamentOverlayRegistration = overlayManager.RegisterBlockingOverlay(ornament); + stage.BindValueChanged(e => onStageChanged(e.NewValue)); } @@ -356,6 +373,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay client.LeaveRoom().FireAndForget(); + ornamentOverlayRegistration?.Dispose(); + ornamentOverlayRegistration = null; + if (retryRequested) controller?.RejoinQueue(); @@ -406,6 +426,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay client.UserStateChanged -= onUserStateChanged; client.LoadRequested -= onLoadRequested; + ornamentOverlayRegistration?.Dispose(); + ornamentOverlayRegistration = null; + base.Dispose(isDisposing); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs new file mode 100644 index 0000000000..6eaa4ae6ff --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlayBottomOrnament.cs @@ -0,0 +1,178 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Lines; +using osu.Framework.Layout; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking +{ + /// + /// A small component intended to be always present at the bottom of all ranked play screens + /// to indicate a ranked play session is in progress. + /// + public partial class RankedPlayBottomOrnament : OverlayContainer + { + private const int width = 400; + private const int height = 24; + + protected override bool BlockPositionalInput => false; + + private Path pathLeft = null!; + private Path pathRight = null!; + + private Path pathCenter = null!; + private Path pathCenterWide = null!; + + private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); + + protected override bool StartHidden => true; + + [BackgroundDependencyLoader] + private void load() + { + Width = width; + Height = height; + Alpha = 0; + + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4Extensions.FromHex("#15061e").Opacity(0.8f), + Type = EdgeEffectType.Glow, + Radius = height * 2, + Roundness = height * 2, + Offset = new Vector2(0, height / 2f), + }; + + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 10, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Children = new Drawable[] + { + pathLeft = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 1, + }, + pathCenter = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 1, + }, + pathCenterWide = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 2, + }, + pathRight = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 1, + }, + }, + }, + new OsuSpriteText + { + Y = 4, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Torus.With(size: 10, weight: FontWeight.Bold), + Spacing = new Vector2(3, 0), + Text = ButtonSystemStrings.RankedPlay.ToUpper(), + }, + }; + } + + private void recomputePaths() + { + const int top = 2; // to account for the middle segment being twice as wide + const int bottom = 10; + const int curve_smoothness = 5; + + pathCenter.AddVertex(new Vector2(30, top)); + pathCenter.AddVertex(new Vector2(DrawWidth - 30, top)); + + pathCenterWide.AddVertex(new Vector2(60, top)); + pathCenterWide.AddVertex(new Vector2(DrawWidth - 60, top)); + + const int left_start = 0; + const int left_corner = 10; + const int left_end = 20; + + List vertices = new List(); + var diagonalDirLeft = (new Vector2(left_start, bottom) - new Vector2(left_corner, top)).Normalized(); + + var sliderPathLeft = new SliderPath(new[] + { + new PathControlPoint(new Vector2(left_start, bottom), PathType.LINEAR), + new PathControlPoint(new Vector2(left_corner, top) + diagonalDirLeft * curve_smoothness, PathType.BEZIER), + new PathControlPoint(new Vector2(left_corner, top)), + new PathControlPoint(new Vector2(left_end, top), PathType.LINEAR), + }); + + sliderPathLeft.GetPathToProgress(vertices, 0.0, 1.0); + pathLeft.Vertices = vertices; + + float rightStart = DrawWidth; + float rightCorner = rightStart - 10; + float rightEnd = rightStart - 20; + + var diagonalDirRight = (new Vector2(rightStart, bottom) - new Vector2(rightCorner, top)).Normalized(); + var sliderPathRight = new SliderPath(new[] + { + new PathControlPoint(new Vector2(rightStart, bottom), PathType.LINEAR), + new PathControlPoint(new Vector2(rightCorner, top) + diagonalDirRight * curve_smoothness, PathType.BEZIER), + new PathControlPoint(new Vector2(rightCorner, top)), + new PathControlPoint(new Vector2(rightEnd, top), PathType.LINEAR), + }); + + sliderPathRight.GetPathToProgress(vertices, 0.0, 1.0); + pathRight.Vertices = vertices; + } + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid) + { + recomputePaths(); + layout.Validate(); + } + } + + protected override void PopIn() + { + this.FadeIn(500, Easing.OutQuint); + // TODO: animate this better. + } + + protected override void PopOut() + { + this.FadeOut(500, Easing.OutQuint); + } + } +}