Skip to content

Commit 4bbe421

Browse files
committed
Adding ConnectionManager skeleton
1 parent 770a693 commit 4bbe421

File tree

4 files changed

+404
-0
lines changed

4 files changed

+404
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using Unity.Multiplayer.Samples.BossRoom.Shared;
5+
using Unity.Multiplayer.Samples.Utilities;
6+
using Unity.Netcode;
7+
using Unity.Services.Authentication;
8+
using Unity.Services.Core;
9+
using UnityEngine;
10+
using UnityEngine.SceneManagement;
11+
12+
namespace Unity.Multiplayer.Samples.BossRoom.Client
13+
{
14+
enum ConnectionState
15+
{
16+
Offline,
17+
Connecting,
18+
Connected,
19+
Reconnecting
20+
}
21+
22+
public class ClientConnectionManager : MonoBehaviour
23+
{
24+
NetworkManager m_NetworkManager;
25+
ProfileManager m_ProfileManager;
26+
ConnectionState m_ConnectionState;
27+
ConnectStatus m_ConnectStatus;
28+
29+
/// <summary>
30+
/// the name of the player chosen at game start
31+
/// </summary>
32+
public string PlayerName;
33+
34+
void Awake()
35+
{
36+
m_NetworkManager.OnClientConnectedCallback += OnClientConnected;
37+
m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnect;
38+
}
39+
40+
void Destroy()
41+
{
42+
m_NetworkManager.OnClientConnectedCallback -= OnClientConnected;
43+
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnect;
44+
}
45+
46+
void OnClientDisconnect(ulong clientId)
47+
{
48+
throw new NotImplementedException();
49+
}
50+
51+
void OnClientConnected(ulong clientId)
52+
{
53+
throw new NotImplementedException();
54+
}
55+
56+
public void OnUserDisconnectRequest()
57+
{
58+
59+
}
60+
61+
public void StartClientLobby()
62+
{
63+
64+
}
65+
66+
public void StartClientIp()
67+
{
68+
69+
}
70+
71+
void ConnectClient()
72+
{
73+
var payload = JsonUtility.ToJson(new ConnectionPayload()
74+
{
75+
playerId = GetPlayerId(),
76+
clientScene = SceneManager.GetActiveScene().buildIndex,
77+
playerName = PlayerName,
78+
isDebug = Debug.isDebugBuild
79+
});
80+
81+
var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
82+
83+
m_NetworkManager.NetworkConfig.ConnectionData = payloadBytes;
84+
85+
//and...we're off! Netcode will establish a socket connection to the host.
86+
// If the socket connection fails, we'll hear back by getting an ReceiveServerToClientSetDisconnectReason_CustomMessage callback for ourselves and get a message telling us the reason
87+
// If the socket connection succeeds, we'll get our ReceiveServerToClientConnectResult_CustomMessage invoked. This is where game-layer failures will be reported.
88+
m_NetworkManager.StartClient();
89+
SceneLoaderWrapper.Instance.AddOnSceneEventCallback();
90+
91+
// should only do this once StartClient has been called (start client will initialize CustomMessagingManager
92+
NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(nameof(ReceiveServerToClientSetDisconnectReason_CustomMessage), ReceiveServerToClientSetDisconnectReason_CustomMessage);
93+
}
94+
95+
public void ReceiveServerToClientSetDisconnectReason_CustomMessage(ulong clientID, FastBufferReader reader)
96+
{
97+
reader.ReadValueSafe(out ConnectStatus status);
98+
m_ConnectStatus = status;
99+
}
100+
101+
public string GetPlayerId()
102+
{
103+
if (UnityServices.State != ServicesInitializationState.Initialized)
104+
{
105+
return ClientPrefs.GetGuid() + m_ProfileManager.Profile;
106+
}
107+
108+
return AuthenticationService.Instance.IsSignedIn ? AuthenticationService.Instance.PlayerId : ClientPrefs.GetGuid() + m_ProfileManager.Profile;
109+
}
110+
}
111+
}

Assets/BossRoom/Scripts/Gameplay/ConnectionManagement/ClientConnectionManager.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using Unity.Collections;
4+
using Unity.Multiplayer.Samples.BossRoom.Client;
5+
using Unity.Multiplayer.Samples.BossRoom.Shared.Infrastructure;
6+
using Unity.Multiplayer.Samples.BossRoom.Shared.Net.UnityServices.Lobbies;
7+
using Unity.Multiplayer.Samples.Utilities;
8+
using Unity.Netcode;
9+
using UnityEngine;
10+
using UnityEngine.SceneManagement;
11+
12+
namespace Unity.Multiplayer.Samples.BossRoom.Server
13+
{
14+
public class ServerConnectionManager : MonoBehaviour
15+
{
16+
[SerializeField]
17+
NetworkObject m_GameState;
18+
19+
// used in ApprovalCheck. This is intended as a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage.
20+
const int k_MaxConnectPayload = 1024;
21+
22+
/// <summary>
23+
/// Keeps a list of what clients are in what scenes.
24+
/// </summary>
25+
Dictionary<ulong, int> m_ClientSceneMap = new Dictionary<ulong, int>();
26+
27+
/// <summary>
28+
/// The active server scene index.
29+
/// </summary>
30+
static int ServerScene => SceneManager.GetActiveScene().buildIndex;
31+
32+
NetworkManager m_NetworkManager;
33+
LobbyServiceFacade m_LobbyServiceFacade;
34+
35+
IPublisher<ConnectionEventMessage> m_ConnectionEventPublisher;
36+
37+
[Inject]
38+
void InjectDependencies(LobbyServiceFacade lobbyServiceFacade, IPublisher<ConnectionEventMessage> connectionEventPublisher)
39+
{
40+
m_LobbyServiceFacade = lobbyServiceFacade;
41+
m_ConnectionEventPublisher = connectionEventPublisher;
42+
}
43+
44+
void Start()
45+
{
46+
m_NetworkManager.ConnectionApprovalCallback += ApprovalCheck;
47+
m_NetworkManager.OnServerStarted += ServerStartedHandler;
48+
m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnect;
49+
}
50+
51+
void OnDestroy()
52+
{
53+
if (m_NetworkManager != null)
54+
{
55+
m_NetworkManager.ConnectionApprovalCallback -= ApprovalCheck;
56+
m_NetworkManager.OnServerStarted -= ServerStartedHandler;
57+
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnect;
58+
}
59+
}
60+
61+
/// <summary>
62+
/// Handles the case where NetworkManager has told us a client has disconnected. This includes ourselves, if we're the host,
63+
/// and the server is stopped."
64+
/// </summary>
65+
void OnClientDisconnect(ulong clientId)
66+
{
67+
m_ClientSceneMap.Remove(clientId);
68+
69+
if (clientId == m_NetworkManager.LocalClientId)
70+
{
71+
//the ServerGameNetPortal may be initialized again, which will cause its OnNetworkSpawn to be called again.
72+
//Consequently we need to unregister anything we registered, when the NetworkManager is shutting down.
73+
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnect;
74+
if (m_LobbyServiceFacade.CurrentUnityLobby != null)
75+
{
76+
m_LobbyServiceFacade.DeleteLobbyAsync(m_LobbyServiceFacade.CurrentUnityLobby.Id);
77+
}
78+
SessionManager<SessionPlayerData>.Instance.OnServerEnded();
79+
}
80+
else
81+
{
82+
var playerId = SessionManager<SessionPlayerData>.Instance.GetPlayerId(clientId);
83+
if (playerId != null)
84+
{
85+
if (m_LobbyServiceFacade.CurrentUnityLobby != null)
86+
{
87+
m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(playerId, m_LobbyServiceFacade.CurrentUnityLobby.Id);
88+
}
89+
90+
var sessionData = SessionManager<SessionPlayerData>.Instance.GetPlayerData(playerId);
91+
if (sessionData.HasValue)
92+
{
93+
m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.GenericDisconnect, PlayerName = sessionData.Value.PlayerName });
94+
}
95+
SessionManager<SessionPlayerData>.Instance.DisconnectClient(clientId);
96+
}
97+
}
98+
}
99+
100+
public void OnClientSceneChanged(ulong clientId, int sceneIndex)
101+
{
102+
m_ClientSceneMap[clientId] = sceneIndex;
103+
}
104+
105+
/// <summary>
106+
/// Handles the flow when a user has requested a disconnect via UI (which can be invoked on the Host, and thus must be
107+
/// handled in server code).
108+
/// </summary>
109+
public void OnUserDisconnectRequest()
110+
{
111+
if (m_NetworkManager.IsHost)
112+
{
113+
SendServerToAllClientsSetDisconnectReason(ConnectStatus.HostEndedSession);
114+
// Wait before shutting down to make sure clients receive that message before they are disconnected
115+
StartCoroutine(WaitToShutdown());
116+
}
117+
Clear();
118+
}
119+
120+
void Clear()
121+
{
122+
//resets all our runtime state.
123+
m_ClientSceneMap.Clear();
124+
}
125+
126+
public bool AreAllClientsInServerScene()
127+
{
128+
foreach (var kvp in m_ClientSceneMap)
129+
{
130+
if (kvp.Value != ServerScene) { return false; }
131+
}
132+
133+
return true;
134+
}
135+
136+
/// <summary>
137+
/// This logic plugs into the "ConnectionApprovalCallback" exposed by Netcode.NetworkManager, and is run every time a client connects to us.
138+
/// See ClientGameNetPortal.StartClient for the complementary logic that runs when the client starts its connection.
139+
/// </summary>
140+
/// <remarks>
141+
/// Since our game doesn't have to interact with some third party authentication service to validate the identity of the new connection, our ApprovalCheck
142+
/// method is simple, and runs synchronously, invoking "callback" to signal approval at the end of the method. Netcode currently doesn't support the ability
143+
/// to send back more than a "true/false", which means we have to work a little harder to provide a useful error return to the client. To do that, we invoke a
144+
/// custom message in the same channel that Netcode uses for its connection callback. Since the delivery is NetworkDelivery.ReliableSequenced, we can be
145+
/// confident that our login result message will execute before any disconnect message.
146+
/// </remarks>
147+
/// <param name="connectionData">binary data passed into StartClient. In our case this is the client's GUID, which is a unique identifier for their install of the game that persists across app restarts. </param>
148+
/// <param name="clientId">This is the clientId that Netcode assigned us on login. It does not persist across multiple logins from the same client. </param>
149+
/// <param name="connectionApprovedCallback">The delegate we must invoke to signal that the connection was approved or not. </param>
150+
void ApprovalCheck(byte[] connectionData, ulong clientId, NetworkManager.ConnectionApprovedDelegate connectionApprovedCallback)
151+
{
152+
if (connectionData.Length > k_MaxConnectPayload)
153+
{
154+
// If connectionData too high, deny immediately to avoid wasting time on the server. This is intended as
155+
// a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage.
156+
connectionApprovedCallback(false, 0, false, null, null);
157+
return;
158+
}
159+
160+
// Approval check happens for Host too, but obviously we want it to be approved
161+
if (clientId == NetworkManager.Singleton.LocalClientId)
162+
{
163+
SessionManager<SessionPlayerData>.Instance.SetupConnectingPlayerSessionData(clientId, m_Portal.GetPlayerId(),
164+
new SessionPlayerData(clientId, m_Portal.PlayerName, m_Portal.AvatarRegistry.GetRandomAvatar().Guid.ToNetworkGuid(), 0, true));
165+
166+
connectionApprovedCallback(true, null, true, null, null);
167+
return;
168+
}
169+
170+
var payload = System.Text.Encoding.UTF8.GetString(connectionData);
171+
var connectionPayload = JsonUtility.FromJson<ConnectionPayload>(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html
172+
var gameReturnStatus = GetConnectStatus(connectionPayload);
173+
174+
if (gameReturnStatus == ConnectStatus.Success)
175+
{
176+
SessionManager<SessionPlayerData>.Instance.SetupConnectingPlayerSessionData(clientId, connectionPayload.playerId,
177+
new SessionPlayerData(clientId, connectionPayload.playerName, m_Portal.AvatarRegistry.GetRandomAvatar().Guid.ToNetworkGuid(), 0, true));
178+
179+
//Populate our client scene map
180+
m_ClientSceneMap[clientId] = connectionPayload.clientScene;
181+
182+
// connection approval will create a player object for you
183+
connectionApprovedCallback(true, null, true, Vector3.zero, Quaternion.identity);
184+
185+
m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.Success, PlayerName = SessionManager<SessionPlayerData>.Instance.GetPlayerData(clientId)?.PlayerName });
186+
}
187+
else
188+
{
189+
//TODO-FIXME:Netcode Issue #796. We should be able to send a reason and disconnect without a coroutine delay.
190+
//TODO:Netcode: In the future we expect Netcode to allow us to return more information as part of the
191+
//approval callback, so that we can provide more context on a reject. In the meantime we must provide
192+
//the extra information ourselves, and then wait a short time before manually close down the connection.
193+
SendServerToClientSetDisconnectReason(clientId, gameReturnStatus);
194+
StartCoroutine(WaitToDenyApproval(connectionApprovedCallback));
195+
if (m_LobbyServiceFacade.CurrentUnityLobby != null)
196+
{
197+
m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(connectionPayload.playerId, m_LobbyServiceFacade.CurrentUnityLobby.Id);
198+
}
199+
}
200+
}
201+
202+
ConnectStatus GetConnectStatus(ConnectionPayload connectionPayload)
203+
{
204+
if (m_NetworkManager.ConnectedClientsIds.Count >= CharSelectData.k_MaxLobbyPlayers)
205+
{
206+
return ConnectStatus.ServerFull;
207+
}
208+
209+
if (connectionPayload.isDebug != Debug.isDebugBuild)
210+
{
211+
return ConnectStatus.IncompatibleBuildType;
212+
}
213+
214+
return SessionManager<SessionPlayerData>.Instance.IsDuplicateConnection(connectionPayload.playerId) ?
215+
ConnectStatus.LoggedInAgain : ConnectStatus.Success;
216+
}
217+
218+
static IEnumerator WaitToDenyApproval(NetworkManager.ConnectionApprovedDelegate connectionApprovedCallback)
219+
{
220+
yield return new WaitForSeconds(0.5f);
221+
connectionApprovedCallback(false, 0, false, null, null);
222+
}
223+
224+
IEnumerator WaitToShutdown()
225+
{
226+
yield return null;
227+
m_NetworkManager.Shutdown();
228+
SessionManager<SessionPlayerData>.Instance.OnServerEnded();
229+
}
230+
231+
/// <summary>
232+
/// Sends a DisconnectReason to all connected clients. This should only be done on the server, prior to disconnecting the client.
233+
/// </summary>
234+
/// <param name="status"> The reason for the upcoming disconnect.</param>
235+
static void SendServerToAllClientsSetDisconnectReason(ConnectStatus status)
236+
{
237+
var writer = new FastBufferWriter(sizeof(ConnectStatus), Allocator.Temp);
238+
writer.WriteValueSafe(status);
239+
NetworkManager.Singleton.CustomMessagingManager.SendNamedMessageToAll(nameof(ClientGameNetPortal.ReceiveServerToClientSetDisconnectReason_CustomMessage), writer);
240+
}
241+
242+
/// <summary>
243+
/// Sends a DisconnectReason to the indicated client. This should only be done on the server, prior to disconnecting the client.
244+
/// </summary>
245+
/// <param name="clientID"> id of the client to send to </param>
246+
/// <param name="status"> The reason for the upcoming disconnect.</param>
247+
static void SendServerToClientSetDisconnectReason(ulong clientID, ConnectStatus status)
248+
{
249+
var writer = new FastBufferWriter(sizeof(ConnectStatus), Allocator.Temp);
250+
writer.WriteValueSafe(status);
251+
NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(nameof(ClientGameNetPortal.ReceiveServerToClientSetDisconnectReason_CustomMessage), clientID, writer);
252+
}
253+
254+
/// <summary>
255+
/// Called after the server is created- This is primarily meant for the host server to clean up or handle/set state as its starting up
256+
/// </summary>
257+
void ServerStartedHandler()
258+
{
259+
// server spawns game state
260+
var gameState = Instantiate(m_GameState);
261+
262+
gameState.Spawn();
263+
264+
SceneLoaderWrapper.Instance.AddOnSceneEventCallback();
265+
266+
//The "BossRoom" server always advances to CharSelect immediately on start. Different games
267+
//may do this differently.
268+
SceneLoaderWrapper.Instance.LoadScene("CharSelect", useNetworkSceneManager: true);
269+
}
270+
}
271+
}

0 commit comments

Comments
 (0)