1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 20:33:35 +08:00

Improvements to discard screen UX (#37245)

Just an initial grab bag to keep these PRs small.

### Avoid showing countdown update when at discard screen

This is needless. We already have the `DiscardFinish` stage which has a
short countdown. Playing this change to the user creates unnecessary
confusion.

### Allow stage caption text to be changed at any point

Also remove custom colour support. We'll handle this internally in a
better way in the future.

### Better explain why we're waiting after discarding our own cards
This commit is contained in:
Dean Herbert
2026-04-10 20:12:27 +09:00
committed by GitHub
Unverified
parent 93b7c3324d
commit 9c43739228
10 changed files with 85 additions and 70 deletions
@@ -14,6 +14,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.RankedPlay;
using osuTK;
using osuTK.Graphics;
@@ -37,6 +38,40 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
private DateTimeOffset countdownStartTime;
private DateTimeOffset countdownEndTime;
private RankedPlayStage? activeStage;
private LocalisableString heading;
/// <summary>
/// Heading text to be displayed indicating the purpose of the current stage.
/// </summary>
public LocalisableString Heading
{
get => heading;
set
{
heading = value;
if (headingText != null)
headingText.Text = value;
}
}
private LocalisableString caption;
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
/// </summary>
public LocalisableString Caption
{
get => caption;
set
{
caption = value;
if (captionText != null)
captionText.Text = value;
}
}
public RankedPlayStageDisplay(RankedPlayColourScheme colourScheme)
{
this.colourScheme = colourScheme;
@@ -152,7 +187,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
Left = 10
},
UseFullGlyphHeight = false,
Text = "00:27:123",
Font = OsuFont.TorusAlternate.With(size: 16, fixedWidth: true, weight: FontWeight.SemiBold)
}
]
@@ -164,61 +198,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
Top = 80,
Left = 20
},
Colour = CaptionColour ?? colourScheme.Primary,
Text = Caption,
Font = OsuFont.TorusAlternate.With(size: 24, weight: FontWeight.SemiBold)
}
};
}
private LocalisableString heading;
/// <summary>
/// Heading text to be displayed indicating the purpose of the current stage.
/// </summary>
public LocalisableString Heading
{
get => heading;
set
{
heading = value;
if (headingText != null)
headingText.Text = value;
}
}
private LocalisableString caption;
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
/// </summary>
public LocalisableString Caption
{
get => caption;
set
{
caption = value;
if (captionText != null)
captionText.Text = value;
}
}
private Color4? captionColour;
/// <summary>
/// Overrides the default caption colour from the colour scheme with a custom one.
/// </summary>
public Color4? CaptionColour
{
get => captionColour;
set
{
captionColour = value;
if (captionText != null)
captionText.Colour = value ?? colourScheme.Primary;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
@@ -254,18 +239,37 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
{
if (countdown is not RankedPlayStageCountdown)
if (countdown is not RankedPlayStageCountdown stageCountdown)
return;
switch (stageCountdown.Stage)
{
case RankedPlayStage.CardDiscard:
// Discard stage ends when both players have discarded, but adds a 3 second delay before completing.
// Showing this in the countdown just creates visual noise, so let's handle internally.
if (activeStage == stageCountdown.Stage)
return;
break;
}
activeStage = stageCountdown.Stage;
countdownStartTime = DateTimeOffset.Now;
countdownEndTime = DateTimeOffset.Now + countdown.TimeRemaining;
});
private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() =>
{
if (countdown is not RankedPlayStageCountdown)
if (countdown is not RankedPlayStageCountdown stageCountdown)
return;
switch (stageCountdown.Stage)
{
// See above special case handling.
case RankedPlayStage.CardDiscard:
return;
}
countdownEndTime = DateTimeOffset.Now;
});
@@ -12,6 +12,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -23,7 +24,6 @@ using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
@@ -36,7 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Discard Phase";
protected override LocalisableString StageCaption => "Replace cards from your hand";
private PlayerHandOfCards playerHand = null!;
private ShearedButton discardButton = null!;
@@ -62,9 +61,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private DateTimeOffset stageEndTime;
private TimeSpan stageDuration;
private ScheduledDelegate? waitingOpponentTextUpdate;
public DiscardScreen()
{
StageDisplay.CaptionColour = Color4.White;
StageCaption = "Replace cards from your hand";
}
[BackgroundDependencyLoader]
@@ -233,6 +234,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
playerHand.SelectionMode = HandSelectionMode.Disabled;
hasDiscardedCards = true;
StageCaption = string.Empty;
// A bit awkward, but we're delaying this until we're mostly sure the opponent is still discarding.
// See the countdown reset logic for DiscardStage which gives 3 seconds for animation.
waitingOpponentTextUpdate = Scheduler.AddDelayed(() => StageCaption = "Waiting for your opponent...", 3200);
}
private readonly List<RankedPlayCardWithPlaylistItem> discardedCards = new List<RankedPlayCardWithPlaylistItem>();
@@ -313,6 +320,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
double presentationTime = Math.Max(earliestPresentationTime, Time.Current);
Scheduler.AddDelayed(presentRemainingCards, presentationTime - Time.Current);
waitingOpponentTextUpdate?.Cancel();
StageCaption = string.Empty;
}
private void presentRemainingCards()
@@ -28,7 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public Action<bool>? ExitRequested { get; init; }
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
[Resolved]
private RankedPlayMatchInfo matchInfo { get; set; } = null!;
@@ -14,7 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public partial class GameplayScreen : RankedPlaySubScreen
{
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[BackgroundDependencyLoader]
private void load()
@@ -33,7 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public override bool ShowBeatmapBackground => true;
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[Cached(typeof(IBindable<SongSelect.BeatmapSetLookupResult?>))]
private readonly Bindable<SongSelect.BeatmapSetLookupResult?> lastLookupResult = new Bindable<SongSelect.BeatmapSetLookupResult?>();
@@ -20,7 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
public partial class IntroScreen : RankedPlaySubScreen
{
public override LocalisableString StageHeading => string.Empty;
protected override LocalisableString StageCaption => string.Empty;
public IntroScreen()
{
@@ -25,7 +25,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "Waiting for your opponent...";
protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.RED;
@@ -40,6 +39,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private const int card_play_samples = 2;
private Sample?[]? cardPlaySamples;
public OpponentPickScreen()
{
StageCaption = "Waiting for your opponent...";
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -31,7 +31,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "It's your turn to play a card!";
private PlayerHandOfCards playerHand = null!;
private OpponentHandOfCards opponentHand = null!;
@@ -46,7 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private Sample? timeRunningOutSample;
private SampleChannel? timeRunningOutSampleChannel;
private Sample? timeUpBuzzerSample;
private DateTimeOffset stageEndTime;
private TimeSpan stageDuration;
@@ -56,6 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
/// </summary>
private bool hasPlayedCard;
public PickScreen()
{
StageCaption = "It's your turn to play a card!";
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -103,7 +106,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
cardPlaySamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Ranked/card-play-{1 + i}");
timeRunningOutSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/time-running-out");
timeUpBuzzerSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/time-up");
}
protected override void LoadComplete()
@@ -209,14 +211,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() =>
{
if (countdown is not RankedPlayStageCountdown stageCountdown)
if (countdown is not RankedPlayStageCountdown)
return;
stageEndTime = DateTimeOffset.Now;
stageDuration = TimeSpan.Zero;
if (stageCountdown.Stage == RankedPlayStage.CardPlay && !hasPlayedCard)
timeUpBuzzerSample?.Play();
});
private void onPlayButtonClicked()
@@ -37,7 +37,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
/// </summary>
protected abstract LocalisableString StageCaption { get; }
protected LocalisableString StageCaption
{
get => StageDisplay.Caption;
set => StageDisplay.Caption = value;
}
/// <summary>
/// The colour scheme commonly used for components of this screen.
@@ -79,7 +83,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
StageDisplay = new RankedPlayStageDisplay(ColourScheme)
{
Heading = StageHeading,
Caption = StageCaption,
Margin = new MarginPadding { Top = 60 },
State = { BindTarget = CountdownVisibility }
},
@@ -40,7 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public partial class ResultsScreen : RankedPlaySubScreen
{
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
public override bool ShowBeatmapBackground => true;