1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 18:10:40 +08:00

Ranked Play: Add countdown timer to all screens (#36930)

There's been [some
feedback](https://discord.com/channels/90072389919997952/1476979671886205060/1477868178775216148)
that there's no timer during some screens. Personally I feel like some
screens shouldn't require it, but I get it.

The results screen is going to be the most controversial one, where I've
decided to shift the content down a bit - it would otherwise overlap
with the progress bar. Another option is to make the progress bar
shorter, but I feel like that makes things inconsistent.

<img width="1922" height="1035" alt="image"
src="https://github.com/user-attachments/assets/ec4ba01e-bb13-45ff-8a1b-b39d7765c1f4"
/>

Regarding implementation, I'm not entirely sure on it being added to
every subscreen via `RankedPlaySubScreen` instead of just a single
component at a top level. I did it this way because the colour scheme
seems annoying to change without recreating the entire component (or
otherwise all of its children) anyway.
This commit is contained in:
Dan Balasescu
2026-03-11 17:04:51 +09:00
committed by GitHub
Unverified
parent ce571e328d
commit 4c0657f4ca
10 changed files with 137 additions and 42 deletions
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@@ -21,24 +20,20 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
{
public partial class RankedPlayStageDisplay : CompositeDrawable
public partial class RankedPlayStageDisplay : VisibilityContainer
{
public required LocalisableString Heading { get; init; }
public required LocalisableString Caption { get; init; }
public Color4? CaptionColour { get; init; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private readonly RankedPlayColourScheme colourScheme;
private Drawable headingTextBackground = null!;
private OsuSpriteText headingText = null!;
private Drawable progressBar = null!;
private OsuSpriteText progressText = null!;
private OsuSpriteText? headingText;
private OsuSpriteText? captionText;
private DateTimeOffset countdownStartTime;
private DateTimeOffset countdownEndTime;
@@ -50,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load()
{
const float phase_text_background_height = 55;
Vector2 progressBarSize = new Vector2(300, 25);
@@ -162,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
}
]
},
new OsuSpriteText
captionText = new OsuSpriteText
{
Margin = new MarginPadding
{
@@ -176,6 +171,54 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
};
}
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();
@@ -194,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
{
base.Update();
headingTextBackground.Width = headingText.DrawWidth + 80;
headingTextBackground.Width = headingText!.DrawWidth + 80;
TimeSpan duration = countdownEndTime - countdownStartTime;
TimeSpan remaining = countdownEndTime - DateTimeOffset.Now;
@@ -226,6 +269,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
countdownEndTime = DateTimeOffset.Now;
});
protected override void PopIn()
{
this.FadeIn();
}
protected override void PopOut()
{
this.FadeOut();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -11,6 +11,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -33,6 +34,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Discard Phase";
protected override LocalisableString StageCaption => "Replace cards from your hand";
private PlayerHandOfCards playerHand = null!;
private ShearedButton discardButton = null!;
private OsuTextFlowContainer explainer = null!;
@@ -57,6 +61,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private DateTimeOffset stageEndTime;
private TimeSpan stageDuration;
public DiscardScreen()
{
StageDisplay.CaptionColour = Color4.White;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -72,13 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new RankedPlayStageDisplay(RankedPlayColourScheme.Blue)
{
Heading = "Discard Phase",
Caption = "Replace cards from your hand",
CaptionColour = Color4.White,
Margin = new MarginPadding { Top = 60 },
},
discardButton = new ShearedButton
{
Name = "Discard Button",
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -24,6 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
/// </summary>
public Action<bool>? ExitRequested { get; init; }
protected override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
[Resolved]
private RankedPlayMatchInfo matchInfo { get; set; } = null!;
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -12,6 +13,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class GameplayScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[BackgroundDependencyLoader]
private void load()
{
@@ -8,6 +8,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
@@ -24,6 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public override bool ShowBeatmapBackground => true;
protected 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?>();
@@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -20,9 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
{
public partial class IntroScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => string.Empty;
protected override LocalisableString StageCaption => string.Empty;
public IntroScreen()
{
CornerPieceVisibility.Value = Visibility.Hidden;
CountdownVisibility.Value = Visibility.Hidden;
}
[Resolved]
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
@@ -22,6 +23,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "Waiting for your opponent...";
protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Red;
private PlayerHandOfCards playerHand = null!;
private OpponentHandOfCards opponentHand = null!;
@@ -46,12 +52,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new RankedPlayStageDisplay(RankedPlayColourScheme.Red)
{
Heading = "Pick Phase",
Caption = "Waiting for your opponent...",
Margin = new MarginPadding { Top = 60 },
},
];
CenterColumn.Children =
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Online.Multiplayer;
@@ -27,6 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected 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!;
@@ -65,12 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new RankedPlayStageDisplay(RankedPlayColourScheme.Blue)
{
Heading = "Pick Phase",
Caption = "It's your turn to play a card!",
Margin = new MarginPadding { Top = 60 },
},
];
CenterColumn.Children =
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osuTK;
@@ -18,19 +19,34 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public const float CENTERED_CARD_SCALE = 1.2f;
public readonly Bindable<Visibility> CornerPieceVisibility = new Bindable<Visibility>(Visibility.Visible);
protected readonly Bindable<Visibility> CountdownVisibility = new Bindable<Visibility>(Visibility.Visible);
public virtual bool ShowBeatmapBackground => false;
/// <summary>
/// Heading text to be displayed indicating the purpose of the current stage.
/// </summary>
protected abstract LocalisableString StageHeading { get; }
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
/// </summary>
protected abstract LocalisableString StageCaption { get; }
/// <summary>
/// The colour scheme commonly used for components of this screen.
/// </summary>
protected virtual RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Blue;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
protected MultiplayerClient Client => client;
protected override Container<Drawable> Content { get; }
protected readonly Container CenterColumn;
protected readonly FillFlowContainer ButtonsContainer;
protected readonly RankedPlayStageDisplay StageDisplay;
protected RankedPlaySubScreen()
{
@@ -62,6 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Direction = FillDirection.Vertical,
Spacing = new Vector2(8)
},
StageDisplay = new RankedPlayStageDisplay(ColourScheme)
{
Heading = StageHeading,
Caption = StageCaption,
Margin = new MarginPadding { Top = 60 },
State = { BindTarget = CountdownVisibility }
},
];
}
@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -36,6 +37,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class ResultsScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
public override bool ShowBeatmapBackground => true;
[Resolved]
@@ -63,14 +67,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
CornerPieceVisibility.Value = Visibility.Hidden;
InternalChildren = new Drawable[]
AddInternal(loadingSpinner = new LoadingSpinner
{
loadingSpinner = new LoadingSpinner
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
};
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
protected override void LoadComplete()
@@ -160,6 +161,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
AddInternal(new ResultScreenContent
{
RelativeSizeAxes = Axes.Both,
// A little bit of room for the countdown timer...
Margin = new MarginPadding { Top = 45 },
PlayerScore = playerScore,
OpponentScore = opponentScore,
PlayerDamageInfo = matchInfo.RoomState.Users[playerId].DamageInfo!,
@@ -210,8 +214,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
.OfType<RankedPlayDamageInfo>()
.MaxBy(it => it.Damage)!;
RelativeSizeAxes = Axes.Both;
AddInternal(panelScaffold = new PanelScaffold
{
Anchor = Anchor.Centre,