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#
182 lines
7.3 KiB
C#
3 weeks 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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|