1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-31 09:33:20 +08:00

Merge pull request #31527 from bdach/spectator-list-ready

Show spectating users during gameplay
This commit is contained in:
Dan Balasescu 2025-01-21 18:54:11 +09:00 committed by GitHub
commit 8f8246278a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 267 additions and 42 deletions

View File

@ -4,6 +4,7 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (keyCounter != null)
{
@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
new SpectatorList(),
}
};
}

View File

@ -9,7 +9,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
})
{
new ArgonManiaComboCounter(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
};
}

View File

@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
new LegacyManiaComboCounter(),
new SpectatorList(),
};
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
}
})
{
@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(),
new SpectatorList(),
}
};
}

View File

@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-classic-20240724.osk",
// Covers skinnable mod display
"Archives/modified-default-20241207.osk",
// Covers skinnable spectator list
"Archives/modified-argon-20250116.osk",
};
/// <summary>

View File

@ -6,48 +6,74 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
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
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
AddStep("create spectator list", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spectators = { BindTarget = spectators },
UserPlayingState = { BindTarget = localUserPlayingState }
Children = new Drawable[]
{
client,
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
],
Child = list = new SpectatorList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
});
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing);
AddRepeatStep("add a user", () =>
{
int id = Interlocked.Increment(ref counter);
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
((ISpectatorClient)client).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
Username = $"User {id}"
}
]);
}, 10);
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 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);
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying);
}
}
}

View File

@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator
/// <param name="userId">The ID of the user who achieved the score.</param>
/// <param name="scoreId">The ID of the score.</param>
Task UserScoreProcessed(int userId, long scoreId);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.StartWatchingUser">started watching this client</see>.
/// </summary>
/// <param name="user">The information about the user who started watching.</param>
Task UserStartedWatching(SpectatorUser[] user);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.EndWatchingUser">ended watching this client</see>
/// </summary>
/// <param name="userId">The ID of the user who ended watching.</param>
Task UserEndedWatching(int userId);
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
};

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@ -36,10 +37,16 @@ namespace osu.Game.Online.Spectator
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// The states of all users currently being watched.
/// The states of all users currently being watched by the local user.
/// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
/// <summary>
/// All users who are currently watching the local user.
/// </summary>
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
/// <summary>
/// A global list of all players currently playing.
/// </summary>
@ -53,6 +60,7 @@ namespace osu.Game.Online.Spectator
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual event Action<int, FrameDataBundle>? OnNewFrames;
/// <summary>
@ -82,6 +90,7 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState();
@ -127,6 +136,7 @@ namespace osu.Game.Online.Spectator
{
playingUsers.Clear();
watchedUserStates.Clear();
watchingUsers.Clear();
}
}), true);
}
@ -179,6 +189,30 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
{
Schedule(() =>
{
foreach (var user in users)
{
if (!watchingUsers.Contains(user))
watchingUsers.Add(user);
}
});
return Task.CompletedTask;
}
Task ISpectatorClient.UserEndedWatching(int userId)
{
Schedule(() =>
{
watchingUsers.RemoveAll(u => u.OnlineID == userId);
});
return Task.CompletedTask;
}
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() => DisconnectInternal());

View File

@ -0,0 +1,39 @@
// 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 MessagePack;
using osu.Game.Users;
namespace osu.Game.Online.Spectator
{
[Serializable]
[MessagePackObject]
public class SpectatorUser : IUser, IEquatable<SpectatorUser>
{
[Key(0)]
public int OnlineID { get; set; }
[Key(1)]
public string Username { get; set; } = string.Empty;
[IgnoreMember]
public CountryCode CountryCode => CountryCode.Unknown;
[IgnoreMember]
public bool IsBot => false;
public bool Equals(SpectatorUser? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return OnlineID == other.OnlineID;
}
public override bool Equals(object? obj) => Equals(obj as SpectatorUser);
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
}

View File

@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
/// <summary>
/// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar).
/// </summary>
public IBindable<LocalUserPlayingState> PlayingState { get; } = new Bindable<LocalUserPlayingState>();
public GameplayState(
IBeatmap beatmap,
Ruleset ruleset,
@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play
Score? score = null,
ScoreProcessor? scoreProcessor = null,
HealthProcessor? healthProcessor = null,
Storyboard? storyboard = null)
Storyboard? storyboard = null,
IBindable<LocalUserPlayingState>? localUserPlayingState = null)
{
Beatmap = beatmap;
Ruleset = ruleset;
@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime);
Storyboard = storyboard ?? new Storyboard();
if (localUserPlayingState != null)
PlayingState.BindTo(localUserPlayingState);
}
/// <summary>

View File

@ -15,18 +15,20 @@ 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 osu.Game.Online.Spectator;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class SpectatorList : CompositeDrawable
public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
{
private const int max_spectators_displayed = 10;
public BindableList<Spectator> Spectators { get; } = new BindableList<Spectator>();
public BindableList<SpectatorUser> Spectators { get; } = new BindableList<SpectatorUser>();
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
@ -41,13 +43,20 @@ namespace osu.Game.Screens.Play.HUD
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
private DrawablePool<SpectatorListEntry> pool = null!;
[Resolved]
private SpectatorClient client { get; set; } = null!;
[Resolved]
private GameplayState gameplayState { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
Empty().With(t => t.Size = new Vector2(100, 50)),
mainFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -76,6 +85,9 @@ namespace osu.Game.Screens.Play.HUD
{
base.LoadComplete();
((IBindableList<SpectatorUser>)Spectators).BindTo(client.WatchingUsers);
((IBindable<LocalUserPlayingState>)UserPlayingState).BindTo(gameplayState.PlayingState);
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
UserPlayingState.BindValueChanged(_ => updateVisibility());
@ -94,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD
{
for (int i = 0; i < e.NewItems!.Count; i++)
{
var spectator = (Spectator)e.NewItems![i]!;
var spectator = (SpectatorUser)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i;
if (index >= max_spectators_displayed)
@ -143,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD
}
}
private void addNewSpectatorToList(int i, Spectator spectator)
private void addNewSpectatorToList(int i, SpectatorUser spectator)
{
var entry = pool.Get(entry =>
{
@ -156,6 +168,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateVisibility()
{
// We don't want to show spectators when we are watching a replay.
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
}
@ -169,7 +182,7 @@ namespace osu.Game.Screens.Play.HUD
private partial class SpectatorListEntry : PoolableDrawable
{
public Bindable<Spectator> Current { get; } = new Bindable<Spectator>();
public Bindable<SpectatorUser> Current { get; } = new Bindable<SpectatorUser>();
private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>();
@ -233,10 +246,6 @@ namespace osu.Game.Screens.Play.HUD
}
}
public record Spectator(int OnlineID, string Username) : IUser
{
public CountryCode CountryCode => CountryCode.Unknown;
public bool IsBot => false;
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard));
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
GameplayClockContainer.Add(new GameplayScrollWheelHandling());

View File

@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
@ -110,15 +109,37 @@ namespace osu.Game.Skinning
case GlobalSkinnableContainers.MainHUDComponents:
if (containerLookup.Ruleset != null)
{
return new Container
return new DefaultSkinComponentsContainer(container =>
{
var comboCounter = container.OfType<ArgonComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2(36, -66);
if (comboCounter != null)
{
comboCounter.Position = pos;
pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20);
}
if (spectatorList != null)
spectatorList.Position = pos;
})
{
RelativeSizeAxes = Axes.Both,
Child = new ArgonComboCounter
Children = new Drawable[]
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Position = new Vector2(36, -66),
Scale = new Vector2(1.3f),
new ArgonComboCounter
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Scale = new Vector2(1.3f),
},
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
},
};
}

View File

@ -367,16 +367,29 @@ namespace osu.Game.Skinning
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
}
})
{
new LegacyDefaultComboCounter()
new LegacyDefaultComboCounter(),
new SpectatorList(),
};
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
@ -90,6 +91,7 @@ namespace osu.Game.Skinning
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<DefaultSongProgress>().FirstOrDefault();
var keyCounter = container.OfType<DefaultKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (score != null)
{
@ -142,17 +144,26 @@ namespace osu.Game.Skinning
}
}
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
if (songProgress != null && keyCounter != null)
{
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
keyCounter.Anchor = Anchor.BottomRight;
keyCounter.Origin = Anchor.BottomRight;
keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding));
}
if (spectatorList != null)
{
spectatorList.Font.Value = Typeface.Venera;
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding));
}
})
{
Children = new Drawable[]
@ -165,7 +176,8 @@ namespace osu.Game.Skinning
new DefaultKeyCounterDisplay(),
new BarHitErrorMeter(),
new BarHitErrorMeter(),
new TrianglesPerformancePointsCounter()
new TrianglesPerformancePointsCounter(),
new SpectatorList(),
}
};