292 lines
8.5 KiB
C#
292 lines
8.5 KiB
C#
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<bool> spawnSequenceActive = new(
|
|
false,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server);
|
|
|
|
private readonly NetworkVariable<double> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts one configured batch. Safe to connect directly to a Button or UnityEvent.
|
|
/// Calls made on a client are forwarded to the server.
|
|
/// </summary>
|
|
public void SpawnItems()
|
|
{
|
|
if (!IsSpawned)
|
|
return;
|
|
|
|
if (IsServer)
|
|
TryStartSpawnSequenceOnServer();
|
|
else
|
|
RequestSpawnItemsRpc();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void SpawnItems(bool activated)
|
|
{
|
|
if (activated)
|
|
SpawnItems();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<GameObject> 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<GameObject> BuildSpawnQueue(ItemPipeConfig activeConfig)
|
|
{
|
|
List<GameObject> 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<GameObject> 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();
|
|
}
|