You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

182 lines
7.3 KiB
C#

1 week ago
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Assertions;
namespace Unity.BossRoom.Utils
{
/// This utility help showing Network statistics at runtime.
///
/// This component attaches to any networked object.
/// It'll spawn all the needed text and canvas.
///
/// NOTE: This class will be removed once Unity provides support for this.
[RequireComponent(typeof(NetworkObject))]
public class NetworkStats : NetworkBehaviour
{
// For a value like RTT an exponential moving average is a better indication of the current rtt and fluctuates less.
struct ExponentialMovingAverageCalculator
{
readonly float m_Alpha;
float m_Average;
public float Average => m_Average;
public ExponentialMovingAverageCalculator(float average)
{
m_Alpha = 2f / (k_MaxWindowSize + 1);
m_Average = average;
}
public float NextValue(float value) => m_Average = (value - m_Average) * m_Alpha + m_Average;
}
// RTT
// Client sends a ping RPC to the server and starts it's timer.
// The server receives the ping and sends a pong response to the client.
// The client receives that pong response and stops its time.
// The RPC value is using a moving average, so we don't have a value that moves too much, but is still reactive to RTT changes.
const int k_MaxWindowSizeSeconds = 3; // it should take x seconds for the value to react to change
const float k_PingIntervalSeconds = 0.1f;
const float k_MaxWindowSize = k_MaxWindowSizeSeconds / k_PingIntervalSeconds;
// Some games are less sensitive to latency than others. For fast-paced games, latency above 100ms becomes a challenge for players while for others 500ms is fine. It's up to you to establish those thresholds.
const float k_StrugglingNetworkConditionsRTTThreshold = 130;
const float k_BadNetworkConditionsRTTThreshold = 200;
ExponentialMovingAverageCalculator m_BossRoomRTT = new ExponentialMovingAverageCalculator(0);
ExponentialMovingAverageCalculator m_UtpRTT = new ExponentialMovingAverageCalculator(0);
float m_LastPingTime;
TextMeshProUGUI m_TextStat;
TextMeshProUGUI m_TextHostType;
TextMeshProUGUI m_TextBadNetworkConditions;
// When receiving pong client RPCs, we need to know when the initiating ping sent it so we can calculate its individual RTT
int m_CurrentRTTPingId;
Dictionary<int, float> m_PingHistoryStartTimes = new Dictionary<int, float>();
RpcParams m_PongClientParams;
string m_TextToDisplay;
public override void OnNetworkSpawn()
{
bool isClientOnly = IsClient && !IsServer;
if (!IsOwner && isClientOnly) // we don't want to track player ghost stats, only our own
{
enabled = false;
return;
}
if (IsOwner)
{
CreateNetworkStatsText();
}
m_PongClientParams = RpcTarget.Group(new[] { OwnerClientId }, RpcTargetUse.Persistent);
}
// Creating a UI text object and add it to NetworkOverlay canvas
void CreateNetworkStatsText()
{
Assert.IsNotNull(Editor.NetworkOverlay.Instance,
"No NetworkOverlay object part of scene. Add NetworkOverlay prefab to bootstrap scene!");
string hostType = IsHost ? "Host" : IsClient ? "Client" : "Unknown";
Editor.NetworkOverlay.Instance.AddTextToUI("UI Host Type Text", $"Type: {hostType}", out m_TextHostType);
Editor.NetworkOverlay.Instance.AddTextToUI("UI Stat Text", "No Stat", out m_TextStat);
Editor.NetworkOverlay.Instance.AddTextToUI("UI Bad Conditions Text", "", out m_TextBadNetworkConditions);
}
void FixedUpdate()
{
if (!IsServer)
{
if (Time.realtimeSinceStartup - m_LastPingTime > k_PingIntervalSeconds)
{
// We could have had a ping/pong where the ping sends the pong and the pong sends the ping. Issue with this
// is the higher the latency, the lower the sampling would be. We need pings to be sent at a regular interval
ServerPingRpc(m_CurrentRTTPingId);
m_PingHistoryStartTimes[m_CurrentRTTPingId] = Time.realtimeSinceStartup;
m_CurrentRTTPingId++;
m_LastPingTime = Time.realtimeSinceStartup;
m_UtpRTT.NextValue(NetworkManager.NetworkConfig.NetworkTransport.GetCurrentRtt(NetworkManager.ServerClientId));
}
if (m_TextStat != null)
{
m_TextToDisplay = $"RTT: {(m_BossRoomRTT.Average * 1000).ToString("0")} ms;\nUTP RTT {m_UtpRTT.Average.ToString("0")} ms";
if (m_UtpRTT.Average > k_BadNetworkConditionsRTTThreshold)
{
m_TextStat.color = Color.red;
}
else if (m_UtpRTT.Average > k_StrugglingNetworkConditionsRTTThreshold)
{
m_TextStat.color = Color.yellow;
}
else
{
m_TextStat.color = Color.white;
}
}
if (m_TextBadNetworkConditions != null)
{
// Right now, we only base this warning on UTP's RTT metric, but in the future we could watch for packet loss as well, or other metrics.
// This could be a simple icon instead of doing heavy string manipulations.
m_TextBadNetworkConditions.text = m_UtpRTT.Average > k_BadNetworkConditionsRTTThreshold ? "Bad Network Conditions Detected!" : "";
var color = Color.red;
color.a = Mathf.PingPong(Time.time, 1f);
m_TextBadNetworkConditions.color = color;
}
}
else
{
m_TextToDisplay = $"Connected players: {NetworkManager.Singleton.ConnectedClients.Count.ToString()}";
}
if (m_TextStat)
{
m_TextStat.text = m_TextToDisplay;
}
}
[Rpc(SendTo.Server)]
void ServerPingRpc(int pingId, RpcParams serverParams = default)
{
ClientPongRpc(pingId, m_PongClientParams);
}
[Rpc(SendTo.SpecifiedInParams)]
void ClientPongRpc(int pingId, RpcParams clientParams = default)
{
var startTime = m_PingHistoryStartTimes[pingId];
m_PingHistoryStartTimes.Remove(pingId);
m_BossRoomRTT.NextValue(Time.realtimeSinceStartup - startTime);
}
public override void OnNetworkDespawn()
{
if (m_TextStat != null)
{
Destroy(m_TextStat.gameObject);
}
if (m_TextHostType != null)
{
Destroy(m_TextHostType.gameObject);
}
if (m_TextBadNetworkConditions != null)
{
Destroy(m_TextBadNetworkConditions.gameObject);
}
}
}
}