using System; using System.Collections; using System.Collections.Generic; using Unity.Netcode; using UnityEngine; [RequireComponent(typeof(NetworkObject))] public sealed class ItemPipe : NetworkBehaviour { [Header("Pipe")] [SerializeField] private Transform spawnPoint; [Header("Configuration")] [SerializeField] private ItemPipeConfig config; private readonly NetworkVariable spawnSequenceActive = new( false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private readonly NetworkVariable cooldownEndsAt = new( 0d, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); private Coroutine spawnRoutine; public float CooldownDuration => config != null ? config.SpawnCooldown : 0f; public float CooldownRemaining { get { double now = NetworkManager != null ? NetworkManager.ServerTime.Time : Time.timeAsDouble; return Mathf.Max(0f, (float)(cooldownEndsAt.Value - now)); } } public int CooldownSecondsRemaining => Mathf.CeilToInt(CooldownRemaining); public bool IsSpawning => spawnSequenceActive.Value; public bool CanSpawnItems => IsSpawned && config != null && !spawnSequenceActive.Value && CooldownRemaining <= 0f; public event Action Changed; private void Reset() => spawnPoint = transform; public override void OnNetworkSpawn() { spawnSequenceActive.OnValueChanged += HandleSpawnStateChanged; cooldownEndsAt.OnValueChanged += HandleCooldownChanged; if (IsServer) { spawnSequenceActive.Value = false; cooldownEndsAt.Value = 0d; } Changed?.Invoke(); } public override void OnNetworkDespawn() { spawnSequenceActive.OnValueChanged -= HandleSpawnStateChanged; cooldownEndsAt.OnValueChanged -= HandleCooldownChanged; if (spawnRoutine != null) { StopCoroutine(spawnRoutine); spawnRoutine = null; } } /// /// Starts one configured batch. Safe to connect directly to a Button or UnityEvent. /// Calls made on a client are forwarded to the server. /// public void SpawnItems() { if (!IsSpawned) return; if (IsServer) TryStartSpawnSequenceOnServer(); else RequestSpawnItemsRpc(); } /// /// UnityEvent<bool> adapter for a lever. Only the true edge starts a batch; /// returning a momentary lever to false does not cancel the current batch. /// public void SpawnItems(bool activated) { if (activated) SpawnItems(); } /// /// Starts while true and cancels the current batch while false. Use this when /// the lever position must control whether the pipe is allowed to keep running. /// public void SetSpawning(bool shouldSpawn) { if (shouldSpawn) SpawnItems(); else StopSpawning(); } public void StopSpawning() { if (!IsSpawned) return; if (IsServer) StopSpawnSequenceOnServer(applyCooldown: true); else RequestStopSpawningRpc(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestSpawnItemsRpc() => TryStartSpawnSequenceOnServer(); [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestStopSpawningRpc() => StopSpawnSequenceOnServer(applyCooldown: true); private bool TryStartSpawnSequenceOnServer() { if (!IsServer || !CanSpawnItems) return false; spawnSequenceActive.Value = true; spawnRoutine = StartCoroutine(SpawnSequenceOnServer(config)); return true; } private IEnumerator SpawnSequenceOnServer(ItemPipeConfig activeConfig) { List spawnQueue = BuildSpawnQueue(activeConfig); for (int index = 0; index < spawnQueue.Count; index++) { SpawnNetworkItemOnServer(spawnQueue[index], activeConfig); if (index < spawnQueue.Count - 1) yield return new WaitForSeconds(activeConfig.SpawnInterval); } spawnRoutine = null; cooldownEndsAt.Value = NetworkManager.ServerTime.Time + activeConfig.SpawnCooldown; spawnSequenceActive.Value = false; } private void StopSpawnSequenceOnServer(bool applyCooldown) { if (!IsServer || !spawnSequenceActive.Value) return; if (spawnRoutine != null) { StopCoroutine(spawnRoutine); spawnRoutine = null; } cooldownEndsAt.Value = applyCooldown && config != null ? NetworkManager.ServerTime.Time + config.SpawnCooldown : 0d; spawnSequenceActive.Value = false; } private static List BuildSpawnQueue(ItemPipeConfig activeConfig) { List queue = new(); foreach (ItemPipeConfig.GuaranteedItem item in activeConfig.GuaranteedItems) { if (item.prefab == null) continue; for (int index = 0; index < item.amount; index++) queue.Add(item.prefab); } for (int index = 0; index < activeConfig.RandomItemCount; index++) { GameObject selectedPrefab = ChooseWeightedRandom(activeConfig); if (selectedPrefab != null) queue.Add(selectedPrefab); } Shuffle(queue); return queue; } private static GameObject ChooseWeightedRandom(ItemPipeConfig activeConfig) { float totalWeight = 0f; foreach (ItemPipeConfig.RandomItem item in activeConfig.RandomItems) { if (item.prefab != null && item.weight > 0f) totalWeight += item.weight; } if (totalWeight <= 0f) return null; float roll = UnityEngine.Random.value * totalWeight; float accumulatedWeight = 0f; foreach (ItemPipeConfig.RandomItem item in activeConfig.RandomItems) { if (item.prefab == null || item.weight <= 0f) continue; accumulatedWeight += item.weight; if (roll < accumulatedWeight) return item.prefab; } return null; } private static void Shuffle(List queue) { for (int index = queue.Count - 1; index > 0; index--) { int randomIndex = UnityEngine.Random.Range(0, index + 1); (queue[index], queue[randomIndex]) = (queue[randomIndex], queue[index]); } } private void SpawnNetworkItemOnServer( GameObject prefab, ItemPipeConfig activeConfig) { Transform activeSpawnPoint = spawnPoint != null ? spawnPoint : transform; if (prefab == null) return; float offsetX = UnityEngine.Random.Range( -activeConfig.RandomSpawnX, activeConfig.RandomSpawnX); float offsetZ = UnityEngine.Random.Range( -activeConfig.RandomSpawnZ, activeConfig.RandomSpawnZ); Vector3 spawnPosition = activeSpawnPoint.position + activeSpawnPoint.right * offsetX + activeSpawnPoint.forward * offsetZ; GameObject item = Instantiate( prefab, spawnPosition, UnityEngine.Random.rotationUniform); if (!item.TryGetComponent(out NetworkObject networkObject)) { Debug.LogError( $"{name} cannot spawn '{prefab.name}': the prefab has no NetworkObject.", this); Destroy(item); return; } networkObject.Spawn(); if (item.TryGetComponent(out Rigidbody body)) { Vector3 worldVelocityChange = activeSpawnPoint.TransformDirection(activeConfig.LocalVelocityChange); body.AddForce(worldVelocityChange, ForceMode.VelocityChange); } } private void HandleSpawnStateChanged(bool _, bool __) => Changed?.Invoke(); private void HandleCooldownChanged(double _, double __) => Changed?.Invoke(); [ContextMenu("Debug/Spawn Batch (Network Play Mode)")] private void DebugSpawnBatch() => SpawnItems(); [ContextMenu("Debug/Stop Batch (Network Play Mode)")] private void DebugStopBatch() => StopSpawning(); }