using System; using System.Collections.Generic; using System.Diagnostics; using Unity.Collections; using Unity.Entities; using UnityEngine; namespace Unity.NetCode.Samples.PlayerList { /// Simple IMGUI example rendering of a player scoreboard with connect/disconnect notifications. public class RenderPlayerListMb : MonoBehaviour { public AnimationCurve notificationOpacityCurve = AnimationCurve.Linear(0, 1, 1, 0); public GUISkin skin; public bool debugShowThinClients; GUILayoutOption m_LabelWidth = GUILayout.Width(300-40); GUILayoutOption m_NetIdWidth = GUILayout.Width(35); GUILayoutOption m_ScoreboardWidth = GUILayout.Width(300); Dictionary m_WorldCaches = new Dictionary(1); ulong m_LastSequenceNumber; class WorldCache { public EntityQuery NetworkIdQuery; public EntityQuery DesiredUsernameQuery; public EntityQuery PlayerListNotificationBufferQuery; public EntityQuery PlayerListBufferEntryQuery; public string DesiredUsername; public bool IsModifying; } void Update() { if (World.NextSequenceNumber != m_LastSequenceNumber) { foreach (var world in World.All) { if(world.SequenceNumber >= m_LastSequenceNumber && world.IsClient()) { if (!world.IsThinClient() || (debugShowThinClients && world.IsThinClient())) { if (!m_WorldCaches.ContainsKey(world)) { m_WorldCaches[world] = new WorldCache { NetworkIdQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()), DesiredUsernameQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadWrite()), PlayerListBufferEntryQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()), PlayerListNotificationBufferQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()) }; } } } } m_LastSequenceNumber = World.NextSequenceNumber; CleanUpWorldCache(); } } void CleanUpWorldCache() { bool repeat; do { repeat = false; foreach (var kvp in m_WorldCaches) { if (!kvp.Key.IsCreated) { m_WorldCaches.Remove(kvp.Key); repeat = true; break; } } } while (repeat); } void OnGUI() { GUI.skin = skin; GUILayout.BeginHorizontal(); { // Clients: foreach (var worldKvp in m_WorldCaches) { GUILayout.BeginVertical(); DrawPlayerListForClientWorld(worldKvp); GUILayout.EndVertical(); } } GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } void DrawPlayerListForClientWorld(KeyValuePair kvp) { var (world, cache) = kvp; if (!world.IsCreated) { return; } if (cache.DesiredUsernameQuery.IsEmptyIgnoreFilter) { return; } GUI.color = Color.yellow; if(debugShowThinClients) GUILayout.Box(world.Name); GUILayout.Box("PLAYER LIST", m_ScoreboardWidth); if (!cache.IsModifying) cache.DesiredUsername = cache.DesiredUsernameQuery.GetSingleton().Value.ToString(); var newNameString = GUILayout.TextField(cache.DesiredUsername, m_ScoreboardWidth); if (newNameString != cache.DesiredUsername) { cache.DesiredUsername = newNameString; cache.IsModifying = true; } if (cache.IsModifying) { var hasNoControlFocus = GUIUtility.keyboardControl == 0; var returnKeyPressed = Event.current.keyCode is KeyCode.Return or KeyCode.KeypadEnter; if (hasNoControlFocus || returnKeyPressed) { ref var desiredUsernameStoreRw = ref cache.DesiredUsernameQuery.GetSingletonRW().ValueRW; FixedStringMethods.CopyFromTruncated(ref desiredUsernameStoreRw.Value, newNameString); } } GUI.color = Color.grey; var networkId = cache.NetworkIdQuery.IsEmptyIgnoreFilter ? -1 : cache.NetworkIdQuery.GetSingleton().Value; if (networkId <= 0) { GUILayout.Box("Disconnected."); return; } DrawPlayerList(cache); DrawPlayerNotifications(cache); } void DrawPlayerNotifications(WorldCache worldCache) { if (worldCache.PlayerListNotificationBufferQuery.IsEmptyIgnoreFilter) return; GUILayout.EndVertical(); GUILayout.BeginVertical(); var notifications = worldCache.PlayerListNotificationBufferQuery.GetSingletonBuffer(); GUI.color = Color.yellow; GUILayout.Box("NOTIFICATIONS"); GUI.color = Color.white; var now = Stopwatch.GetTimestamp(); foreach (var entry in notifications) DrawNotification(entry, now); } void DrawPlayerList(WorldCache cache) { if (cache.PlayerListBufferEntryQuery.IsEmptyIgnoreFilter) return; var players = cache.PlayerListBufferEntryQuery.GetSingletonBuffer(); var drawnPlayers = 0; GUI.color = Color.white; for (var i = 0; i < players.Length; i++) { var entry = players[i]; if (!entry.IsCreated || !entry.State.IsConnected) continue; drawnPlayers++; GUILayout.BeginHorizontal(); { GUI.color = entry.State.IsConnected ? entry.State.Username.Value.IsEmpty ? Color.grey : Color.white : Color.red; GUILayout.Box(entry.State.NetworkId.ToString(), m_NetIdWidth); GUILayout.Label(entry.State.Username.Value.Value, m_LabelWidth); } GUILayout.EndHorizontal(); } if (drawnPlayers == 0) GUILayout.Box("No players."); } void DrawNotification(PlayerListNotificationBuffer entry, long now) { string notificationText; var username = entry.Event.Username.Value; Color color; if (entry.Event.IsConnected) { notificationText = $"{username} connected!"; color = Color.green; } else { switch (entry.Event.Reason) { case NetworkStreamDisconnectReason.Timeout: notificationText = $"{username} timed out!"; color = Color.red; break; case NetworkStreamDisconnectReason.MaxConnectionAttempts: notificationText = $"{username} exceeded max connection attempts!"; color = Color.red; break; case NetworkStreamDisconnectReason.ClosedByRemote: notificationText = $"{username} quit!"; color = new Color(1f, 0.39f, 0.43f); break; case NetworkStreamDisconnectReason.ConnectionClose: notificationText = $"{username} was disconnected by server!"; color = new Color(1f, 0.39f, 0.43f); break; case NetworkStreamDisconnectReason.InvalidRpc: notificationText = $"{username} had invalid RPC!"; color = new Color(0.39f, 0.01f, 0.63f); break; case NetworkStreamDisconnectReason.BadProtocolVersion: notificationText = $"{username} had invalid protocol version!"; color = new Color(0.39f, 0.01f, 0.63f); break; case NetworkStreamDisconnectReason.AuthenticationFailure: notificationText = $"{username} could not be authenticated!"; color = new Color(0.39f, 0.01f, 0.63f); break; case NetworkStreamDisconnectReason.ProtocolError: notificationText = $"{username} had a low-level transport error!"; color = new Color(0.39f, 0.01f, 0.63f); break; default: notificationText = $"{username} disconnected with error {(int) entry.Event.Reason}!"; color = new Color(0.39f, 0.01f, 0.63f); break; } } var targetAlpha = notificationOpacityCurve.Evaluate(entry.DurationLeft); GUI.color = Color.LerpUnclamped(Color.clear, color, targetAlpha); GUILayout.Box(notificationText, m_ScoreboardWidth); } } }