using System; using System.Collections; using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using UnityEngine.SceneManagement; namespace Unity.Multiplayer.Samples.Utilities { /// /// This NetworkBehavior, when added to a GameObject containing a collider (or multiple colliders) with the /// IsTrigger property On, allows the server to load or unload a scene additively according to the position of /// player-owned objects. The scene is loaded when there is at least one NetworkObject with the specified tag that /// enters its collider. It also unloads it when all such NetworkObjects leave the collider, after a specified /// delay to prevent it from repeatedly loading and unloading the same scene. /// public class ServerAdditiveSceneLoader : NetworkBehaviour { [SerializeField] float m_DelayBeforeUnload = 5.0f; [SerializeField] string m_SceneName; /// /// We assume that all NetworkObjects with this tag are player-owned /// [SerializeField] string m_PlayerTag; /// /// We keep the clientIds of every player-owned object inside the collider's volume /// List m_PlayersInTrigger; bool IsActive => IsServer && IsSpawned; enum SceneState { Loaded, Unloaded, Loading, Unloading, WaitingToUnload, } SceneState m_SceneState = SceneState.Unloaded; Coroutine m_UnloadCoroutine; public override void OnNetworkSpawn() { if (IsServer) { // Adding this to remove all pending references to a specific client when they disconnect, since objects // that are destroyed do not generate OnTriggerExit events. NetworkManager.OnClientDisconnectCallback += RemovePlayer; NetworkManager.SceneManager.OnSceneEvent += OnSceneEvent; m_PlayersInTrigger = new List(); } } public override void OnNetworkDespawn() { if (IsServer) { NetworkManager.OnClientDisconnectCallback -= RemovePlayer; NetworkManager.SceneManager.OnSceneEvent -= OnSceneEvent; } } void OnSceneEvent(SceneEvent sceneEvent) { if (sceneEvent.SceneEventType == SceneEventType.LoadEventCompleted && sceneEvent.SceneName == m_SceneName) { m_SceneState = SceneState.Loaded; } else if (sceneEvent.SceneEventType == SceneEventType.UnloadEventCompleted && sceneEvent.SceneName == m_SceneName) { m_SceneState = SceneState.Unloaded; } } void OnTriggerEnter(Collider other) { if (IsActive) // make sure that OnNetworkSpawn has been called before this { if (other.CompareTag(m_PlayerTag) && other.TryGetComponent(out NetworkObject networkObject)) { m_PlayersInTrigger.Add(networkObject.OwnerClientId); if (m_UnloadCoroutine != null) { // stopping the unloading coroutine since there is now a player-owned NetworkObject inside StopCoroutine(m_UnloadCoroutine); if (m_SceneState == SceneState.WaitingToUnload) { m_SceneState = SceneState.Loaded; } } } } } void OnTriggerExit(Collider other) { if (IsActive) // make sure that OnNetworkSpawn has been called before this { if (other.CompareTag(m_PlayerTag) && other.TryGetComponent(out NetworkObject networkObject)) { m_PlayersInTrigger.Remove(networkObject.OwnerClientId); } } } void FixedUpdate() { if (IsActive) // make sure that OnNetworkSpawn has been called before this { if (m_SceneState == SceneState.Unloaded && m_PlayersInTrigger.Count > 0) { var status = NetworkManager.SceneManager.LoadScene(m_SceneName, LoadSceneMode.Additive); // if successfully started a LoadScene event, set state to Loading if (status == SceneEventProgressStatus.Started) { m_SceneState = SceneState.Loading; } } else if (m_SceneState == SceneState.Loaded && m_PlayersInTrigger.Count == 0) { // using a coroutine here to add a delay before unloading the scene m_UnloadCoroutine = StartCoroutine(WaitToUnloadCoroutine()); m_SceneState = SceneState.WaitingToUnload; } } } void RemovePlayer(ulong clientId) { // remove all references to this clientId. There could be multiple references if a single client owns // multiple NetworkObjects with the m_PlayerTag, or if this script's GameObject has overlapping colliders while (m_PlayersInTrigger.Remove(clientId)) { } } IEnumerator WaitToUnloadCoroutine() { yield return new WaitForSeconds(m_DelayBeforeUnload); Scene scene = SceneManager.GetSceneByName(m_SceneName); if (scene.isLoaded) { var status = NetworkManager.SceneManager.UnloadScene(SceneManager.GetSceneByName(m_SceneName)); // if successfully started an UnloadScene event, set state to Unloading, if not, reset state to Loaded so a new Coroutine will start m_SceneState = status == SceneEventProgressStatus.Started ? SceneState.Unloading : SceneState.Loaded; } } } }