-
Notifications
You must be signed in to change notification settings - Fork 1
/
NetworkStats.cs
185 lines (155 loc) · 7.39 KB
/
NetworkStats.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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>();
ClientRpcParams m_PongClientParams;
bool m_IsServer;
string m_TextToDisplay;
public override void OnNetworkSpawn()
{
m_IsServer = IsServer;
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 = new ClientRpcParams() { Send = new ClientRpcSendParams() { TargetClientIds = new[] { OwnerClientId } } };
}
// 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 (!m_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
PingServerRPC(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;
}
}
[ServerRpc]
void PingServerRPC(int pingId, ServerRpcParams serverParams = default)
{
PongClientRPC(pingId, m_PongClientParams);
}
[ClientRpc]
void PongClientRPC(int pingId, ClientRpcParams 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);
}
}
}
}