using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using Unity.Netcode;
using System;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace XRMultiplayer
{
///
/// Represents a networked object dispenser that can spawn and despawn interactable objects.
///
public class NetworkObjectDispenser : NetworkBehaviour
{
const int k_NonToggleablePanelId = -1;
public Action OnProxiesUpdated;
///
/// The button used to clear the current interactables.
///
[SerializeField] UnityEngine.UI.Button m_ClearButton;
// [SerializeField] bool m_UseCapacity = false;
///
/// The maximum capacity of the dispenser.
///
[SerializeField] int m_Capacity;
[SerializeField] float m_DistanceCheckTimeInterval = .5f;
///
/// The text component displaying the current capacity.
///
[SerializeField] TMP_Text m_CountText;
///
/// The network variable representing the current capacity.
///
NetworkVariable m_CurrentCapacityNetworked = new NetworkVariable(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
///
/// The network variable representing the current panel ID.
///
NetworkVariable m_CurrentPanelIdNetworked = new NetworkVariable(-1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
///
/// The list of currently active interactables.
///
List m_ActiveInteractables = new List();
// ///
// /// The panels containing the dispenser slots.
// ///
[SerializeField] DispenserPanel[] m_Panels;
// ///
// /// This panel is persistent and does not switch on or off.
// /// ///
[SerializeField] DispenserPanel m_PersistentPanel;
[SerializeField] Transform m_DefaultSpawnTransform;
///
private void Start()
{
CheckForProxies();
m_CurrentCapacityNetworked.OnValueChanged += UpdateCapacity;
}
///
public override void OnDestroy()
{
base.OnDestroy();
m_CurrentCapacityNetworked.OnValueChanged -= UpdateCapacity;
}
#if UNITY_EDITOR
void OnValidate()
{
CheckForProxies(false);
PrefabStage.prefabStageOpened -= (PrefabStage stage) => CheckForProxies();
PrefabStage.prefabStageOpened += (PrefabStage stage) => CheckForProxies();
for(int i = 0; i < m_Panels.Length; i++)
{
m_Panels[i].panelId = i;
for(int j = 0; j < m_Panels[i].dispenserSlots.Length; j++)
{
if(m_Panels[i].dispenserSlots[j] == null) continue;
m_Panels[i].dispenserSlots[j].panelId = i;
m_Panels[i].dispenserSlots[j].slotId = j;
}
}
m_PersistentPanel.panelId = k_NonToggleablePanelId;
for(int j = 0; j < m_PersistentPanel.dispenserSlots.Length; j++)
{
if(m_PersistentPanel.dispenserSlots[j] == null) continue;
m_PersistentPanel.dispenserSlots[j].panelId = k_NonToggleablePanelId;
m_PersistentPanel.dispenserSlots[j].slotId = j;
}
}
#endif
IEnumerator ServerSpawnCooldownRoutine()
{
float deltaTime;
while (IsServer)
{
deltaTime = Time.deltaTime;
foreach (DispenserPanel panel in m_Panels)
{
foreach (DispenserSlot slot in panel.dispenserSlots)
{
if (!slot.dispenserSlotTransform.gameObject.activeInHierarchy) continue;
if (slot.CanSpawn(deltaTime))
{
AddInteractableToDispenser(panel, slot.slotId);
}
}
}
foreach (DispenserSlot slot in m_PersistentPanel.dispenserSlots)
{
if (!slot.dispenserSlotTransform.gameObject.activeInHierarchy) continue;
if (slot.CanSpawn(deltaTime))
{
AddInteractableToDispenser(m_PersistentPanel, slot.slotId);
}
}
yield return new WaitForEndOfFrame();
}
}
IEnumerator ServerDistanceCheckRoutine()
{
while (IsServer)
{
foreach (DispenserPanel panel in m_Panels)
{
foreach (DispenserSlot slot in panel.dispenserSlots)
{
if (slot.CheckInteractablePosition())
{
m_ActiveInteractables.Add(slot.currentInteractable);
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
slot.currentInteractable = null;
}
}
}
foreach (DispenserSlot slot in m_PersistentPanel.dispenserSlots)
{
if (slot.CheckInteractablePosition())
{
m_ActiveInteractables.Add(slot.currentInteractable);
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
slot.currentInteractable = null;
}
}
yield return new WaitForSeconds(m_DistanceCheckTimeInterval);
}
}
///
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
m_ActiveInteractables.Clear();
if (IsServer)
{
EnablePanel(0);
StartCoroutine(ServerDistanceCheckRoutine());
StartCoroutine(ServerSpawnCooldownRoutine());
m_CurrentCapacityNetworked.Value = 0;
}
else if (m_CurrentPanelIdNetworked.Value != -1)
{
EnablePanel(m_CurrentPanelIdNetworked.Value);
if (m_CountText != null)
m_CountText.text = $"Current Capacity: {m_CurrentCapacityNetworked.Value} / {m_Capacity}";
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
StopAllCoroutines();
}
///
/// Updates the capacity UI based on the current capacity value.
///
/// The old capacity value.
/// The current capacity value.
void UpdateCapacity(int old, int current)
{
if (m_CountText != null)
{
m_CountText.text = $"Current Capacity: {m_CurrentCapacityNetworked.Value} / {m_Capacity}";
m_CountText.color = m_CurrentCapacityNetworked.Value > m_Capacity ? Color.red : Color.white;
}
if (m_ClearButton != null)
m_ClearButton.interactable = m_CurrentCapacityNetworked.Value > 0;
}
///
/// Clears the current interactables on the server.
///
public void ClearCurrentInteractables()
{
if (IsServer)
{
for (int i = m_ActiveInteractables.Count - 1; i >= 0; i--)
{
if (m_ActiveInteractables[i] != null & !m_ActiveInteractables[i].isInteracting)
{
m_ActiveInteractables[i].NetworkObject.Despawn();
m_ActiveInteractables.Remove(m_ActiveInteractables[i]);
}
}
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
}
}
///
/// Picks up an object from a dispenser panel slot.
///
/// The dispenser panel.
/// The slot ID.
void PickupObject(UnityAction bindingAction, int panelId, int slotId)
{
var panel = GetPanelById(panelId);
if (!IsServer) { Utils.Log("Trying to Spawn Object from Non-Server Client"); return; }
NetworkBaseInteractable netInteractable = panel.dispenserSlots[slotId].currentInteractable;
if (netInteractable == null) return;
netInteractable.OnInteractingChanged.RemoveListener(bindingAction);
if (netInteractable.TryGetComponent(out Rigidbody rb))
{
rb.constraints = RigidbodyConstraints.None;
}
}
///
/// Adds an interactable to a dispenser panel slot on the server.
///
/// The dispenser panel.
/// The slot ID.
/// The prefab ID, if default (-1) it will spawn based on the index of the prefab list.
void AddInteractableToDispenser(DispenserPanel panel, int slotId)
{
if (panel.dispenserSlots[slotId].currentInteractable != null)
{
Utils.Log($"Cannot Spawn. Interactable already exists in Panel {panel.panelId}, slot {panel.dispenserSlots[slotId].slotId}");
return;
}
Transform spawnerTransform = panel.dispenserSlots[slotId].dispenserSlotTransform;
panel.dispenserSlots[slotId].m_SpawnCooldownTimer = panel.dispenserSlots[slotId].spawnCooldown;
NetworkBaseInteractable spawnedInteractable = panel.dispenserSlots[slotId].SpawnInteractablePrefab(spawnerTransform);
panel.dispenserSlots[slotId].currentInteractable = spawnedInteractable;
panel.dispenserSlots[slotId].currentInteractable.NetworkObject.Spawn();
// Creates a UnityAction that calls PickupObject with the correct parameters
void PickupBinding(bool arg0)
{
PickupObject(PickupBinding, panel.panelId, slotId);
}
spawnedInteractable.OnInteractingChanged.AddListener(PickupBinding);
if (panel.dispenserSlots[slotId].freezeOnSpawn && spawnedInteractable.TryGetComponent(out Rigidbody rb))
{
rb.constraints = RigidbodyConstraints.FreezeAll;
}
}
DispenserPanel GetPanelById(int Id)
{
if (Id == k_NonToggleablePanelId) return m_PersistentPanel;
return m_Panels[Id];
}
///
/// Clears the current panel on the server.
///
void ClearPanel()
{
StopAllCoroutines();
foreach (var slot in m_Panels[m_CurrentPanelIdNetworked.Value].dispenserSlots)
{
if (slot.currentInteractable != null)
{
slot.currentInteractable.NetworkObject.Despawn();
slot.currentInteractable = null;
}
}
StartCoroutine(ServerDistanceCheckRoutine());
StartCoroutine(ServerSpawnCooldownRoutine());
}
///
/// Enables a specific panel and adds interactables to its slots on the server.
///
/// The panel ID.
public void EnablePanel(int panelId)
{
for (int i = 0; i < m_Panels.Length; i++)
{
m_Panels[i].panel.SetActive(i == panelId);
}
if (IsServer)
{
if (m_CurrentPanelIdNetworked.Value != -1)
{
ClearPanel();
}
m_CurrentPanelIdNetworked.Value = panelId;
}
}
[ContextMenu("Spawn Random Object")]
void SpawnRandomObject()
{
SpawnRandomRpc(m_DefaultSpawnTransform.position, m_DefaultSpawnTransform.rotation);
}
[Rpc(SendTo.Server)]
public void SpawnRandomRpc(Vector3 spawnPosition, Quaternion spawnRotation)
{
var spawnObject = m_PersistentPanel.dispenserSlots[UnityEngine.Random.Range(0, m_PersistentPanel.dispenserSlots.Length)].spawnableInteractablePrefab;
if (UnityEngine.Random.value < .85f)
{
int randomPanel = UnityEngine.Random.Range(0, m_Panels.Length);
int randomSlot = UnityEngine.Random.Range(0, m_Panels[randomPanel].dispenserSlots.Length);
spawnObject = m_Panels[randomPanel].dispenserSlots[randomSlot].spawnableInteractablePrefab;
}
NetworkPhysicsInteractable spawnedObject = Instantiate(spawnObject.gameObject, spawnPosition, spawnRotation).GetComponent();
spawnedObject.spawnLocked = false;
spawnedObject.NetworkObject.Spawn();
m_ActiveInteractables.Add(spawnedObject.GetComponent());
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
}
[ContextMenu("Clear Proxies")]
public void ClearProxies()
{
foreach (var panel in m_Panels)
{
foreach (var slot in panel.dispenserSlots)
{
slot.ClearProxy();
}
}
foreach (var slot in m_PersistentPanel.dispenserSlots)
{
slot.ClearProxy();
}
}
public void ClearProxyPanel(int panelId)
{
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).ClearProxyPanel();
OnProxiesUpdated?.Invoke();
}
public void ClearProxy(int panelId, int slotId)
{
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].ClearProxy();
OnProxiesUpdated?.Invoke();
}
public void SpawnProxyPanel(int panelId)
{
if (panelId != k_NonToggleablePanelId)
{
for (int i = 0; i < m_Panels.Length; i++)
{
if (panelId != i)
{
m_Panels[i].ClearProxyPanel();
}
}
}
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).SpawnProxyPanel();
OnProxiesUpdated?.Invoke();
}
public void SpawnProxy(int panelId, int slotId)
{
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].SpawnProxy();
NetworkBaseInteractable interactable = (panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].currentInteractable;
OnProxiesUpdated?.Invoke();
}
public bool IsProxyPanelShowing(int panelId)
{
if (m_Panels.Length == 0) return false;
DispenserPanel panel = panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId];
foreach (var slot in panel.dispenserSlots)
{
if (slot.hasSpawnedProxy) return true;
}
return false;
}
public bool IsProxyPanelFull(int panelId)
{
if (m_Panels.Length == 0) return false;
DispenserPanel panel = panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId];
foreach (var slot in panel.dispenserSlots)
{
if (!slot.hasSpawnedProxy) return false;
}
return true;
}
public bool IsProxySlotShowing(int panelId, int slotId)
{
if (m_Panels.Length == 0) return false;
return (panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].hasSpawnedProxy;
}
public void CheckForProxies(bool clearAllProxies = true)
{
foreach (var panel in m_Panels)
{
foreach (var slot in panel.dispenserSlots)
{
if (slot.currentInteractable == null && slot.dispenserSlotTransform != null)
{
if (slot.dispenserSlotTransform.parent.GetComponentInChildren() != null)
{
slot.currentInteractable = slot.dispenserSlotTransform.parent.GetComponentInChildren();
}
}
}
}
foreach (var slot in m_PersistentPanel.dispenserSlots)
{
if (slot.currentInteractable == null && slot.dispenserSlotTransform != null)
{
if (slot.dispenserSlotTransform.parent.GetComponentInChildren() != null)
{
slot.currentInteractable = slot.dispenserSlotTransform.parent.GetComponentInChildren();
}
}
}
if (clearAllProxies)
ClearProxies();
OnProxiesUpdated?.Invoke();
}
}
[Serializable]
///
/// Represents a dispenser panel.
///
public class DispenserPanel
{
///
/// The type of physics used by this panel.
///
public string panelName;
///
/// The panel game object associated with the object dispenser.
///
public GameObject panel;
///
/// The array of dispenser slots used by the object dispenser.
///
[SerializeField] public DispenserSlot[] dispenserSlots;
public int panelId;
public void SpawnProxyPanel()
{
for (int i = 0; i < dispenserSlots.Length; i++)
{
dispenserSlots[i].SpawnProxy();
}
}
public void ClearProxyPanel()
{
for (int i = 0; i < dispenserSlots.Length; i++)
{
dispenserSlots[i].ClearProxy();
}
}
}
[Serializable]
///
/// Represents a dispenser slot.
///
public class DispenserSlot
{
///
/// The transform of the dispenser slot.
///
public Transform dispenserSlotTransform;
public NetworkBaseInteractable spawnableInteractablePrefab;
public NetworkObjectSpawner objectSpawner;
public int panelId;
public int slotId;
public bool freezeOnSpawn = true;
public float distanceToSpawnNew = .5f;
public float spawnCooldown = .5f;
internal float m_SpawnCooldownTimer = 0f;
[SerializeField] public bool hasSpawnedProxy = false;
///
/// The current network interactable object in the dispenser slot.
///
[SerializeField] public NetworkBaseInteractable currentInteractable;
public void ClearProxy()
{
if (currentInteractable == null) return;
if (Application.isPlaying)
{
UnityEngine.Object.Destroy(currentInteractable.gameObject);
}
else
{
UnityEngine.Object.DestroyImmediate(currentInteractable.gameObject);
}
hasSpawnedProxy = false;
}
public void SpawnProxy()
{
if (currentInteractable != null) ClearProxy();
NetworkBaseInteractable spawnedInteractable = UnityEngine.Object.Instantiate(spawnableInteractablePrefab, dispenserSlotTransform.position, dispenserSlotTransform.rotation);
spawnedInteractable.transform.localScale = dispenserSlotTransform.localScale;
spawnedInteractable.transform.parent = dispenserSlotTransform.transform;
currentInteractable = spawnedInteractable;
hasSpawnedProxy = true;
}
public bool CheckInteractablePosition()
{
if (currentInteractable == null)
return false;
float currentDistance = Vector3.Distance(currentInteractable.transform.position, dispenserSlotTransform.position);
if (objectSpawner != null && currentDistance > 0.001f)
objectSpawner.OnSpawnDistanceUpdated.Invoke(Mathf.Clamp01(currentDistance / distanceToSpawnNew));
return currentDistance > distanceToSpawnNew;
}
public bool CanSpawn(float deltaTime)
{
if (currentInteractable != null) return false;
if (m_SpawnCooldownTimer > 0)
{
UpdateCooldown(m_SpawnCooldownTimer - deltaTime);
return false;
}
UpdateCooldown(spawnCooldown);
return true;
}
void UpdateCooldown(float newTime)
{
m_SpawnCooldownTimer = newTime;
if (objectSpawner != null)
objectSpawner.OnSpawnCooldownUpdated.Invoke(Mathf.Clamp01(1 - (m_SpawnCooldownTimer / spawnCooldown)));
}
public NetworkBaseInteractable SpawnInteractablePrefab(Transform spawnerTransform)
{
UpdateCooldown(spawnCooldown);
NetworkBaseInteractable spawnedInteractable = UnityEngine.Object.Instantiate
(
spawnableInteractablePrefab,
spawnerTransform.position,
spawnerTransform.rotation
);
spawnedInteractable.transform.localScale = spawnerTransform.localScale;
if (objectSpawner != null)
objectSpawner.OnObjectSpawned.Invoke();
return spawnedInteractable;
}
}
}