1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 02:31:25 +08:00

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`:

<img width="1838" height="1353" alt="image"
src="https://github.com/user-attachments/assets/c621d759-a88f-49f0-b0df-3a6da90eca65"
/>

Gameplay:

<img width="1838" height="1353" alt="image"
src="https://github.com/user-attachments/assets/ee848d06-3878-4cbb-bd4b-de2c4bcfa688"
/>

Currently the drawable overlaps with some components, but it will be
resolved in later pull requests.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Krzysztof Gutkowski
2026-04-14 17:27:43 +02:00
committed by GitHub
Unverified
parent ac620ee375
commit 16a2a96bcf
3 changed files with 208 additions and 2 deletions
@@ -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)
@@ -1,6 +1,7 @@
// 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;
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<RankedPlaySubScreen> screenContainer;
private readonly RankedPlayChatDisplay chat;
private RankedPlayBottomOrnament ornament = null!;
private IDisposable? ornamentOverlayRegistration;
private IBindable<RankedPlayStage> 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);
}
}
@@ -0,0 +1,178 @@
// 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.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
{
/// <summary>
/// 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.
/// </summary>
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<Vector2> vertices = new List<Vector2>();
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);
}
}
}