1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 06:22:56 +08:00

Merge pull request #31526 from bdach/spectator-list-visuals

Implement spectator list display
This commit is contained in:
Dean Herbert 2025-01-17 10:26:02 +09:00 committed by GitHub
commit 3272224a28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 331 additions and 11 deletions

View File

@ -0,0 +1,53 @@
// 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.Threading;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public partial class TestSceneSpectatorList : OsuTestScene
{
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
private int counter;
[Test]
public void TestBasics()
{
SpectatorList list = null!;
AddStep("create spectator list", () => Child = list = new SpectatorList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spectators = { BindTarget = spectators },
UserPlayingState = { BindTarget = localUserPlayingState }
});
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
AddRepeatStep("add a user", () =>
{
int id = Interlocked.Increment(ref counter);
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
}, 10);
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying);
}
}
}

View File

@ -15,9 +15,11 @@ namespace osu.Game.Graphics.Containers
{
protected const float FADE_DURATION = 500;
protected Color4 HoverColour;
public Color4? HoverColour { get; set; }
private Color4 fallbackHoverColour;
protected Color4 IdleColour = Color4.White;
public Color4? IdleColour { get; set; }
private Color4 fallbackIdleColour;
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
@ -67,18 +69,18 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (HoverColour == default)
HoverColour = colours.Yellow;
fallbackHoverColour = colours.Yellow;
fallbackIdleColour = Color4.White;
}
protected override void LoadComplete()
{
base.LoadComplete();
EffectTargets.ForEach(d => d.FadeColour(IdleColour));
EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour));
}
private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint));
private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour ?? fallbackHoverColour, FADE_DURATION, Easing.OutQuint));
private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint));
private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour, FADE_DURATION, Easing.OutQuint));
}
}

View File

@ -0,0 +1,19 @@
// 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 osu.Framework.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class SpectatorListStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SpectatorList";
/// <summary>
/// "Spectators ({0})"
/// </summary>
public static LocalisableString SpectatorCount(int arg0) => new TranslatableString(getKey(@"spectator_count"), @"Spectators ({0})", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Online.Chat
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IdleColour = overlayColourProvider?.Light2 ?? colours.Blue;
IdleColour ??= overlayColourProvider?.Light2 ?? colours.Blue;
}
protected override IEnumerable<Drawable> EffectTargets => Parts;

View File

@ -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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -200,16 +201,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
case FriendStatus.NotMutual:
IdleColour = colour.Green.Opacity(0.7f);
HoverColour = IdleColour.Lighten(0.1f);
HoverColour = IdleColour.Value.Lighten(0.1f);
break;
case FriendStatus.Mutual:
IdleColour = colour.Pink.Opacity(0.7f);
HoverColour = IdleColour.Lighten(0.1f);
HoverColour = IdleColour.Value.Lighten(0.1f);
break;
default:
throw new ArgumentOutOfRangeException();
}
EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour : IdleColour, FADE_DURATION, Easing.OutQuint));
EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour.Value : IdleColour.Value, FADE_DURATION, Easing.OutQuint));
}
private enum FriendStatus

View File

@ -0,0 +1,242 @@
// 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.Collections.Specialized;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Users;
using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class SpectatorList : CompositeDrawable
{
private const int max_spectators_displayed = 10;
public BindableList<Spectator> Spectators { get; } = new BindableList<Spectator>();
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White);
protected OsuSpriteText Header { get; private set; } = null!;
private FillFlowContainer mainFlow = null!;
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
private DrawablePool<SpectatorListEntry> pool = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
mainFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header = new OsuSpriteText
{
Colour = colours.Blue0,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
},
spectatorsFlow = new FillFlowContainer<SpectatorListEntry>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
}
}
},
pool = new DrawablePool<SpectatorListEntry>(max_spectators_displayed),
};
HeaderColour.Value = Header.Colour;
}
protected override void LoadComplete()
{
base.LoadComplete();
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
UserPlayingState.BindValueChanged(_ => updateVisibility());
Font.BindValueChanged(_ => updateAppearance());
HeaderColour.BindValueChanged(_ => updateAppearance(), true);
FinishTransforms(true);
this.FadeInFromZero(200, Easing.OutQuint);
}
private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
for (int i = 0; i < e.NewItems!.Count; i++)
{
var spectator = (Spectator)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i;
if (index >= max_spectators_displayed)
break;
addNewSpectatorToList(index, spectator);
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
spectatorsFlow.RemoveAll(entry => e.OldItems!.Contains(entry.Current.Value), false);
for (int i = 0; i < spectatorsFlow.Count; i++)
spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i);
if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
{
for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++)
addNewSpectatorToList(i, Spectators[i]);
}
break;
}
case NotifyCollectionChangedAction.Reset:
{
spectatorsFlow.Clear(false);
break;
}
default:
throw new NotSupportedException();
}
Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper();
updateVisibility();
for (int i = 0; i < spectatorsFlow.Count; i++)
{
spectatorsFlow[i].Colour = i < max_spectators_displayed - 1
? Color4.White
: ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0));
}
}
private void addNewSpectatorToList(int i, Spectator spectator)
{
var entry = pool.Get(entry =>
{
entry.Current.Value = spectator;
entry.UserPlayingState = UserPlayingState;
});
spectatorsFlow.Insert(i, entry);
}
private void updateVisibility()
{
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
}
private void updateAppearance()
{
Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
Header.Colour = HeaderColour.Value;
Width = Header.DrawWidth;
}
private partial class SpectatorListEntry : PoolableDrawable
{
public Bindable<Spectator> Current { get; } = new Bindable<Spectator>();
private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>();
public Bindable<LocalUserPlayingState> UserPlayingState
{
get => current.Current;
set => current.Current = value;
}
private OsuSpriteText username = null!;
private DrawableLinkCompiler? linkCompiler;
[Resolved]
private OsuGame? game { get; set; }
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
username = new OsuSpriteText(),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UserPlayingState.BindValueChanged(_ => updateEnabledState());
Current.BindValueChanged(_ => updateState(), true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
username.MoveToX(10)
.Then()
.MoveToX(0, 400, Easing.OutQuint);
this.FadeInFromZero(400, Easing.OutQuint);
}
private void updateState()
{
username.Text = Current.Value.Username;
linkCompiler?.Expire();
AddInternal(linkCompiler = new DrawableLinkCompiler([username])
{
IdleColour = Colour4.White,
Action = () => game?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, Current.Value)),
});
updateEnabledState();
}
private void updateEnabledState()
{
if (linkCompiler != null)
linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing;
}
}
public record Spectator(int OnlineID, string Username) : IUser
{
public CountryCode CountryCode => CountryCode.Unknown;
public bool IsBot => false;
}
}
}