#define ENABLE_LOGS using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.SceneManagement; using Fusion; using Fusion.Plugin; using Fusion.Sockets; using UnityScene = UnityEngine.SceneManagement.Scene; namespace TPSBR { public struct SessionRequest { public string UserID; public GameMode GameMode; public string DisplayName; public string SessionName; public string ScenePath; public EGameplayType GameplayType; public int MaxPlayers; public int ExtraPeers; public string CustomLobby; public string IPAddress; public ushort Port; } // This Networking class is complex due to handling of reconnection of extra peers that we used for initial batch testing. // We suggest to use standard approach (inheriting from NetworkSceneManagerBase for custom loading // functionality and directly starting via NetworkRunner) for your game unless such functionality is needed. public class Networking : MonoBehaviour { // CONSTANTS public const string DISPLAY_NAME_KEY = "name"; public const string MAP_KEY = "map"; public const string TYPE_KEY = "type"; public const string MODE_KEY = "mode"; public const string STATUS_SERVER_CLOSED = "Server Closed"; // PUBLIC MEMBERS public string Status { get; private set; } public string StatusDescription { get; private set; } public string ErrorStatus { get; private set; } public bool HasSession => _pendingSession != null || _currentSession != null; public bool IsConnecting => _pendingSession != null || _currentSession.IsConnected == false; public bool IsConnected => _currentSession != null && _pendingSession == null && _currentSession.IsConnected == true; public int PeerCount => _currentSession != null ? _currentSession.GamePeers.SafeCount() : 0; // PRIVATE MEMBERS private Session _pendingSession; private Session _currentSession; private bool _stopGameOnDisconnect; private string _loadingScene; private Coroutine _coroutine; // PUBLIC METHODS public void StartGame(SessionRequest request) { var session = new Session(); if (request.ExtraPeers > 0 && NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Single) { Debug.LogError("Cannot start with multiple peers. PeerMode is set to Single."); request.ExtraPeers = 0; } SceneRef sceneRef = default; int sceneIndex = SceneUtility.GetBuildIndexByScenePath(request.ScenePath); if (sceneIndex >= 0) { sceneRef = SceneRef.FromIndex(sceneIndex); } int totalPeers = 1 + request.ExtraPeers; session.GamePeers = new GamePeer[totalPeers]; NetworkSceneInfo sceneInfo = new NetworkSceneInfo(); if (sceneRef.IsValid == true) { sceneInfo.AddSceneRef(sceneRef, LoadSceneMode.Additive, LocalPhysicsMode.None, true); } for (int i = 0; i < totalPeers; i++) { session.GamePeers[i] = new GamePeer(i) { UserID = i == 0 ? request.UserID : $"{request.UserID}.{i}", Scene = sceneInfo, GameMode = i == 0 ? request.GameMode : GameMode.Client, Request = request, }; } session.ConnectionRequested = true; _pendingSession = session; _stopGameOnDisconnect = false; ErrorStatus = null; Log($"StartGame() UserID:{request.UserID} GameMode:{request.GameMode} DisplayName:{request.DisplayName} SessionName:{request.SessionName} ScenePath:{request.ScenePath} GameplayType:{request.GameplayType} MaxPlayers:{request.MaxPlayers} ExtraPeers:{request.ExtraPeers} CustomLobby:{request.CustomLobby}"); } public void StopGame(string errorStatus = null) { Log($"StopGame()"); _pendingSession = null; _stopGameOnDisconnect = false; if (_currentSession != null) { _currentSession.ConnectionRequested = false; } ErrorStatus = errorStatus; } public void StopGameOnDisconnect() { Log($"StopGameOnDisconnect()"); _stopGameOnDisconnect = true; } public void ClearErrorStatus() { ErrorStatus = null; } // MONOBEHAVIOUR protected void Awake() { _loadingScene = Global.Settings.LoadingScene; } protected void Update() { if (_pendingSession != null) { if (_currentSession == null) { _currentSession = _pendingSession; _pendingSession = null; } else { // Request end of current session _currentSession.ConnectionRequested = false; } } UpdateCurrentSession(); // Check if current session finished if (_coroutine == null && _currentSession != null && _currentSession.IsConnected == false) { if (_pendingSession == null) { Log($"Starting LoadMenuCoroutine()"); // Current session is finished and there is no pending session, let's go to menu _coroutine = StartCoroutine(LoadMenuCoroutine()); } _currentSession = null; } } // PRIVATE MEMBERS public void UpdateCurrentSession() { if (_currentSession == null) { Status = string.Empty; StatusDescription = string.Empty; return; } if (_coroutine != null) return; var peers = _currentSession.GamePeers; if (_stopGameOnDisconnect == true) { for (int i = 0; i < peers.Length; i++) { if (_currentSession.ConnectionRequested == true && peers[i].IsConnected == false) { Log($"Stopping game after disconnect"); _stopGameOnDisconnect = false; StopGame(); return; } } } for (int i = 0; i < peers.Length; i++) { var peer = peers[i]; bool isConnected = peer.IsConnected; if (_currentSession.ConnectionRequested == true && peer.Loaded == false && isConnected == false && peer.CanConnect == true) { // First connect or reconnect after failed connect Status = peer.WasConnected == false ? "Starting" : "Reconnecting"; Log($"Starting ConnectPeerCoroutine() - {Status} - Peer {peer.ID}"); _coroutine = StartCoroutine(ConnectPeerCoroutine(peer)); return; } else if (_currentSession.ConnectionRequested == false && (isConnected == true || peer.Loaded == true)) { // Disconnect requested Status = "Quitting"; Log($"Starting DisconnectPeerCoroutine() - {Status} - Peer {peer.ID}"); _coroutine = StartCoroutine(DisconnectPeerCoroutine(peer)); return; } else if (peer.Loaded == true && isConnected == false) { // Connection lost Status = "Connection Lost"; Log($"Starting DisconnectPeerCoroutine() - {Status} - Peer {peer.ID}"); _coroutine = StartCoroutine(DisconnectPeerCoroutine(peer)); return; } } UpdatePeerSwitch(_currentSession.GamePeers); ValidateMultiPeers(_currentSession.GamePeers); } private IEnumerator ConnectPeerCoroutine(GamePeer peer, float connectionTimeout = 10f, float loadTimeout = 45f) { peer.Loaded = true; if (peer.WasConnected == true) { peer.ReconnectionTries--; } else { peer.ConnectionTries--; } StatusDescription = "Unloading current scene"; UnityScene activeScene = SceneManager.GetActiveScene(); if (IsSameScene(activeScene.path, peer.Request.ScenePath) == false && activeScene.name != _loadingScene) { Log($"Show loading scene"); yield return ShowLoadingSceneCoroutine(true); bool unloadScene = true; for (int i = 0; i < _currentSession.GamePeers.Length; ++i) { if (activeScene == _currentSession.GamePeers[i].LoadedScene) { unloadScene = false; break; } } if (unloadScene == true) { Scene currentScene = activeScene.GetComponent(); if (currentScene != null) { Log($"Deinitializing Scene"); currentScene.Deinitialize(); } Log($"Unloading scene {activeScene.name}"); yield return SceneManager.UnloadSceneAsync(activeScene); yield return null; } } float baseTime = Time.realtimeSinceStartup; float limitTime = baseTime + connectionTimeout; string peerName = $"{peer.GameMode}#{peer.ID}"; Debug.LogWarning($"Starting {peerName} ..."); StatusDescription = "Starting network connection"; yield return null; NetworkObjectPool pool = new NetworkObjectPool(); NetworkRunner runner = Instantiate(Global.Settings.RunnerPrefab); runner.name = peerName; runner.EnableVisibilityExtension(); peer.Runner = runner; peer.SceneManager = runner.GetComponent(); peer.LoadedScene = default; StartGameArgs startGameArgs = new StartGameArgs(); startGameArgs.GameMode = peer.GameMode; startGameArgs.SessionName = peer.Request.SessionName; startGameArgs.Scene = peer.Scene; startGameArgs.OnGameStarted = OnGamePeerInitialized; startGameArgs.ObjectProvider = pool; startGameArgs.CustomLobbyName = peer.Request.CustomLobby; startGameArgs.SceneManager = peer.SceneManager; startGameArgs.EnableClientSessionCreation = false; if (peer.Request.MaxPlayers > 0) { startGameArgs.PlayerCount = peer.Request.MaxPlayers; } if (peer.GameMode == GameMode.Server || peer.GameMode == GameMode.Host) { startGameArgs.SessionProperties = CreateSessionProperties(peer.Request); } if (peer.Request.IPAddress.HasValue() == true) { startGameArgs.Address = NetAddress.CreateFromIpPort(peer.Request.IPAddress, peer.Request.Port); } else if (peer.Request.Port > 0) { startGameArgs.Address = NetAddress.Any(peer.Request.Port); } Log($"NetworkRunner.StartGame()"); var startGameTask = runner.StartGame(startGameArgs); while (startGameTask.IsCompleted == false) { yield return null; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} start timeout! IsCompleted: {startGameTask.IsCompleted} IsCanceled: {startGameTask.IsCanceled} IsFaulted: {startGameTask.IsFaulted}"); break; } if (_currentSession.ConnectionRequested == false) { Log($"Stopping coroutine (requested by user)"); // Stop requested by user break; } } if (startGameTask.IsCanceled == true || startGameTask.IsFaulted == true || startGameTask.IsCompleted == false) { Debug.LogError($"{peerName} failed to start!"); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } var result = startGameTask.Result; Log($"StartGame() Result: {result.ToString()} - Peer {peer.ID}"); if (result.Ok == false) { Debug.LogError($"{peerName} failed to start! Result: {result}"); // Probably incorrect start game parameters, go back to menu immediately if (Application.isBatchMode == false) { StopGame(); } if (peer.WasConnected == true && result.ShutdownReason == ShutdownReason.GameNotFound) { ErrorStatus = STATUS_SERVER_CLOSED; } else { ErrorStatus = StringToLabel(result.ShutdownReason.ToString()); } Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } limitTime += loadTimeout; Log($"Waiting for connection - Peer {peer.ID}"); StatusDescription = "Waiting for server connection"; while (peer.IsConnected == false) { yield return null; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} start timeout! IsCloudReady: {runner.IsCloudReady} IsRunning: {runner.IsRunning}"); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } } Log($"Loading gameplay scene - Peer {peer.ID}"); StatusDescription = "Loading gameplay scene"; while (runner.SimulationUnityScene.IsValid() == false || runner.SimulationUnityScene.isLoaded == false) { Log($"Waiting for NetworkRunner.SimulationUnityScene - Peer {peer.ID}"); yield return null; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} scene load timeout!"); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } } Debug.LogWarning($"{peerName} started in {(Time.realtimeSinceStartup - baseTime):0.00}s"); peer.LoadedScene = runner.SimulationUnityScene; if (peer.ID == 0) { SceneManager.SetActiveScene(peer.LoadedScene); } StatusDescription = "Waiting for gameplay scene load"; var scene = peer.SceneManager.GameplayScene; while (scene == null) { Log($"Waiting for GameplayScene - Peer {peer.ID}"); yield return null; scene = peer.SceneManager.GameplayScene; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} GameplayScene query timeout!"); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } } Log($"Scene.PrepareContext() - Peer {peer.ID}"); scene.PrepareContext(); var sceneContext = scene.Context; sceneContext.IsVisible = peer.ID == 0; sceneContext.HasInput = peer.ID == 0; sceneContext.Runner = peer.Runner; sceneContext.PeerUserID = peer.UserID; peer.Context = sceneContext; pool.Context = sceneContext; StatusDescription = "Waiting for networked game"; var networkGame = scene.GetComponentInChildren(true); while (networkGame.Object == null) { Log($"Waiting for NetworkGame - Peer {peer.ID}"); yield return null; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} start timeout! Network game not started properly."); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } if (_currentSession.ConnectionRequested == false) { // Stop requested by user Log($"Starting DisconnectPeerCoroutine() - Connection is not requested anymore - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } } StatusDescription = "Waiting for gameplay load"; Log($"NetworkGame.Initialize() - Peer {peer.ID}"); networkGame.Initialize(peer.Request.GameplayType); while (scene.Context.GameplayMode == null) { Log($"Waiting for GameplayMode - Peer {peer.ID}"); yield return null; if (Time.realtimeSinceStartup >= limitTime) { Debug.LogError($"{peerName} start timeout! Gameplay mode not started properly."); Log($"Starting DisconnectPeerCoroutine() - Peer {peer.ID}"); yield return DisconnectPeerCoroutine(peer); _coroutine = null; yield break; } } StatusDescription = "Activating scene"; Log($"Scene.Initialize() - Peer {peer.ID}"); scene.Initialize(); Log($"Scene.Activate() - Peer {peer.ID}"); yield return scene.Activate(); StatusDescription = "Activating network game"; Log($"NetworkGame.Activate() - Peer {peer.ID}"); networkGame.Activate(); if (SceneManager.GetSceneByName(_loadingScene).IsValid() == true) { // Wait a little bit for scene activation before showing it yield return new WaitForSeconds(1f); Log($"Hide loading scene"); yield return ShowLoadingSceneCoroutine(false); } if (peer.WasConnected == true) { peer.ReconnectionTries++; } peer.WasConnected = true; _coroutine = null; Log($"ConnectPeerCoroutine() finished"); } private IEnumerator DisconnectPeerCoroutine(GamePeer peer) { StatusDescription = "Disconnecting from server"; UnityScene gameplayScene = default; try { if (peer.Runner != null) { // Possible exception when runner tries to read config gameplayScene = peer.Runner.SimulationUnityScene; // Close and hide the room if (peer.Runner.IsServer == true && peer.Runner.SessionInfo != null) { Log($"Closing the room"); peer.Runner.SessionInfo.IsOpen = false; peer.Runner.SessionInfo.IsVisible = false; } } } catch (Exception exception) { Debug.LogException(exception); } if (gameplayScene.IsValid() == false) { gameplayScene = peer.LoadedScene; } if (gameplayScene.IsValid() == true) { Scene scene = gameplayScene.GetComponent(true); if (scene != null) { try { Log($"Deinitializing Scene"); scene.Deinitialize(); } catch (Exception exception) { Debug.LogException(exception); } } } Task shutdownTask = null; if (peer.Runner != null) { Debug.LogWarning($"Shutdown {peer.Runner.name} ..."); try { shutdownTask = peer.Runner.Shutdown(true); } catch (Exception exception) { Debug.LogException(exception); } } Log($"Show loading scene"); yield return ShowLoadingSceneCoroutine(true); if (shutdownTask != null) { float operationTimeout = 10.0f; while (operationTimeout > 0.0f && shutdownTask.IsCompleted == false) { yield return null; operationTimeout -= Time.unscaledDeltaTime; } } StatusDescription = "Unloading gameplay scene"; yield return null; if (gameplayScene.IsValid() == true) { Debug.LogWarning($"Unloading scene {gameplayScene.name}"); yield return SceneManager.UnloadSceneAsync(gameplayScene); yield return null; } peer.Loaded = default; peer.Runner = default; peer.SceneManager = default; peer.LoadedScene = default; _coroutine = null; Log($"DisconnectPeerCoroutine() finished"); } private IEnumerator ShowLoadingSceneCoroutine(bool show, float additionalTime = 1f) { var loadingScene = SceneManager.GetSceneByName(_loadingScene); if (loadingScene.IsValid() == false) { yield return SceneManager.LoadSceneAsync(_loadingScene, LoadSceneMode.Additive); loadingScene = SceneManager.GetSceneByName(_loadingScene); } if (show == false && additionalTime > 0f) { // Wait additional time till fade out starts yield return new WaitForSeconds(additionalTime); } yield return null; var loadingSceneObject = loadingScene.GetComponent(); if (loadingSceneObject != null) { if (show == true) { loadingSceneObject.FadeIn(); } else { loadingSceneObject.FadeOut(); } while (loadingSceneObject.IsFading == true) yield return null; } if (show == true && additionalTime > 0f) { // Wait additional time after fade in yield return new WaitForSeconds(additionalTime); } if (show == false) { yield return SceneManager.UnloadSceneAsync(loadingScene); } } private IEnumerator LoadMenuCoroutine() { string menuSceneName = Global.Settings.MenuScene; if (SceneManager.sceneCount == 1 && SceneManager.GetSceneAt(0).name == menuSceneName) { _coroutine = null; yield break; } StatusDescription = "Unloading gameplay scenes"; yield return ShowLoadingSceneCoroutine(true); for (int i = SceneManager.sceneCount - 1; i >= 0; --i) { var scene = SceneManager.GetSceneAt(i); if (scene.name != _loadingScene) { yield return SceneManager.UnloadSceneAsync(scene); } } StatusDescription = "Loading menu scene"; yield return null; yield return SceneManager.LoadSceneAsync(menuSceneName, LoadSceneMode.Additive); yield return ShowLoadingSceneCoroutine(false); SceneManager.SetActiveScene(SceneManager.GetSceneByName(menuSceneName)); _coroutine = null; } private void OnGamePeerInitialized(NetworkRunner runner) { if (NetworkProjectConfig.Global.PeerMode != NetworkProjectConfig.PeerModes.Multiple) return; Camera camera = runner.SimulationUnityScene.FindMainCamera(); if (camera != null) { camera.gameObject.SetActive(false); } EventSystem eventSystem = runner.SimulationUnityScene.GetComponent(true); if (eventSystem != null) { eventSystem.gameObject.SetActive(false); } } private void UpdatePeerSwitch(GamePeer[] peers) { int newID = -1; bool showOthers = false; bool canSwitchPeer = Application.isEditor == true ? true : Keyboard.current.leftCtrlKey.isPressed == true && Keyboard.current.leftShiftKey.isPressed == true; if (canSwitchPeer == true) { if (Keyboard.current.numpad1Key.wasPressedThisFrame == true) { newID = 0; } else if (Keyboard.current.numpad2Key.wasPressedThisFrame == true) { newID = 1; } else if (Keyboard.current.numpad3Key.wasPressedThisFrame == true) { newID = 2; } else if (Keyboard.current.numpad4Key.wasPressedThisFrame == true) { newID = 0; showOthers = true; } else if (Keyboard.current.numpad5Key.wasPressedThisFrame == true) { newID = 1; showOthers = true; } else if (Keyboard.current.numpad6Key.wasPressedThisFrame == true) { newID = 2; showOthers = true; } } if (newID >= 0 && newID < peers.Length) { for (int i = 0; i < peers.Length; i++) { GamePeer peer = peers[i]; peer.Context.HasInput = peer.ID == newID; peer.Context.IsVisible = peer.ID == newID || showOthers == true; } } } private void ValidateMultiPeers(GamePeer[] peers) { if (peers.SafeCount() <= 0) return; int inputPeer = -1; int visibilityPeer = -1; for (int i = 0; i < peers.Length; i++) { GamePeer peer = peers[i]; if (peer.Context == null) continue; if (peer.Context.HasInput) { if (inputPeer >= 0) { Debug.Log($"Multiple peers with input is not allowed, turning off input for peer {peer.ID}"); peer.Context.HasInput = false; } else { inputPeer = peer.ID; } } if (peer.Context.IsVisible == true && visibilityPeer < 0) { visibilityPeer = peer.ID; } } if (peers[0].Context != null) { if (inputPeer < 0) { Debug.Log($"No input peer, turning on input for peer {peers[0].ID}"); peers[0].Context.HasInput = true; } if (visibilityPeer < 0) { Debug.Log($"No visible peer, turning on visibility for peer {peers[0].ID}"); peers[0].Context.IsVisible = true; } } } private Dictionary CreateSessionProperties(SessionRequest request) { var dictionary = new Dictionary(); dictionary[DISPLAY_NAME_KEY] = request.DisplayName; dictionary[MAP_KEY] = Global.Settings.Map.GetMapIndexFromScenePath(request.ScenePath); dictionary[TYPE_KEY] = (int)request.GameplayType; dictionary[MODE_KEY] = (int)request.GameMode; return dictionary; } [System.Diagnostics.Conditional("ENABLE_LOGS")] private void Log(string message) { Debug.Log($"[{Time.realtimeSinceStartup:F3}][{Time.frameCount}] Networking({GetInstanceID()}): {message}"); } private static string StringToLabel(string myString) { var label = System.Text.RegularExpressions.Regex.Replace(myString, "(?<=[A-Z])(?=[A-Z][a-z])", " "); label = System.Text.RegularExpressions.Regex.Replace(label, "(?<=[^A-Z])(?=[A-Z])", " "); return label; } private static bool IsSameScene(string assetPath, string scenePath) { return assetPath == $"Assets/{scenePath}.unity"; } // HELPERS private sealed class GamePeer { public int ID; public NetworkSceneInfo Scene; public SceneContext Context; public GameMode GameMode; public NetworkRunner Runner; public NetworkSceneManager SceneManager; public UnityScene LoadedScene; public string UserID; public SessionRequest Request; public int ConnectionTries = 3; public int ReconnectionTries = 1; public bool Loaded; public bool WasConnected; public bool CanConnect => WasConnected == true ? ReconnectionTries > 0 : ConnectionTries > 0; public bool IsConnected { get { if (Runner == null) return false; if (Request.GameMode == GameMode.Single) return true; if (Runner.IsCloudReady == false || Runner.IsRunning == false) return false; return GameMode == GameMode.Client ? Runner.IsConnectedToServer : true; } } public GamePeer(int id) { ID = id; } } private class Session { public bool ConnectionRequested; public GamePeer[] GamePeers; public bool IsConnected { get { if (GamePeers.SafeCount() == 0) return false; for (int i = 0; i < GamePeers.Length; i++) { if (GamePeers[i].IsConnected == false) return false; } return true; } } } } }