-
Notifications
You must be signed in to change notification settings - Fork 0
/
ServerWaveSpawner.cs
316 lines (269 loc) · 10.9 KB
/
ServerWaveSpawner.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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;
}
}
}