using System; using System.Collections; using System.Collections.Generic; using System.Linq; using DefaultNamespace; using Events; using RuntimeSet; using UnityEngine; using UnityEngine.Assertions; using Random = UnityEngine.Random; namespace Wave { public class WaveSpawner : MonoBehaviour { [SerializeField] private SpawnWaveEventSO startNewSpawnWaveEventChannel; [SerializeField] private SpawnWaveEventSO endSpawnWaveEventChannel; [SerializeField] private EnemyRuntimeSetSO enemyRuntimeSet; [SerializeField] private Transform spawnCenter; [SerializeField] private float minimumSpawnRadius; [SerializeField] [Min(0)] private float timeBetweenClearedWaves; [SerializeField] [Min(0)] private float timeBeforeFirstWave; private float _timeSinceLastSpawnWaveStarted; private Coroutine _handleSpawnWaves; private bool HasWaveBeenCleared => enemyRuntimeSet.IsEmpty; public void Begin(IEnumerable spawnWaves) { Assert.IsNull(_handleSpawnWaves); _handleSpawnWaves = StartCoroutine(CO_HandleSpawnWaves(spawnWaves)); } public void End() { StopCoroutine(_handleSpawnWaves); } private IEnumerator CO_HandleSpawnWaves(IEnumerable spawnWaves) { yield return new WaitForSeconds(timeBeforeFirstWave); foreach (var spawnWave in spawnWaves) { startNewSpawnWaveEventChannel.RaiseEvent(spawnWave); yield return SpawnWave(spawnWave); _timeSinceLastSpawnWaveStarted = 0.0f; yield return new WaitUntil(() => _timeSinceLastSpawnWaveStarted >= spawnWave.TimeToComplete || HasWaveBeenCleared); endSpawnWaveEventChannel.RaiseEvent(spawnWave); if (HasWaveBeenCleared) { yield return new WaitForSeconds(timeBetweenClearedWaves); } } } private IEnumerator SpawnWave(SpawnWaveSO spawnWave) { var enemyPacksToSpawn = spawnWave.Packs.Select(pack => pack.EnemiesToSpawn()).ToList().AsReadOnly(); for (var packN = 0; packN < enemyPacksToSpawn.Count; packN++) { var waveCompletionPercentage = (float)packN / (float)enemyPacksToSpawn.Count; var enemyPackSpawnOffset = SampleSpawnOffset(spawnWave.Distribution, spawnWave.Radius, waveCompletionPercentage); enemyPackSpawnOffset += enemyPackSpawnOffset.normalized * (minimumSpawnRadius + spawnWave.PackRadius); var enemyPackSpawnPosition = spawnCenter.position + enemyPackSpawnOffset; var enemyPackToSpawn = enemyPacksToSpawn[packN]; for (var enemyN = 0; enemyN < enemyPackToSpawn.Count; enemyN++) { var packCompletionPercentage = (float)enemyN / (float)enemyPackToSpawn.Count; var enemySpawnOffset = SampleSpawnOffset(SpawnDistribution.DiskRandom, spawnWave.PackRadius, packCompletionPercentage); var enemySpawnPosition = enemyPackSpawnPosition + enemySpawnOffset; var spawnedEnemy = Instantiate(enemyPackToSpawn[enemyN], enemySpawnPosition, Quaternion.identity); enemyRuntimeSet.Add(spawnedEnemy); if (spawnWave.TimeBetweenSpawns > 0.0f) { yield return new WaitForSeconds(spawnWave.TimeBetweenSpawns); } } if (spawnWave.TimeBetweenPacks > 0.0f) { yield return new WaitForSeconds(spawnWave.TimeBetweenPacks); } } } private static Vector3 SampleSpawnOffset(SpawnDistribution distribution, float radius, float completionPercentage) { // TODO (Michael): Both the pack and enemy position samplers need to be a bit smarter. I think in most scenarios the pack circle just needs to not overlap with other pack circles. // The enemy spawns probably need something like a poission disk sampler so that monsters dont overlap. The enemy spawn radius at the pack's position should also probably scale with the size of the pack. var angle = completionPercentage * 2 * MathF.PI; var unitCirclePosition = Random.insideUnitCircle; var swizzledUnitCirclePosition = new Vector3(unitCirclePosition.x, 0, unitCirclePosition.y); return distribution switch { SpawnDistribution.Uniform => new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * radius, SpawnDistribution.Random => swizzledUnitCirclePosition.normalized * radius, SpawnDistribution.DiskRandom => swizzledUnitCirclePosition * radius, _ => throw new ArgumentOutOfRangeException(nameof(distribution), distribution, null) }; } private void Update() { _timeSinceLastSpawnWaveStarted += Time.deltaTime; } } }