namespace Fusion { using System; using Fusion.Sockets; using System.Collections; using System.Threading.Tasks; using UnityEngine; using UnityEngine.SceneManagement; using System.Collections.Generic; using System.Linq; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; #endif /// /// A Fusion prototyping class for starting up basic networking. Add this component to your startup scene, and supply a . /// Can be set to automatically startup the network, display an in-game menu, or allow simplified start calls like . /// [DisallowMultipleComponent] [AddComponentMenu("Fusion/Fusion Bootstrap")] [ScriptHelp(BackColor = ScriptHeaderBackColor.Steel)] public class FusionBootstrap : Fusion.Behaviour { /// /// Selection for how will behave at startup. /// public enum StartModes { UserInterface, Automatic, Manual } /// /// The current stage of connection or shutdown. /// public enum Stage { Disconnected, StartingUp, UnloadOriginalScene, ConnectingServer, ConnectingClients, AllConnected, } /// /// Supply a Prefab or a scene object which has the component on it, /// as well as any runner dependent components which implement , /// such as or your own custom INetworkInput implementations. /// [InlineHelp] [WarnIf(nameof(RunnerPrefab), false, "No " + nameof(RunnerPrefab) + " supplied. Will search for a " + nameof(NetworkRunner) + " in the scene at startup.")] public NetworkRunner RunnerPrefab; /// /// Select how network startup will be triggered. Automatically, by in-game menu selection, or exclusively by script. /// [InlineHelp] [WarnIf(nameof(StartMode), (long)StartModes.Manual, "Start network by calling the methods " + nameof(StartHost) + "(), " + nameof(StartServer) + "(), " + nameof(StartClient) + "(), " + nameof(StartHostPlusClients) + "(), or " + nameof(StartServerPlusClients) + "()")] public StartModes StartMode = StartModes.UserInterface; /// /// When is set to , this option selects if the /// will be started as a dedicated server, or as a host (which is a server with a local player). /// [InlineHelp] [UnityEngine.Serialization.FormerlySerializedAs("Server")] [DrawIf(nameof(StartMode), (long)StartModes.Automatic, Hide = true)] public GameMode AutoStartAs = GameMode.Shared; /// /// will not render GUI elements while == . /// [InlineHelp] [DrawIf(nameof(StartMode), (long)StartModes.UserInterface, Hide = true)] public bool AutoHideGUI = true; /// /// The number of client instances that will be created if running in Mulit-Peer Mode. /// When using the Select start mode, this number will be the default value for the additional clients option box. /// [InlineHelp] [DrawIf(nameof(ShowAutoClients), Hide = true)] public int AutoClients = 1; /// /// How long to wait after starting a peer before starting the next one. /// [InlineHelp] public float ClientStartDelay = 0.1f; /// /// The port that server/host will use. /// [InlineHelp] public ushort ServerPort; // Any port /// /// The default room name to use when connecting to photon cloud. /// [InlineHelp] public string DefaultRoomName = string.Empty; // empty/null means Random Room Name /// /// Will automatically enable once peers have finished connecting. /// [InlineHelp] public bool AlwaysShowStats; [NonSerialized] NetworkRunner _server; /// /// The Scene that will be loaded after network shutdown completes (all peers have disconnected). /// If this field is null or invalid, will be set to the current scene when runs Awake(). /// [InlineHelp] [ScenePath] public string InitialScenePath; // TODO: this is debt static string _initialScenePath; /// /// Indicates which step of the startup process is currently in. /// [InlineHelp] [SerializeField] [ReadOnly] protected Stage _currentStage; /// /// Indicates which step of the startup process is currently in. /// public Stage CurrentStage { get => _currentStage; internal set { _currentStage = value; #if UNITY_EDITOR // Hack to force an inspector refresh when this value changes, as it affects which buttons are shown. EditorUtility.SetDirty(this); #endif } } /// /// The index number used for the last created peer. /// public int LastCreatedClientIndex { get; internal set; } /// /// The server mode that was used for initial startup. Used to inform UI which client modes should be available. /// public GameMode CurrentServerMode { get; internal set; } protected bool CanAddClients => CurrentStage == Stage.AllConnected && CurrentServerMode > 0 && CurrentServerMode != GameMode.Shared && CurrentServerMode != GameMode.Single; protected bool CanAddSharedClients => CurrentStage == Stage.AllConnected && CurrentServerMode > 0 && CurrentServerMode == GameMode.Shared; protected bool IsShutdown => CurrentStage == Stage.Disconnected; protected bool IsShutdownAndMultiPeer => CurrentStage == Stage.Disconnected && UsingMultiPeerMode; protected bool UsingMultiPeerMode => NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple; protected bool ShowAutoClients => UsingMultiPeerMode && (StartMode == StartModes.UserInterface || (StartMode == StartModes.Automatic && AutoStartAs != GameMode.Single)); #if UNITY_EDITOR protected virtual void Reset() { if (TryGetComponent(out var ndsg) == false) { ndsg = gameObject.AddComponent(); } } #endif protected virtual void Start() { if (_initialScenePath == null) { if (string.IsNullOrEmpty(InitialScenePath)) { var currentScene = SceneManager.GetActiveScene(); if (currentScene.IsValid()) { _initialScenePath = currentScene.path; } else { // Last fallback is the first entry in the build settings _initialScenePath = SceneManager.GetSceneByBuildIndex(0).path; } InitialScenePath = _initialScenePath; } else { _initialScenePath = InitialScenePath; } } var config = NetworkProjectConfig.Global; var isMultiPeer = config.PeerMode == NetworkProjectConfig.PeerModes.Multiple; var existingRunner = FindFirstObjectByType(); if (existingRunner && existingRunner != RunnerPrefab) { if (existingRunner.State != NetworkRunner.States.Shutdown) { // disable enabled = false; // destroy this and GUI (if exists), and return var gui = GetComponent(); if (gui) { Destroy(gui); } Destroy(this); return; } else { // If no RunnerPrefab is supplied, use the scene runner. if (RunnerPrefab == null) { RunnerPrefab = existingRunner; } } } switch (StartMode) { case StartModes.Manual: // skip return; case StartModes.Automatic: { if (TryGetSceneRef(out var sceneRef)) { int clientCount; if (AutoStartAs == GameMode.Single) { clientCount = 0; } else if (isMultiPeer) { clientCount = AutoClients; } else if (AutoStartAs == GameMode.Client || AutoStartAs == GameMode.Shared || AutoStartAs == GameMode.AutoHostOrClient) { clientCount = 1; } else { clientCount = 0; } StartCoroutine(StartWithClients(AutoStartAs, sceneRef, clientCount)); } break; } default: { if (TryGetComponent(out _) == false) { gameObject.AddComponent(); } break; } } } private bool TryGetSceneRef(out SceneRef sceneRef) { var activeScene = SceneManager.GetActiveScene(); if (activeScene.buildIndex < 0 || activeScene.buildIndex >= SceneManager.sceneCountInBuildSettings) { sceneRef = default; return false; } else { sceneRef = SceneRef.FromIndex(activeScene.buildIndex); return true; } } /// /// Start a single player instance. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartSinglePlayer() { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Single, sceneRef, 0)); } } /// /// Start a server instance. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartServer() { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Server, sceneRef, 0)); } } /// /// Start a host instance. This is a server instance, with a local player. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartHost() { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Host, sceneRef, 0)); } } /// /// Start a client instance. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartClient() { StartCoroutine(StartWithClients(GameMode.Client, default, 1)); } [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartSharedClient() { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Shared, sceneRef, 1)); } } [EditorButton("Start Auto Host Or Client", EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartAutoClient() { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.AutoHostOrClient, sceneRef, 1)); } } /// /// Start a Fusion server instance, and the number of client instances indicated by . /// InstanceMode must be set to Multi-Peer mode, as this requires multiple instances. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public virtual void StartServerPlusClients() { StartServerPlusClients(AutoClients); } /// /// Start a Fusion host instance, and the number of client instances indicated by . /// InstanceMode must be set to Multi-Peer mode, as this requires multiple instances. /// [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(IsShutdown), Hide = true)] public void StartHostPlusClients() { StartHostPlusClients(AutoClients); } [EditorButton(EditorButtonVisibility.PlayMode)] [DrawIf(nameof(CurrentStage), Hide = true)] public void Shutdown() { ShutdownAll(); } /// /// Start a Fusion server instance, and the indicated number of client instances. /// InstanceMode must be set to Multi-Peer mode, as this requires multiple instances. /// public virtual void StartServerPlusClients(int clientCount) { if (NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Server, sceneRef, clientCount)); } } else { Debug.LogWarning($"Unable to start multiple {nameof(NetworkRunner)}s in Unique Instance mode."); } } /// /// Start a Fusion host instance (server with local player), and the indicated number of additional client instances. /// InstanceMode must be set to Multi-Peer mode, as this requires multiple instances. /// public void StartHostPlusClients(int clientCount) { if (NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Host, sceneRef, clientCount)); } } else { Debug.LogWarning($"Unable to start multiple {nameof(NetworkRunner)}s in Unique Instance mode."); } } /// /// Start a Fusion host instance (server with local player), and the indicated number of additional client instances. /// InstanceMode must be set to Multi-Peer mode, as this requires multiple instances. /// public void StartMultipleClients(int clientCount) { if (NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Client, sceneRef, clientCount)); } } else { Debug.LogWarning($"Unable to start multiple {nameof(NetworkRunner)}s in Unique Instance mode."); } } /// /// Start as Room on the Photon cloud, and connects as one or more clients. /// /// public void StartMultipleSharedClients(int clientCount) { if (NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.Shared, sceneRef, clientCount)); } } else { Debug.LogWarning($"Unable to start multiple {nameof(NetworkRunner)}s in Unique Instance mode."); } } public void StartMultipleAutoClients(int clientCount) { if (NetworkProjectConfig.Global.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (TryGetSceneRef(out var sceneRef)) { StartCoroutine(StartWithClients(GameMode.AutoHostOrClient, sceneRef, clientCount)); } } else { Debug.LogWarning($"Unable to start multiple {nameof(NetworkRunner)}s in Unique Instance mode."); } } public void ShutdownAll() { foreach (var runner in NetworkRunner.Instances.ToList()) { if (runner != null && runner.IsRunning) { runner.Shutdown(); } } SceneManager.LoadSceneAsync(_initialScenePath); // Destroy our DontDestroyOnLoad objects to finish the reset Destroy(RunnerPrefab.gameObject); Destroy(gameObject); CurrentStage = Stage.Disconnected; CurrentServerMode = 0; } protected IEnumerator StartWithClients(GameMode serverMode, SceneRef sceneRef, int clientCount) { // Avoid double clicks or disallow multiple startup calls. if (CurrentStage != Stage.Disconnected) { yield break; } bool includesServerStart = serverMode != GameMode.Shared && serverMode != GameMode.Client && serverMode != GameMode.AutoHostOrClient; if (!includesServerStart && clientCount == 0) { Debug.LogError($"{nameof(GameMode)} is set to {serverMode}, and {nameof(clientCount)} is set to zero. Starting no network runners."); yield break; } CurrentStage = Stage.StartingUp; var currentScene = SceneManager.GetActiveScene(); // must have a runner if (!RunnerPrefab) { Debug.LogError($"{nameof(RunnerPrefab)} not set, can't perform debug start."); yield break; } // Clone the RunnerPrefab so we can safely delete the startup scene (the prefab might be part of it, rather than an asset). RunnerPrefab = Instantiate(RunnerPrefab); DontDestroyOnLoad(RunnerPrefab); RunnerPrefab.name = "Temporary Runner Prefab"; // Single-peer can't start more than one peer. Validate clientCount to make sure it complies with current PeerMode. var config = NetworkProjectConfig.Global; if (config.PeerMode != NetworkProjectConfig.PeerModes.Multiple) { int maxClientsAllowed = includesServerStart ? 0 : 1; if (clientCount > maxClientsAllowed) { Debug.LogWarning($"Instance mode must be set to {nameof(NetworkProjectConfig.PeerModes.Multiple)} to perform a debug start multiple peers. Restricting client count to {maxClientsAllowed}."); clientCount = maxClientsAllowed; } } // If NDS is starting more than 1 shared or auto client, they need to use the same Session Name, otherwise, they will end up on different Rooms // as Fusion creates a Random Session Name when no name is passed on the args if ((serverMode == GameMode.Shared || serverMode == GameMode.AutoHostOrClient || serverMode == GameMode.Server || serverMode == GameMode.Host) && clientCount > 1 && config.PeerMode == NetworkProjectConfig.PeerModes.Multiple) { if (string.IsNullOrEmpty(DefaultRoomName)) { DefaultRoomName = Guid.NewGuid().ToString(); Debug.Log($"Generated Session Name: {DefaultRoomName}"); } } if (gameObject.transform.parent) { Debug.LogWarning($"{nameof(FusionBootstrap)} can't be a child game object, un-parenting."); gameObject.transform.parent = null; } DontDestroyOnLoad(gameObject); CurrentServerMode = serverMode; // start server, just take address from it if (includesServerStart) { _server = Instantiate(RunnerPrefab); _server.name = serverMode.ToString(); var serverTask = InitializeNetworkRunner(_server, serverMode, NetAddress.Any(ServerPort), sceneRef, (runner) => { #if FUSION_DEV var name = _server.name; // closures do not capture values, need a local var to save it Debug.Log($"Server NetworkRunner '{name}' started."); #endif }); while (serverTask.IsCompleted == false) { yield return new WaitForSeconds(1f); } if (serverTask.IsFaulted) { Log.Debug($"Unable to start server: {serverTask.Exception}"); ShutdownAll(); yield break; } // this action is called after InitializeNetworkRunner for the server has completed startup yield return StartClients(clientCount, serverMode, sceneRef); } else { yield return StartClients(clientCount, serverMode, sceneRef); } // Add stats last, so any event systems that may be getting created are already in place. if (includesServerStart && AlwaysShowStats && serverMode != GameMode.Shared) { FusionStats.Create(runner: _server, screenLayout: FusionStats.DefaultLayouts.Left, objectLayout: FusionStats.DefaultLayouts.Left); } } [EditorButton("Add Additional Client", EditorButtonVisibility.PlayMode)] [DrawIf(nameof(CanAddClients), Hide = true)] public void AddClient() { if (TryGetSceneRef(out var sceneRef)) { AddClient(GameMode.Client, sceneRef); } } [EditorButton("Add Additional Shared Client", EditorButtonVisibility.PlayMode)] [DrawIf(nameof(CanAddSharedClients), Hide = true)] public void AddSharedClient() { if (TryGetSceneRef(out var sceneRef)) { AddClient(GameMode.Shared, sceneRef); } } public Task AddClient(GameMode serverMode, SceneRef sceneRef) { var client = Instantiate(RunnerPrefab); DontDestroyOnLoad(client); client.name = $"Client {(Char)(65 + LastCreatedClientIndex++)}"; // if server mode is Shared or AutoHostOrClient, then game client mode is the same as the server, otherwise it is client var mode = GameMode.Client; switch (serverMode) { case GameMode.Shared: case GameMode.AutoHostOrClient: mode = serverMode; break; } #if FUSION_DEV var clientTask = InitializeNetworkRunner(client, mode, NetAddress.Any(), sceneRef, (runner) => { var name = client.name; // closures do not capture values, need a local var to save it Debug.Log($"Client NetworkRunner '{name}' started."); }); #else var clientTask = InitializeNetworkRunner(client, mode, NetAddress.Any(), sceneRef, null); #endif // Add stats last, so that event systems that may be getting created are already in place. if (AlwaysShowStats && LastCreatedClientIndex == 0) { FusionStats.Create(runner: client, screenLayout: FusionStats.DefaultLayouts.Right, objectLayout: FusionStats.DefaultLayouts.Right); } return clientTask; } protected IEnumerator StartClients(int clientCount, GameMode serverMode, SceneRef sceneRef = default) { CurrentStage = Stage.ConnectingClients; var clientTasks = new List(); for (int i = 0; i < clientCount; ++i) { clientTasks.Add(AddClient(serverMode, sceneRef)); yield return new WaitForSeconds(ClientStartDelay); } var clientsStartTask = Task.WhenAll(clientTasks); while (clientsStartTask.IsCompleted == false) { yield return new WaitForSeconds(1f); } if (clientsStartTask.IsFaulted) { Debug.LogWarning(clientsStartTask.Exception); } CurrentStage = Stage.AllConnected; } protected virtual Task InitializeNetworkRunner(NetworkRunner runner, GameMode gameMode, NetAddress address, SceneRef scene, Action onGameStarted, INetworkRunnerUpdater updater = null) { var sceneManager = runner.GetComponent(); if (sceneManager == null) { Debug.Log($"NetworkRunner does not have any component implementing {nameof(INetworkSceneManager)} interface, adding {nameof(NetworkSceneManagerDefault)}.", runner); sceneManager = runner.gameObject.AddComponent(); } var objectProvider = runner.GetComponent(); if (objectProvider == null) { Debug.Log($"NetworkRunner does not have any component implementing {nameof(INetworkObjectProvider)} interface, adding {nameof(NetworkObjectProviderDefault)}.", runner); objectProvider = runner.gameObject.AddComponent(); } var sceneInfo = new NetworkSceneInfo(); if (scene.IsValid) { sceneInfo.AddSceneRef(scene, LoadSceneMode.Additive); } return runner.StartGame(new StartGameArgs { GameMode = gameMode, Address = address, Scene = sceneInfo, SessionName = DefaultRoomName, OnGameStarted = onGameStarted, SceneManager = sceneManager, Updater = updater, ObjectProvider = objectProvider, }); } } }