Skip to content

Commit 3272224

Browse files
authored
Merge pull request #31526 from bdach/spectator-list-visuals
Implement spectator list display
2 parents 70c81b1 + 81f5450 commit 3272224

File tree

6 files changed

+331
-11
lines changed

6 files changed

+331
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System.Threading;
5+
using NUnit.Framework;
6+
using osu.Framework.Bindables;
7+
using osu.Framework.Graphics;
8+
using osu.Framework.Utils;
9+
using osu.Game.Graphics;
10+
using osu.Game.Screens.Play;
11+
using osu.Game.Screens.Play.HUD;
12+
13+
namespace osu.Game.Tests.Visual.Gameplay
14+
{
15+
[TestFixture]
16+
public partial class TestSceneSpectatorList : OsuTestScene
17+
{
18+
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
19+
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
20+
21+
private int counter;
22+
23+
[Test]
24+
public void TestBasics()
25+
{
26+
SpectatorList list = null!;
27+
AddStep("create spectator list", () => Child = list = new SpectatorList
28+
{
29+
Anchor = Anchor.Centre,
30+
Origin = Anchor.Centre,
31+
Spectators = { BindTarget = spectators },
32+
UserPlayingState = { BindTarget = localUserPlayingState }
33+
});
34+
35+
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
36+
37+
AddRepeatStep("add a user", () =>
38+
{
39+
int id = Interlocked.Increment(ref counter);
40+
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
41+
}, 10);
42+
43+
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
44+
45+
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
46+
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
47+
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
48+
49+
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break);
50+
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying);
51+
}
52+
}
53+
}

osu.Game/Graphics/Containers/OsuHoverContainer.cs

+9-7
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ public partial class OsuHoverContainer : OsuClickableContainer
1515
{
1616
protected const float FADE_DURATION = 500;
1717

18-
protected Color4 HoverColour;
18+
public Color4? HoverColour { get; set; }
19+
private Color4 fallbackHoverColour;
1920

20-
protected Color4 IdleColour = Color4.White;
21+
public Color4? IdleColour { get; set; }
22+
private Color4 fallbackIdleColour;
2123

2224
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
2325

@@ -67,18 +69,18 @@ protected override void OnHoverLost(HoverLostEvent e)
6769
[BackgroundDependencyLoader]
6870
private void load(OsuColour colours)
6971
{
70-
if (HoverColour == default)
71-
HoverColour = colours.Yellow;
72+
fallbackHoverColour = colours.Yellow;
73+
fallbackIdleColour = Color4.White;
7274
}
7375

7476
protected override void LoadComplete()
7577
{
7678
base.LoadComplete();
77-
EffectTargets.ForEach(d => d.FadeColour(IdleColour));
79+
EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour));
7880
}
7981

80-
private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint));
82+
private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour ?? fallbackHoverColour, FADE_DURATION, Easing.OutQuint));
8183

82-
private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint));
84+
private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour, FADE_DURATION, Easing.OutQuint));
8385
}
8486
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using osu.Framework.Localisation;
5+
6+
namespace osu.Game.Localisation.HUD
7+
{
8+
public static class SpectatorListStrings
9+
{
10+
private const string prefix = @"osu.Game.Resources.Localisation.SpectatorList";
11+
12+
/// <summary>
13+
/// "Spectators ({0})"
14+
/// </summary>
15+
public static LocalisableString SpectatorCount(int arg0) => new TranslatableString(getKey(@"spectator_count"), @"Spectators ({0})", arg0);
16+
17+
private static string getKey(string key) => $@"{prefix}:{key}";
18+
}
19+
}

osu.Game/Online/Chat/DrawableLinkCompiler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public DrawableLinkCompiler(IEnumerable<Drawable> parts)
5656
[BackgroundDependencyLoader]
5757
private void load(OsuColour colours)
5858
{
59-
IdleColour = overlayColourProvider?.Light2 ?? colours.Blue;
59+
IdleColour ??= overlayColourProvider?.Light2 ?? colours.Blue;
6060
}
6161

6262
protected override IEnumerable<Drawable> EffectTargets => Parts;

osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
33

4+
using System;
45
using System.Linq;
56
using osu.Framework.Allocation;
67
using osu.Framework.Bindables;
@@ -200,16 +201,19 @@ private void updateColor()
200201

201202
case FriendStatus.NotMutual:
202203
IdleColour = colour.Green.Opacity(0.7f);
203-
HoverColour = IdleColour.Lighten(0.1f);
204+
HoverColour = IdleColour.Value.Lighten(0.1f);
204205
break;
205206

206207
case FriendStatus.Mutual:
207208
IdleColour = colour.Pink.Opacity(0.7f);
208-
HoverColour = IdleColour.Lighten(0.1f);
209+
HoverColour = IdleColour.Value.Lighten(0.1f);
209210
break;
211+
212+
default:
213+
throw new ArgumentOutOfRangeException();
210214
}
211215

212-
EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour : IdleColour, FADE_DURATION, Easing.OutQuint));
216+
EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour.Value : IdleColour.Value, FADE_DURATION, Easing.OutQuint));
213217
}
214218

215219
private enum FriendStatus
+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Collections.Specialized;
6+
using osu.Framework.Allocation;
7+
using osu.Framework.Bindables;
8+
using osu.Framework.Extensions.Color4Extensions;
9+
using osu.Framework.Extensions.LocalisationExtensions;
10+
using osu.Framework.Graphics;
11+
using osu.Framework.Graphics.Colour;
12+
using osu.Framework.Graphics.Containers;
13+
using osu.Framework.Graphics.Pooling;
14+
using osu.Game.Configuration;
15+
using osu.Game.Graphics;
16+
using osu.Game.Graphics.Sprites;
17+
using osu.Game.Online.Chat;
18+
using osu.Game.Users;
19+
using osu.Game.Localisation.HUD;
20+
using osu.Game.Localisation.SkinComponents;
21+
using osuTK.Graphics;
22+
23+
namespace osu.Game.Screens.Play.HUD
24+
{
25+
public partial class SpectatorList : CompositeDrawable
26+
{
27+
private const int max_spectators_displayed = 10;
28+
29+
public BindableList<Spectator> Spectators { get; } = new BindableList<Spectator>();
30+
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
31+
32+
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
33+
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
34+
35+
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
36+
public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White);
37+
38+
protected OsuSpriteText Header { get; private set; } = null!;
39+
40+
private FillFlowContainer mainFlow = null!;
41+
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
42+
private DrawablePool<SpectatorListEntry> pool = null!;
43+
44+
[BackgroundDependencyLoader]
45+
private void load(OsuColour colours)
46+
{
47+
AutoSizeAxes = Axes.Y;
48+
49+
InternalChildren = new Drawable[]
50+
{
51+
mainFlow = new FillFlowContainer
52+
{
53+
AutoSizeAxes = Axes.Both,
54+
Direction = FillDirection.Vertical,
55+
Children = new Drawable[]
56+
{
57+
Header = new OsuSpriteText
58+
{
59+
Colour = colours.Blue0,
60+
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
61+
},
62+
spectatorsFlow = new FillFlowContainer<SpectatorListEntry>
63+
{
64+
AutoSizeAxes = Axes.Both,
65+
Direction = FillDirection.Vertical,
66+
}
67+
}
68+
},
69+
pool = new DrawablePool<SpectatorListEntry>(max_spectators_displayed),
70+
};
71+
72+
HeaderColour.Value = Header.Colour;
73+
}
74+
75+
protected override void LoadComplete()
76+
{
77+
base.LoadComplete();
78+
79+
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
80+
UserPlayingState.BindValueChanged(_ => updateVisibility());
81+
82+
Font.BindValueChanged(_ => updateAppearance());
83+
HeaderColour.BindValueChanged(_ => updateAppearance(), true);
84+
FinishTransforms(true);
85+
86+
this.FadeInFromZero(200, Easing.OutQuint);
87+
}
88+
89+
private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
90+
{
91+
switch (e.Action)
92+
{
93+
case NotifyCollectionChangedAction.Add:
94+
{
95+
for (int i = 0; i < e.NewItems!.Count; i++)
96+
{
97+
var spectator = (Spectator)e.NewItems![i]!;
98+
int index = Math.Max(e.NewStartingIndex, 0) + i;
99+
100+
if (index >= max_spectators_displayed)
101+
break;
102+
103+
addNewSpectatorToList(index, spectator);
104+
}
105+
106+
break;
107+
}
108+
109+
case NotifyCollectionChangedAction.Remove:
110+
{
111+
spectatorsFlow.RemoveAll(entry => e.OldItems!.Contains(entry.Current.Value), false);
112+
113+
for (int i = 0; i < spectatorsFlow.Count; i++)
114+
spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i);
115+
116+
if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
117+
{
118+
for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++)
119+
addNewSpectatorToList(i, Spectators[i]);
120+
}
121+
122+
break;
123+
}
124+
125+
case NotifyCollectionChangedAction.Reset:
126+
{
127+
spectatorsFlow.Clear(false);
128+
break;
129+
}
130+
131+
default:
132+
throw new NotSupportedException();
133+
}
134+
135+
Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper();
136+
updateVisibility();
137+
138+
for (int i = 0; i < spectatorsFlow.Count; i++)
139+
{
140+
spectatorsFlow[i].Colour = i < max_spectators_displayed - 1
141+
? Color4.White
142+
: ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0));
143+
}
144+
}
145+
146+
private void addNewSpectatorToList(int i, Spectator spectator)
147+
{
148+
var entry = pool.Get(entry =>
149+
{
150+
entry.Current.Value = spectator;
151+
entry.UserPlayingState = UserPlayingState;
152+
});
153+
154+
spectatorsFlow.Insert(i, entry);
155+
}
156+
157+
private void updateVisibility()
158+
{
159+
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
160+
}
161+
162+
private void updateAppearance()
163+
{
164+
Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
165+
Header.Colour = HeaderColour.Value;
166+
167+
Width = Header.DrawWidth;
168+
}
169+
170+
private partial class SpectatorListEntry : PoolableDrawable
171+
{
172+
public Bindable<Spectator> Current { get; } = new Bindable<Spectator>();
173+
174+
private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>();
175+
176+
public Bindable<LocalUserPlayingState> UserPlayingState
177+
{
178+
get => current.Current;
179+
set => current.Current = value;
180+
}
181+
182+
private OsuSpriteText username = null!;
183+
private DrawableLinkCompiler? linkCompiler;
184+
185+
[Resolved]
186+
private OsuGame? game { get; set; }
187+
188+
[BackgroundDependencyLoader]
189+
private void load()
190+
{
191+
AutoSizeAxes = Axes.Both;
192+
193+
InternalChildren = new Drawable[]
194+
{
195+
username = new OsuSpriteText(),
196+
};
197+
}
198+
199+
protected override void LoadComplete()
200+
{
201+
base.LoadComplete();
202+
UserPlayingState.BindValueChanged(_ => updateEnabledState());
203+
Current.BindValueChanged(_ => updateState(), true);
204+
}
205+
206+
protected override void PrepareForUse()
207+
{
208+
base.PrepareForUse();
209+
210+
username.MoveToX(10)
211+
.Then()
212+
.MoveToX(0, 400, Easing.OutQuint);
213+
214+
this.FadeInFromZero(400, Easing.OutQuint);
215+
}
216+
217+
private void updateState()
218+
{
219+
username.Text = Current.Value.Username;
220+
linkCompiler?.Expire();
221+
AddInternal(linkCompiler = new DrawableLinkCompiler([username])
222+
{
223+
IdleColour = Colour4.White,
224+
Action = () => game?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, Current.Value)),
225+
});
226+
updateEnabledState();
227+
}
228+
229+
private void updateEnabledState()
230+
{
231+
if (linkCompiler != null)
232+
linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing;
233+
}
234+
}
235+
236+
public record Spectator(int OnlineID, string Username) : IUser
237+
{
238+
public CountryCode CountryCode => CountryCode.Unknown;
239+
public bool IsBot => false;
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)