mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 05:42:56 +08:00
Merge pull request #31526 from bdach/spectator-list-visuals
Implement spectator list display
This commit is contained in:
commit
3272224a28
53
osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
Normal file
53
osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,9 +15,11 @@ namespace osu.Game.Graphics.Containers
|
|||||||
{
|
{
|
||||||
protected const float FADE_DURATION = 500;
|
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 };
|
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
|
||||||
|
|
||||||
@ -67,18 +69,18 @@ namespace osu.Game.Graphics.Containers
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
if (HoverColour == default)
|
fallbackHoverColour = colours.Yellow;
|
||||||
HoverColour = colours.Yellow;
|
fallbackIdleColour = Color4.White;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
osu.Game/Localisation/HUD/SpectatorListStrings.cs
Normal file
19
osu.Game/Localisation/HUD/SpectatorListStrings.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ namespace osu.Game.Online.Chat
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
IdleColour = overlayColourProvider?.Light2 ?? colours.Blue;
|
IdleColour ??= overlayColourProvider?.Light2 ?? colours.Blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<Drawable> EffectTargets => Parts;
|
protected override IEnumerable<Drawable> EffectTargets => Parts;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -200,16 +201,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
|||||||
|
|
||||||
case FriendStatus.NotMutual:
|
case FriendStatus.NotMutual:
|
||||||
IdleColour = colour.Green.Opacity(0.7f);
|
IdleColour = colour.Green.Opacity(0.7f);
|
||||||
HoverColour = IdleColour.Lighten(0.1f);
|
HoverColour = IdleColour.Value.Lighten(0.1f);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FriendStatus.Mutual:
|
case FriendStatus.Mutual:
|
||||||
IdleColour = colour.Pink.Opacity(0.7f);
|
IdleColour = colour.Pink.Opacity(0.7f);
|
||||||
HoverColour = IdleColour.Lighten(0.1f);
|
HoverColour = IdleColour.Value.Lighten(0.1f);
|
||||||
break;
|
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
|
private enum FriendStatus
|
||||||
|
242
osu.Game/Screens/Play/HUD/SpectatorList.cs
Normal file
242
osu.Game/Screens/Play/HUD/SpectatorList.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user