You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
11 KiB
C#

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
{
/// <summary>
/// Component responsible for spawning prefab clones in waves on the server.
/// <see cref="EnemyPortal"/> calls our SetSpawnerEnabled() to turn on/off spawning.
/// </summary>
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<Transform> 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<NetworkObject> m_ActiveSpawns = new List<NetworkObject>();
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();
}
/// <summary>
/// Coroutine for continually validating proximity to players and starting a wave of enemies in response.
/// </summary>
IEnumerator TriggerSpawnWhenPlayersNear()
{
while (true)
{
if (m_WaveSpawning == null && IsAnyPlayerNearbyAndVisible())
{
m_WaveSpawning = StartCoroutine(SpawnWaves());
}
yield return new WaitForSeconds(m_PlayerProximityValidationTimestep);
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Coroutine that spawns a wave of prefab clones, with some time between spawns.
/// </summary>
/// <returns></returns>
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++;
}
/// <summary>
/// Spawn a NetworkObject prefab clone.
/// </summary>
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<ServerCharacter>();
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();
}
/// <summary>
/// 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.
/// </summary>
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));
}
/// <summary>
/// Determines whether any player is within range & visible through RaycastNonAlloc check.
/// </summary>
/// <returns> True if visible and within range, else false. </returns>
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;
}
}
}