Files
WhiteMan_Unity2D/Assets/02_Scripts/Enemy/WaveManager.cs

277 lines
8.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
public class WaveManager : MonoBehaviour
{
[Header("Waves")]
[SerializeField] private List<WaveData> _waves = new();
[SerializeField] private float _intermissionDuration = 2f;
[SerializeField] private bool _startOnAwake = true;
[Header("Spawn Position")]
[SerializeField] private Transform[] _spawnPoints;
[SerializeField] private float _spawnRadius = 0f;
[SerializeField] private Transform _enemyParent;
[Header("On Defeat")]
[SerializeField] private bool _destroyAliveEnemiesOnDefeat = true;
public event Action<int, WaveData> OnWaveStart;
public event Action<int> OnWaveCleared;
public event Action OnAllWavesCleared;
public event Action<int> OnDefeat;
public int CurrentWaveIndex { get; private set; }
public int TotalWaveCount => _waves != null ? _waves.Count : 0;
public WaveData CurrentWave =>
_waves != null && CurrentWaveIndex >= 0 && CurrentWaveIndex < _waves.Count
? _waves[CurrentWaveIndex]
: null;
public float TimeRemaining { get; private set; }
public int AliveCount => _aliveEnemies.Count;
public int RemainingToSpawn { get; private set; }
public bool IsRunning { get; private set; }
public bool IsDefeated { get; private set; }
public bool IsVictory { get; private set; }
private readonly List<Enemy> _aliveEnemies = new();
private CancellationTokenSource _waveCts;
private void Start()
{
if (_startOnAwake) StartWaves();
}
private void OnDestroy()
{
_waveCts?.Cancel();
_waveCts?.Dispose();
}
public void StartWaves()
{
if (_waves == null || _waves.Count == 0) return;
RunAllWaves();
}
public void StopWaves()
{
_waveCts?.Cancel();
IsRunning = false;
}
private async void RunAllWaves()
{
_waveCts?.Cancel();
_waveCts?.Dispose();
_waveCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
CancellationToken token = _waveCts.Token;
IsDefeated = false;
IsVictory = false;
try
{
for (CurrentWaveIndex = 0; CurrentWaveIndex < _waves.Count; CurrentWaveIndex++)
{
WaveData wave = _waves[CurrentWaveIndex];
if (wave == null) continue;
if (wave.StartDelay > 0f)
await Awaitable.WaitForSecondsAsync(wave.StartDelay, token);
OnWaveStart?.Invoke(CurrentWaveIndex, wave);
bool cleared = await RunWave(wave, token);
if (!cleared)
{
IsDefeated = true;
OnDefeat?.Invoke(CurrentWaveIndex);
if (_destroyAliveEnemiesOnDefeat)
DestroyAliveEnemies();
return;
}
OnWaveCleared?.Invoke(CurrentWaveIndex);
if (CurrentWaveIndex < _waves.Count - 1 && _intermissionDuration > 0f)
await Awaitable.WaitForSecondsAsync(_intermissionDuration, token);
}
IsVictory = true;
OnAllWavesCleared?.Invoke();
}
catch (OperationCanceledException) { }
}
private async Awaitable<bool> RunWave(WaveData wave, CancellationToken token)
{
IsRunning = true;
TimeRemaining = wave.TimeLimit;
_aliveEnemies.Clear();
int totalToSpawn = 0;
foreach (var entry in wave.Spawns)
if (entry != null) totalToSpawn += Mathf.Max(entry.Count, 0);
RemainingToSpawn = totalToSpawn;
// 적 스폰은 백그라운드로 흘려보내고, 본 루프는 시간/클리어만 체크.
SpawnWaveEnemiesAsync(wave, token);
while (true)
{
token.ThrowIfCancellationRequested();
_aliveEnemies.RemoveAll(e => e == null);
if (RemainingToSpawn == 0 && _aliveEnemies.Count == 0)
{
IsRunning = false;
return true;
}
TimeRemaining -= Time.deltaTime;
if (TimeRemaining <= 0f)
{
IsRunning = false;
return false;
}
await Awaitable.NextFrameAsync(token);
}
}
private async void SpawnWaveEnemiesAsync(WaveData wave, CancellationToken token)
{
try
{
foreach (var entry in wave.Spawns)
{
if (entry == null || entry.EnemyPrefab == null)
{
RemainingToSpawn -= entry != null ? entry.Count : 0;
continue;
}
for (int i = 0; i < entry.Count; i++)
{
token.ThrowIfCancellationRequested();
SpawnEnemy(entry.EnemyPrefab);
RemainingToSpawn--;
if (wave.SpawnInterval > 0f)
await Awaitable.WaitForSecondsAsync(wave.SpawnInterval, token);
}
}
}
catch (OperationCanceledException) { }
}
private void SpawnEnemy(Enemy prefab)
{
Vector3 position = GetSpawnPosition();
Enemy enemy = Instantiate(prefab, position, Quaternion.identity, _enemyParent);
_aliveEnemies.Add(enemy);
}
private Vector3 GetSpawnPosition()
{
Vector3 basePos;
if (_spawnPoints != null && _spawnPoints.Length > 0)
{
Transform pick = _spawnPoints[UnityEngine.Random.Range(0, _spawnPoints.Length)];
basePos = pick != null ? pick.position : transform.position;
}
else
{
basePos = transform.position;
}
if (_spawnRadius > 0f)
{
Vector2 offset = UnityEngine.Random.insideUnitCircle * _spawnRadius;
basePos.x += offset.x;
basePos.y += offset.y;
}
return basePos;
}
private void DestroyAliveEnemies()
{
for (int i = _aliveEnemies.Count - 1; i >= 0; i--)
{
if (_aliveEnemies[i] != null)
Destroy(_aliveEnemies[i].gameObject);
}
_aliveEnemies.Clear();
}
private void OnGUI()
{
GUIStyle style = new GUIStyle(GUI.skin.box)
{
fontSize = 22,
alignment = TextAnchor.MiddleCenter,
richText = true,
normal = { textColor = Color.white }
};
string text;
if (IsVictory)
{
text = "<color=#88ff88><b>VICTORY</b></color>";
}
else if (IsDefeated)
{
text = $"<color=#ff8888><b>DEFEAT</b></color>\nWave {CurrentWaveIndex + 1} timed out";
}
else if (IsRunning && CurrentWave != null)
{
string name = string.IsNullOrEmpty(CurrentWave.WaveName)
? $"Wave {CurrentWaveIndex + 1}/{TotalWaveCount}"
: CurrentWave.WaveName;
text = $"<b>{name}</b>\nTime: {TimeRemaining:F1}s\nEnemies: {AliveCount} (+{RemainingToSpawn} 대기)";
}
else
{
return;
}
GUI.Box(new Rect(Screen.width / 2f - 180f, 20f, 360f, 110f), text, style);
}
private void OnDrawGizmosSelected()
{
Color pointColor = Color.cyan;
Color radiusColor = new Color(0f, 1f, 1f, 0.25f);
if (_spawnPoints != null && _spawnPoints.Length > 0)
{
foreach (var p in _spawnPoints)
{
if (p == null) continue;
Gizmos.color = pointColor;
Gizmos.DrawWireSphere(p.position, 0.3f);
if (_spawnRadius > 0f)
{
Gizmos.color = radiusColor;
Gizmos.DrawWireSphere(p.position, _spawnRadius);
}
}
}
else
{
Gizmos.color = pointColor;
Gizmos.DrawWireSphere(transform.position, 0.3f);
if (_spawnRadius > 0f)
{
Gizmos.color = radiusColor;
Gizmos.DrawWireSphere(transform.position, _spawnRadius);
}
}
}
}