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#
317 lines
11 KiB
C#
3 weeks ago
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|