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;
}
}
}
}