1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 20:50:41 +08:00

Merge pull request #32972 from bdach/multiplayer-position-indicator

Add non-skinnable position indicator to multiplayer player
This commit is contained in:
Dean Herbert
2025-05-13 20:53:15 +09:00
committed by GitHub
Unverified
5 changed files with 317 additions and 27 deletions
@@ -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<Box>().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<Box>())
@@ -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<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
public BindableList<GameplayLeaderboardScore> Scores { get; } = new BindableList<GameplayLeaderboardScore>();
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<GameplayLeaderboardScore> IGameplayLeaderboardProvider.Scores => Scores;
}
}
}
@@ -0,0 +1,90 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
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;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private GameplayLeaderboardScore score = null!;
private readonly Bindable<int?> position = new Bindable<int?>(8);
private TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider leaderboardProvider = null!;
private MultiplayerPositionDisplay display = null!;
private GameplayState gameplayState = null!;
private const int player_count = 32;
[Test]
public void TestAppearance()
{
AddStep("create content", () =>
{
Children = new Drawable[]
{
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()),
(typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset()))
],
Child = display = new MultiplayerPositionDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
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, player_count, position.Value!.Value, 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 leaderboardProvider off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false));
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
AddStep("enter break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Break);
AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1));
AddStep("exit break", () => ((Bindable<LocalUserPlayingState>)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing);
AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0));
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));
}
}
}
@@ -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 =>
{
@@ -0,0 +1,193 @@
// 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.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.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;
using osu.Game.Screens.Select.Leaderboards;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerPositionDisplay : VisibilityContainer
{
private readonly IBindable<APIUser> user = new Bindable<APIUser>();
private readonly IBindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
private readonly BindableBool showLeaderboard = new BindableBool();
private readonly IBindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
private readonly Bindable<int?> position = new Bindable<int?>();
private RollingCounter<int> 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;
private GameplayLeaderboardScore? userScore;
protected override bool StartHidden => true;
[BackgroundDependencyLoader]
private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState, OsuColour colours)
{
scores.BindTo(leaderboardProvider.Scores);
user.BindTo(api.LocalUser);
configManager.BindWith(OsuSetting.GameplayLeaderboard, showLeaderboard);
localUserPlayingState.BindTo(gameplayState.PlayingState);
AutoSizeAxes = Axes.Y;
Width = width;
InternalChildren = new Drawable[]
{
positionText = new PositionCounter
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Alpha = 0,
Padding = new MarginPadding { Right = -5 },
},
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.Centre,
Colour = colours.Blue1,
Size = new Vector2(marker_size),
Blending = BlendingParameters.Additive,
Alpha = 0.4f,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
user.BindValueChanged(_ => updateScoreBindings());
scores.BindCollectionChanged((_, __) => updateScoreBindings(), true);
showLeaderboard.BindValueChanged(_ => updateVisibility());
localUserPlayingState.BindValueChanged(_ => updateVisibility(), true);
State.BindValueChanged(_ => updatePosition());
position.BindValueChanged(_ => updatePosition(), true);
}
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();
userScore = scores.SingleOrDefault(s => s.User.Equals(user.Value));
if (userScore != null)
position.BindTo(userScore.Position);
else
position.Value = null;
updateVisibility();
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.Current.Value = -1;
localPlayerMarker.FadeOut();
return;
}
float relativePosition = (float)(position.Value.Value - 1) / scores.Count;
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 partial class PositionCounter : RollingCounter<int>
{
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),
};
}
}
}
}
+7 -7
View File
@@ -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<bool> IsPlaying = new Bindable<bool>();
@@ -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)
{