using System.Collections.Generic; using UnityEngine; // ============================================================================ // EnemySpawner // ---------------------------------------------------------------------------- // 단순한 적 스폰 컨트롤러 (웨이브 없이 자동 스폰/리스폰). // WaveManager가 시간 제한 스폰이라면, 이건 무한/제한 스폰의 일반 패턴. // // 사용 예시: // - 테스트 씬: Max Alive 1 + Respawn 켜기 → 한 마리씩 무한 등장 // - 아레나: Max Alive 5 + Total Limit 20 + Respawn 켜기 → 총 20마리, 동시 5 // - 일회성 매복: Spawn On Start 끄고 외부에서 SpawnNow() 호출 // ============================================================================ public class EnemySpawner : MonoBehaviour { // ─── 스폰 설정 ─────────────────────────────────────────────────────── [Header("Spawn Configuration")] [SerializeField] private Enemy _enemyPrefab; // 스폰할 적 프리팹 [SerializeField] private int _maxAliveCount = 3; // 동시에 살아있을 수 있는 최대 수 [SerializeField] private float _spawnInterval = 2f; // 스폰 간격 (초) [SerializeField] private float _initialDelay = 0f; // 첫 스폰 전 대기 시간 // ─── 위치 설정 ─────────────────────────────────────────────────────── [Header("Spawn Position")] [SerializeField] private Transform[] _spawnPoints; // 비워두면 자기 위치, 여러 개면 랜덤 선택 [SerializeField] private float _spawnRadius = 0f; // 0보다 크면 spawn point 주변 무작위 분산 [SerializeField] private Transform _enemyParent; // 스폰된 적을 묶을 부모 (Hierarchy 정리용) // ─── 동작 옵션 ─────────────────────────────────────────────────────── [Header("Behavior")] [SerializeField] private bool _spawnOnStart = true; // Start에서 자동 스폰 시작 [SerializeField] private bool _respawnOnDeath = true; // 죽으면 자동 리스폰 (false면 일회성) [SerializeField] private int _totalSpawnLimit = 0; // 누적 스폰 한도 (0이면 무제한) private readonly List _aliveEnemies = new(); // 현재 살아있는 적들 (자동 정리됨) private float _nextSpawnTime; // 다음 스폰 가능 시각 private int _totalSpawned; // 지금까지 누적 스폰 수 private bool _active; // 스폰 활성 상태 private void Start() { if (_spawnOnStart) BeginSpawning(); } // 외부 트리거에서 스폰 시작 (예: 플레이어가 영역 진입). public void BeginSpawning() { _active = true; _nextSpawnTime = Time.time + _initialDelay; } // 스폰 일시 중지. 살아있는 적은 그대로 유지. public void StopSpawning() => _active = false; // 스폰 + 살아있는 적 모두 제거 + 카운터 초기화. 재시작용. public void ResetSpawner() { for (int i = _aliveEnemies.Count - 1; i >= 0; i--) { if (_aliveEnemies[i] != null) Destroy(_aliveEnemies[i].gameObject); } _aliveEnemies.Clear(); _totalSpawned = 0; _nextSpawnTime = Time.time + _initialDelay; } private void Update() { if (!_active) return; // Destroy된 적 참조는 null이 됨. 자동 정리. _aliveEnemies.RemoveAll(e => e == null); // 조기 종료 조건들: if (!_respawnOnDeath && _aliveEnemies.Count == 0 && _totalSpawned > 0) return; // 일회성이고 다 죽었으면 끝 if (_totalSpawnLimit > 0 && _totalSpawned >= _totalSpawnLimit) return; // 누적 한도 도달 if (_aliveEnemies.Count >= _maxAliveCount) return; // 동시 최대치 도달 if (Time.time < _nextSpawnTime) return; // 다음 스폰 시간 안 됨 if (_enemyPrefab == null) return; SpawnOne(); _nextSpawnTime = Time.time + _spawnInterval; } // 1마리 즉시 스폰. 외부에서 직접 호출 가능 (예: 이벤트 트리거). public Enemy SpawnOne() { if (_enemyPrefab == null) return null; Vector3 spawnPos = GetSpawnPosition(); Enemy enemy = Instantiate(_enemyPrefab, spawnPos, Quaternion.identity, _enemyParent); _aliveEnemies.Add(enemy); _totalSpawned++; return enemy; } // 스폰 위치 결정: // 1) Spawn Points 있으면 그중 랜덤 선택 // 2) 없으면 자기 위치 // 3) Spawn Radius > 0이면 위 위치에서 ±radius 무작위 오프셋 추가 private Vector3 GetSpawnPosition() { Vector3 basePos; if (_spawnPoints != null && _spawnPoints.Length > 0) { Transform pick = _spawnPoints[Random.Range(0, _spawnPoints.Length)]; basePos = pick != null ? pick.position : transform.position; } else { basePos = transform.position; } if (_spawnRadius > 0f) { Vector2 offset = Random.insideUnitCircle * _spawnRadius; basePos.x += offset.x; basePos.y += offset.y; } return basePos; } // Scene 뷰에서 스폰 위치/반경 시각화. // 각 spawn point마다 시안색 원(위치)과 반경 원(분산) 표시. 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); } } } }