using System; using System.Collections; using System.Collections.Generic; using Unity.BossRoom.Gameplay.GameplayObjects.Character; using Unity.Netcode; using UnityEngine; namespace Unity.BossRoom.Gameplay.GameplayObjects { /// /// Component responsible for spawning prefab clones in waves on the server. /// calls our SetSpawnerEnabled() to turn on/off spawning. /// public class ServerWaveSpawner : NetworkBehaviour { // networked object that will be spawned in waves [SerializeField] NetworkObject m_NetworkedPrefab; [SerializeField] [Tooltip("Each spawned enemy appears at one of the points in this list")] List m_SpawnPositions; [Tooltip("Select which layers will block visibility.")] [SerializeField] LayerMask m_BlockingMask; [Tooltip("Time between player distance & visibility scans, in seconds.")] [SerializeField] float m_PlayerProximityValidationTimestep = 2; [SerializeField] [Tooltip("The detection range of spawned entities. Only meaningful for NPCs (not breakables). -1 = \"use default for this NPC\"")] float m_SpawnedEntityDetectDistance = -1; [Header("Wave parameters")] [Tooltip("Total number of waves.")] [SerializeField] int m_NumberOfWaves = 2; [Tooltip("Number of spawns per wave.")] [SerializeField] int m_SpawnsPerWave = 2; [Tooltip("Time between individual spawns, in seconds.")] [SerializeField] float m_TimeBetweenSpawns = 0.5f; [Tooltip("Time between waves, in seconds.")] [SerializeField] float m_TimeBetweenWaves = 5; [Tooltip("Once last wave is spawned, the spawner waits this long to restart wave spawns, in seconds.")] [SerializeField] float m_RestartDelay = 10; [Tooltip("A player must be within this distance to commence first wave spawn.")] [SerializeField] float m_ProximityDistance = 30; [SerializeField] [Tooltip("When looking for players within proximity distance, should we count players in stealth mode?")] bool m_DetectStealthyPlayers = true; [Header("Spawn Cap (i.e. number of simultaneously spawned entities)")] [SerializeField] [Tooltip("The minimum number of entities this spawner will try to maintain (regardless of player count)")] int m_MinSpawnCap = 2; [SerializeField] [Tooltip("The maximum number of entities this spawner will try to maintain (regardless of player count)")] int m_MaxSpawnCap = 10; [SerializeField] [Tooltip("For each player in the game, the Spawn Cap is raised above the minimum by this amount. (Rounds up to nearest whole number.)")] float m_SpawnCapIncreasePerPlayer = 1; // cache reference to our own transform Transform m_Transform; // track wave index and reset once all waves are complete int m_WaveIndex; // keep reference to our current watch-for-players coroutine Coroutine m_WatchForPlayers; // keep reference to our wave spawning coroutine Coroutine m_WaveSpawning; // cache array of RaycastHit as it will be reused for player visibility RaycastHit[] m_Hit; // indicates whether OnNetworkSpawn() has been called on us yet bool m_IsStarted; // are we currently spawning stuff? bool m_IsSpawnerEnabled; // a running tally of spawned entities, used in determining which spawn-point to use next int m_SpawnedCount; // the currently-spawned entities. We only bother to track these if m_MaxActiveSpawns is non-zero List m_ActiveSpawns = new List(); void Awake() { m_Transform = transform; } public override void OnNetworkSpawn() { if (!IsServer) { enabled = false; return; } m_Hit = new RaycastHit[1]; m_IsStarted = true; if (m_IsSpawnerEnabled) { StartWaveSpawning(); } } public void SetSpawnerEnabled(bool isEnabledNow) { if (m_IsStarted && m_IsSpawnerEnabled != isEnabledNow) { if (!isEnabledNow) { StopWaveSpawning(); } else { StartWaveSpawning(); } } m_IsSpawnerEnabled = isEnabledNow; } void StartWaveSpawning() { StopWaveSpawning(); m_WatchForPlayers = StartCoroutine(TriggerSpawnWhenPlayersNear()); } void StopWaveSpawning() { if (m_WaveSpawning != null) { StopCoroutine(m_WaveSpawning); } m_WaveSpawning = null; if (m_WatchForPlayers != null) { StopCoroutine(m_WatchForPlayers); } m_WatchForPlayers = null; } public override void OnNetworkDespawn() { StopWaveSpawning(); } /// /// Coroutine for continually validating proximity to players and starting a wave of enemies in response. /// IEnumerator TriggerSpawnWhenPlayersNear() { while (true) { if (m_WaveSpawning == null && IsAnyPlayerNearbyAndVisible()) { m_WaveSpawning = StartCoroutine(SpawnWaves()); } yield return new WaitForSeconds(m_PlayerProximityValidationTimestep); } } /// /// Coroutine for spawning prefabs clones in waves, waiting a duration before spawning a new wave. /// Once all waves are completed, it waits a restart time before termination. /// /// IEnumerator SpawnWaves() { m_WaveIndex = 0; while (m_WaveIndex < m_NumberOfWaves) { yield return SpawnWave(); yield return new WaitForSeconds(m_TimeBetweenWaves); } yield return new WaitForSeconds(m_RestartDelay); m_WaveSpawning = null; } /// /// Coroutine that spawns a wave of prefab clones, with some time between spawns. /// /// IEnumerator SpawnWave() { for (int i = 0; i < m_SpawnsPerWave; i++) { if (IsRoomAvailableForAnotherSpawn()) { var newSpawn = SpawnPrefab(); m_ActiveSpawns.Add(newSpawn); } yield return new WaitForSeconds(m_TimeBetweenSpawns); } m_WaveIndex++; } /// /// Spawn a NetworkObject prefab clone. /// NetworkObject SpawnPrefab() { if (m_NetworkedPrefab == null) { throw new System.ArgumentNullException("m_NetworkedPrefab"); } int posIdx = m_SpawnedCount++ % m_SpawnPositions.Count; var clone = Instantiate(m_NetworkedPrefab, m_SpawnPositions[posIdx].position, m_SpawnPositions[posIdx].rotation); if (!clone.IsSpawned) { clone.Spawn(true); } if (m_SpawnedEntityDetectDistance > -1) { // need to override the spawned creature's detection range (if they even have a detection range!) var serverChar = clone.GetComponent(); if (serverChar && serverChar.AIBrain != null) { serverChar.AIBrain.DetectRange = m_SpawnedEntityDetectDistance; } } return clone; } bool IsRoomAvailableForAnotherSpawn() { // references to spawned components that no longer exist will become null, // so clear those out. Then we know how many we have left m_ActiveSpawns.RemoveAll(spawnedNetworkObject => { return spawnedNetworkObject == null; }); return m_ActiveSpawns.Count < GetCurrentSpawnCap(); } /// /// Returns the current max number of entities we should try to maintain. /// This can change based on the current number of living players; if the cap goes below /// our current number of active spawns, we don't spawn anything new until we're below the cap. /// int GetCurrentSpawnCap() { int numPlayers = 0; foreach (var serverCharacter in PlayerServerCharacter.GetPlayerServerCharacters()) { if (serverCharacter.NetLifeState.LifeState.Value == LifeState.Alive) { ++numPlayers; } } return Mathf.CeilToInt(Mathf.Min(m_MinSpawnCap + (numPlayers * m_SpawnCapIncreasePerPlayer), m_MaxSpawnCap)); } /// /// Determines whether any player is within range & visible through RaycastNonAlloc check. /// /// True if visible and within range, else false. bool IsAnyPlayerNearbyAndVisible() { var spawnerPosition = m_Transform.position; var ray = new Ray(); // note: this is not cached to allow runtime modifications to m_ProximityDistance var squaredProximityDistance = m_ProximityDistance * m_ProximityDistance; // iterate through clients and only return true if a player is in range // and is not occluded by a blocking collider. foreach (var serverCharacter in PlayerServerCharacter.GetPlayerServerCharacters()) { if (!m_DetectStealthyPlayers && serverCharacter.IsStealthy.Value) { // we don't detect stealthy players continue; } var playerPosition = serverCharacter.physicsWrapper.Transform.position; var direction = playerPosition - spawnerPosition; if (direction.sqrMagnitude > squaredProximityDistance) { continue; } ray.origin = spawnerPosition; ray.direction = direction; var hit = Physics.RaycastNonAlloc(ray, m_Hit, Mathf.Min(direction.magnitude, m_ProximityDistance), m_BlockingMask); if (hit == 0) { return true; } } return false; } } }