277 lines
8.2 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|