From 52fbd9e7969f3d21c5c945da323ca9e6fe7be2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 13:08:42 +0200 Subject: [PATCH 1/8] Implement local user position display for multiplayer --- .../TestSceneMultiplayerPositionDisplay.cs | 93 +++++++++++++++++++ .../Multiplayer/MultiplayerPositionDisplay.cs | 71 ++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs new file mode 100644 index 0000000000..34e9080db3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Tests.Gameplay; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene + { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Test] + public void TestAppearance() + { + TestGameplayLeaderboardProvider leaderboard = null!; + MultiplayerPositionDisplay display = null!; + GameplayState gameplayState = null!; + + AddStep("create content", () => + { + leaderboard = new TestGameplayLeaderboardProvider(); + Children = new Drawable[] + { + leaderboard, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(IGameplayLeaderboardProvider), leaderboard), + (typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset())) + ], + Child = display = new MultiplayerPositionDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; + }); + AddSliderStep("set score position", 1, 100, 50, r => + { + if (leaderboard.IsNotNull() && leaderboard.Score.IsNotNull()) + leaderboard.Score.Position.Value = r; + }); + AddStep("unset position", () => leaderboard.Score.Position.Value = null); + + AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("toggle leaderboard off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false)); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + + AddStep("enter break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Break); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("exit break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + + AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser()); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + } + + private partial class TestGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public GameplayLeaderboardScore Score { get; private set; } = null!; + + IBindableList IGameplayLeaderboardProvider.Scores => scores; + private readonly BindableList scores = new BindableList(); + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + scores.Add(Score = new GameplayLeaderboardScore(api.LocalUser.Value, true, new BindableLong())); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs new file mode 100644 index 0000000000..af847a7b51 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -0,0 +1,71 @@ +// 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; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerPositionDisplay : CompositeDrawable + { + private readonly IBindable user = new Bindable(); + private readonly IBindableList scores = new BindableList(); + private readonly BindableBool showLeaderboard = new BindableBool(); + private readonly IBindable localUserPlayingState = new Bindable(); + + private readonly Bindable position = new Bindable(); + + private OsuSpriteText positionText = null!; + + [BackgroundDependencyLoader] + private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState) + { + scores.BindTo(leaderboardProvider.Scores); + user.BindTo(api.LocalUser); + configManager.BindWith(OsuSetting.GameplayLeaderboard, showLeaderboard); + localUserPlayingState.BindTo(gameplayState.PlayingState); + + AutoSizeAxes = Axes.Both; + InternalChild = positionText = new OsuSpriteText + { + Alpha = 0.5f, + Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + user.BindValueChanged(_ => updateState()); + scores.BindCollectionChanged((_, __) => updateState()); + showLeaderboard.BindValueChanged(_ => updateState()); + localUserPlayingState.BindValueChanged(_ => updateState(), true); + + position.BindValueChanged(_ => positionText.Text = position.Value != null ? $@"#{position.Value.Value:N0}" : "-", true); + } + + private void updateState() + { + position.UnbindBindings(); + + var userScore = scores.SingleOrDefault(s => s.User.Equals(user.Value)); + if (userScore != null) + position.BindTo(userScore.Position); + else + position.Value = null; + + Alpha = userScore != null && (showLeaderboard.Value || localUserPlayingState.Value == LocalUserPlayingState.Break) ? 1 : 0; + } + } +} From 4e497637877a1bb6613129c4ce22832773e3ad69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 13:28:45 +0200 Subject: [PATCH 2/8] Add local user position display to multiplayer --- .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 5 +++++ osu.Game/Screens/Play/HUDOverlay.cs | 14 +++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 386276720e..0e114b752e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -88,6 +88,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, HUDOverlay.TopLeftElements.Add); + LoadComponentAsync(new MultiplayerPositionDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, d => HUDOverlay.BottomRightElements.Insert(-1, d)); LoadComponentAsync(leaderboardProvider, loaded => { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d108d82a6b..806e593729 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play return base.ShouldBeConsideredForInput(child); // hold to quit button should always be interactive. - return child == bottomRightElements; + return child == BottomRightElements; } public readonly ModDisplay ModDisplay; @@ -92,8 +92,8 @@ namespace osu.Game.Screens.Play // They will make a best-effort attempt to get out of the way of any other skinnable components. public readonly FillFlowContainer TopLeftElements; - internal readonly FillFlowContainer TopRightElements; - private readonly FillFlowContainer bottomRightElements; + public readonly FillFlowContainer TopRightElements; + public readonly FillFlowContainer BottomRightElements; internal readonly IBindable IsPlaying = new Bindable(); @@ -153,7 +153,7 @@ namespace osu.Game.Screens.Play ModDisplay = CreateModsContainer(), } }, - bottomRightElements = new FillFlowContainer + BottomRightElements = new FillFlowContainer { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -289,10 +289,10 @@ namespace osu.Game.Screens.Play else TopLeftElements.Y = 0; - if (highestBottomScreenSpace.HasValue && DrawHeight - bottomRightElements.DrawHeight > 0) - bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + if (highestBottomScreenSpace.HasValue && DrawHeight - BottomRightElements.DrawHeight > 0) + BottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight); else - bottomRightElements.Y = 0; + BottomRightElements.Y = 0; void processDrawables(SkinnableContainer components) { From 1d7f9220686941a4cf381e29cf0462b9b7ac9ea0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 May 2025 15:29:33 +0900 Subject: [PATCH 3/8] Fix test not showing immediately useful state Also standardises the testing leaderboard provider component for (immediate) future use. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 42 +++++++-------- .../TestSceneMultiplayerPositionDisplay.cs | 51 +++++++------------ 2 files changed, 41 insertions(+), 52 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index f8caa121a9..f45e6326d1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -50,11 +50,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add many scores in one go", () => { for (int i = 0; i < 32; i++) - createRandomScore(new APIUser { Username = $"Player {i + 1}" }); + leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {i + 1}" }); // Add player at end to force an animation down the whole list. playerScore.Value = 0; - createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); + leaderboardProvider.CreateLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); }); // Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay addLocalPlayer(); int playerNumber = 1; - AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10); + AddRepeatStep("add player with random score", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10); } [Test] @@ -93,10 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay createLeaderboard(); addLocalPlayer(); - AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 })); - AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 })); - AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 })); - AddStep("add frenzibyte", () => createRandomScore(new APIUser { Username = "frenzibyte", Id = 14210502 })); + AddStep("add peppy", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "peppy", Id = 2 })); + AddStep("add smoogipoo", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 })); + AddStep("add flyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "flyte", Id = 3103765 })); + AddStep("add frenzibyte", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = "frenzibyte", Id = 14210502 })); } [Test] @@ -123,12 +123,12 @@ namespace osu.Game.Tests.Visual.Gameplay int playerNumber = 1; - AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3); + AddRepeatStep("add 3 other players", () => leaderboardProvider.CreateRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3); AddUntilStep("no pink color scores", () => leaderboard.ChildrenOfType().Select(b => ((Colour4)b.Colour).ToHex()), () => Does.Not.Contain("#FF549A")); - AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3); + AddRepeatStep("add 3 friend score", () => leaderboardProvider.CreateRandomScore(friend), 3); AddUntilStep("at least one friend score is pink", () => leaderboard.GetAllScoresForUsername("my friend") .SelectMany(score => score.ChildrenOfType()) @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add local player", () => { playerScore.Value = 1222333; - createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); + leaderboardProvider.CreateLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); }); } @@ -159,14 +159,6 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user); - - private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) - { - var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score); - leaderboardProvider.Scores.Add(leaderboardScore); - } - private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard { public float Spacing => Flow.Spacing.Y; @@ -175,10 +167,20 @@ namespace osu.Game.Tests.Visual.Gameplay => Flow.Where(i => i.User?.Username == username); } - private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider + public class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider { - IBindableList IGameplayLeaderboardProvider.Scores => Scores; public BindableList Scores { get; } = new BindableList(); + + public GameplayLeaderboardScore CreateRandomScore(APIUser user) => CreateLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user); + + public GameplayLeaderboardScore CreateLeaderboardScore(BindableLong totalScore, APIUser user, bool isTracked = false) + { + var score = new GameplayLeaderboardScore(user, isTracked, totalScore); + Scores.Add(score); + return score; + } + + IBindableList IGameplayLeaderboardProvider.Scores => Scores; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs index 34e9080db3..a3581bd1e0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Online.API; @@ -13,6 +12,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Gameplay; +using osu.Game.Tests.Visual.Gameplay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -21,25 +21,27 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private OsuConfigManager config { get; set; } = null!; + private GameplayLeaderboardScore score = null!; + + private readonly Bindable position = new Bindable(50); + + private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!; + private MultiplayerPositionDisplay display = null!; + private GameplayState gameplayState = null!; + [Test] public void TestAppearance() { - TestGameplayLeaderboardProvider leaderboard = null!; - MultiplayerPositionDisplay display = null!; - GameplayState gameplayState = null!; - AddStep("create content", () => { - leaderboard = new TestGameplayLeaderboardProvider(); Children = new Drawable[] { - leaderboard, new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = [ - (typeof(IGameplayLeaderboardProvider), leaderboard), + (typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()), (typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset())) ], Child = display = new MultiplayerPositionDisplay @@ -49,18 +51,17 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }; - }); - AddSliderStep("set score position", 1, 100, 50, r => - { - if (leaderboard.IsNotNull() && leaderboard.Score.IsNotNull()) - leaderboard.Score.Position.Value = r; - }); - AddStep("unset position", () => leaderboard.Score.Position.Value = null); - AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true); + score.Position.BindTo(position); + }); + AddSliderStep("set score position", 1, 100, 50, r => position.Value = r); + AddStep("unset position", () => position.Value = null); + + AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); - AddStep("toggle leaderboard off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false)); + AddStep("toggle leaderboardProvider off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false)); AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); AddStep("enter break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Break); @@ -69,25 +70,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("exit break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing); AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); - AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser()); AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); } - - private partial class TestGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider - { - public GameplayLeaderboardScore Score { get; private set; } = null!; - - IBindableList IGameplayLeaderboardProvider.Scores => scores; - private readonly BindableList scores = new BindableList(); - - [BackgroundDependencyLoader] - private void load(IAPIProvider api) - { - scores.Add(Score = new GameplayLeaderboardScore(api.LocalUser.Value, true, new BindableLong())); - } - } } } From 91b9b41d580661edb1022ed54cd343b135b6b892 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 May 2025 16:30:38 +0900 Subject: [PATCH 4/8] Add bar display --- .../TestSceneMultiplayerPositionDisplay.cs | 14 +++- .../Multiplayer/MultiplayerPositionDisplay.cs | 78 +++++++++++++++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs index a3581bd1e0..42f34539de 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; @@ -23,12 +24,14 @@ namespace osu.Game.Tests.Visual.Multiplayer private GameplayLeaderboardScore score = null!; - private readonly Bindable position = new Bindable(50); + private readonly Bindable position = new Bindable(8); private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!; private MultiplayerPositionDisplay display = null!; private GameplayState gameplayState = null!; + private const int player_count = 32; + [Test] public void TestAppearance() { @@ -54,8 +57,15 @@ namespace osu.Game.Tests.Visual.Multiplayer score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true); score.Position.BindTo(position); + + for (int i = 0; i < player_count - 1; i++) + { + var r = leaderboardProvider.CreateRandomScore(new APIUser()); + r.Position.Value = i; + } }); - AddSliderStep("set score position", 1, 100, 50, r => position.Value = r); + + AddSliderStep("set score position", 1, player_count, position.Value!.Value, r => position.Value = r); AddStep("unset position", () => position.Value = null); AddStep("toggle leaderboardProvider on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index af847a7b51..5c5108d652 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -1,11 +1,15 @@ // 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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -13,6 +17,8 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Play; using osu.Game.Screens.Select.Leaderboards; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -27,6 +33,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private OsuSpriteText positionText = null!; + private Drawable localPlayerMarker = null!; + + private const float marker_size = 5; + private const float width = 90; + + private const float min_alpha = 0.2f; + private const float max_alpha = 0.4f; + [BackgroundDependencyLoader] private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState) { @@ -35,11 +49,48 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer configManager.BindWith(OsuSetting.GameplayLeaderboard, showLeaderboard); localUserPlayingState.BindTo(gameplayState.PlayingState); - AutoSizeAxes = Axes.Both; - InternalChild = positionText = new OsuSpriteText + AutoSizeAxes = Axes.Y; + Width = width; + + InternalChildren = new Drawable[] { - Alpha = 0.5f, - Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light), + positionText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Alpha = 0, + Padding = new MarginPadding { Right = -5 }, + Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true), + Spacing = new Vector2(-8, 0), + }, + new Container + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + RelativeSizeAxes = Axes.X, + Height = marker_size - 2, + Children = new[] + { + new Circle + { + Colour = ColourInfo.GradientHorizontal( + Color4.White.Opacity(max_alpha), + Color4.White.Opacity(min_alpha) + ), + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + localPlayerMarker = new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.Cyan, + Size = new Vector2(marker_size), + Blending = BlendingParameters.Additive, + Alpha = 0.4f, + }, + } + }, }; } @@ -52,7 +103,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer showLeaderboard.BindValueChanged(_ => updateState()); localUserPlayingState.BindValueChanged(_ => updateState(), true); - position.BindValueChanged(_ => positionText.Text = position.Value != null ? $@"#{position.Value.Value:N0}" : "-", true); + position.BindValueChanged(_ => + { + if (position.Value == null) + { + positionText.Alpha = 0; + positionText.Text = "-"; + localPlayerMarker.FadeOut(); + return; + } + + float relativePosition = (float)(position.Value.Value - 1) / scores.Count; + + positionText.Text = $@"#{position.Value.Value:N0}"; + positionText.Alpha = min_alpha + (max_alpha - min_alpha) * (1 - relativePosition); + + localPlayerMarker.FadeIn(); + localPlayerMarker.MoveToX(Math.Min(relativePosition * width, width - marker_size), 1000, Easing.OutQuint); + }, true); } private void updateState() From e7d26669c0d5db8c68059bfeb43501b0eedc51bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 May 2025 16:45:12 +0900 Subject: [PATCH 5/8] Improve transition fade in/out --- .../Multiplayer/MultiplayerPositionDisplay.cs | 86 +++++++++++++------ 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index 5c5108d652..91520566d7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerPositionDisplay : CompositeDrawable + public partial class MultiplayerPositionDisplay : VisibilityContainer { private readonly IBindable user = new Bindable(); private readonly IBindableList scores = new BindableList(); @@ -41,8 +41,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private const float min_alpha = 0.2f; private const float max_alpha = 0.4f; + private GameplayLeaderboardScore? userScore; + + protected override bool StartHidden => true; + [BackgroundDependencyLoader] - private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState) + private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState, OsuColour colours) { scores.BindTo(leaderboardProvider.Scores); user.BindTo(api.LocalUser); @@ -83,8 +87,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer localPlayerMarker = new Circle { Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.Cyan, + Origin = Anchor.Centre, + Colour = colours.Blue1, Size = new Vector2(marker_size), Blending = BlendingParameters.Additive, Alpha = 0.4f, @@ -98,42 +102,68 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - user.BindValueChanged(_ => updateState()); - scores.BindCollectionChanged((_, __) => updateState()); - showLeaderboard.BindValueChanged(_ => updateState()); - localUserPlayingState.BindValueChanged(_ => updateState(), true); + user.BindValueChanged(_ => updateScoreBindings()); + scores.BindCollectionChanged((_, __) => updateScoreBindings(), true); - position.BindValueChanged(_ => - { - if (position.Value == null) - { - positionText.Alpha = 0; - positionText.Text = "-"; - localPlayerMarker.FadeOut(); - return; - } + showLeaderboard.BindValueChanged(_ => updateVisibility()); + localUserPlayingState.BindValueChanged(_ => updateVisibility(), true); - float relativePosition = (float)(position.Value.Value - 1) / scores.Count; - - positionText.Text = $@"#{position.Value.Value:N0}"; - positionText.Alpha = min_alpha + (max_alpha - min_alpha) * (1 - relativePosition); - - localPlayerMarker.FadeIn(); - localPlayerMarker.MoveToX(Math.Min(relativePosition * width, width - marker_size), 1000, Easing.OutQuint); - }, true); + State.BindValueChanged(_ => updatePosition()); + position.BindValueChanged(_ => updatePosition(), true); } - private void updateState() + protected override void PopIn() + { + this.FadeIn(500, Easing.OutQuint); + localPlayerMarker.ScaleTo(Vector2.One, 500, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(500, Easing.OutQuint); + localPlayerMarker.ScaleTo(new Vector2(0.8f), 500, Easing.Out); + } + + private void updateVisibility() + { + bool shouldDisplay = userScore != null && (showLeaderboard.Value || localUserPlayingState.Value == LocalUserPlayingState.Break); + + State.Value = shouldDisplay ? Visibility.Visible : Visibility.Hidden; + } + + private void updateScoreBindings() { position.UnbindBindings(); - var userScore = scores.SingleOrDefault(s => s.User.Equals(user.Value)); + userScore = scores.SingleOrDefault(s => s.User.Equals(user.Value)); if (userScore != null) position.BindTo(userScore.Position); else position.Value = null; - Alpha = userScore != null && (showLeaderboard.Value || localUserPlayingState.Value == LocalUserPlayingState.Break) ? 1 : 0; + updatePosition(); + } + + private void updatePosition() + { + // only update when visible to delay animations. + if (State.Value != Visibility.Visible) return; + + if (position.Value == null) + { + positionText.Alpha = min_alpha; + positionText.Text = "-"; + localPlayerMarker.FadeOut(); + return; + } + + float relativePosition = (float)(position.Value.Value - 1) / scores.Count; + + positionText.Text = $@"#{position.Value.Value:N0}"; + positionText.Alpha = min_alpha + (max_alpha - min_alpha) * (1 - relativePosition); + + localPlayerMarker.FadeIn(); + localPlayerMarker.MoveToX(marker_size / 2 + Math.Min(relativePosition * (width - marker_size / 2), width - marker_size / 2), 1000, Easing.OutPow10); } } } From eaa7af58d5f5ed9874375ca70740e545696ed1ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 May 2025 16:57:57 +0900 Subject: [PATCH 6/8] Roll the rank counter instead of immediate updates --- .../Multiplayer/MultiplayerPositionDisplay.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index 91520566d7..2439d38c6f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -10,9 +10,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Play; @@ -31,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private readonly Bindable position = new Bindable(); - private OsuSpriteText positionText = null!; + private RollingCounter positionText = null!; private Drawable localPlayerMarker = null!; @@ -58,14 +60,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer InternalChildren = new Drawable[] { - positionText = new OsuSpriteText + positionText = new PositionCounter { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Alpha = 0, Padding = new MarginPadding { Right = -5 }, - Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true), - Spacing = new Vector2(-8, 0), }, new Container { @@ -152,18 +152,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (position.Value == null) { positionText.Alpha = min_alpha; - positionText.Text = "-"; + positionText.Current.Value = -1; localPlayerMarker.FadeOut(); return; } float relativePosition = (float)(position.Value.Value - 1) / scores.Count; - positionText.Text = $@"#{position.Value.Value:N0}"; - positionText.Alpha = min_alpha + (max_alpha - min_alpha) * (1 - relativePosition); + positionText.Current.Value = position.Value.Value; + positionText.FadeTo(min_alpha + (max_alpha - min_alpha) * (1 - relativePosition), 1000, Easing.OutPow10); localPlayerMarker.FadeIn(); localPlayerMarker.MoveToX(marker_size / 2 + Math.Min(relativePosition * (width - marker_size / 2), width - marker_size / 2), 1000, Easing.OutPow10); } + + private class PositionCounter : RollingCounter + { + protected override double RollingDuration => Current.Value > 0 ? 1000 : 0; + protected override Easing RollingEasing => Easing.OutPow10; + + protected override LocalisableString FormatCount(int count) + { + if (count <= 0) + return "-"; + + return "#" + base.FormatCount(count); + } + + protected override OsuSpriteText CreateSpriteText() + { + return new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true), + Spacing = new Vector2(-8, 0), + }; + } + } } } From 79f88528aee2c3b5f0ce4bebdee202d8e3b1cf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 May 2025 11:37:52 +0200 Subject: [PATCH 7/8] Fix code inspection --- .../OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index 2439d38c6f..e09d2ee7c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer localPlayerMarker.MoveToX(marker_size / 2 + Math.Min(relativePosition * (width - marker_size / 2), width - marker_size / 2), 1000, Easing.OutPow10); } - private class PositionCounter : RollingCounter + private partial class PositionCounter : RollingCounter { protected override double RollingDuration => Current.Value > 0 ? 1000 : 0; protected override Easing RollingEasing => Easing.OutPow10; From 1d3f4ac02b58ed682cafdc904c4a3168e4db48fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 13 May 2025 11:44:05 +0200 Subject: [PATCH 8/8] Fix multiplayer position display not hiding on user change --- .../Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index e09d2ee7c2..4be6e3b8c4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -141,6 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer else position.Value = null; + updateVisibility(); updatePosition(); }