Projext/Assets/02_Scripts/Enemy/BossAI/BossZoneTrigger.cs

356 lines
23 KiB
C#
Raw Normal View History

using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
// ══════════════════════════════════════════════════════════════
// BossZoneTrigger — 보스방 입장 트리거
// ══════════════════════════════════════════════════════════════
// 사용법:
// 1. 빈 GameObject에 BoxCollider(isTrigger=true) + 이 스크립트 부착
// 2. Inspector에서 boss, entranceWall, playerInput 연결
// 3. 플레이어가 트리거존에 들어오면:
// 입구 벽 활성화 → 입력 잠금 → 카메라 줌인 → 보스 포효 → BGM 전환
// → 입력 해제 → 전투 시작
//
// ★ 빌드에서 보스 프리팹이 누락되는 문제 대응:
// 보스가 씬에 없으면 Resources/BossMonster/BossMonster 에서
// 프리팹을 로드하여 런타임에 직접 생성합니다.
// ══════════════════════════════════════════════════════════════
2026-02-10 15:29:22 +00:00
2026-02-13 09:11:54 +00:00
public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 입장 트리거를
2026-02-10 15:29:22 +00:00
{
// ── Inspector 연결 ──────────────────────────────────────
[Header("=== 보스 연결 ===")]
[SerializeField] private NorcielBoss boss; // 변수를 선언할거에요 -> 보스 참조를 (StartBossBattle 호출 대상)
[Header("=== 런타임 보스 스폰 (빌드 프리팹 누락 대비) ===")]
[Tooltip("보스가 씬에 없을 때 스폰할 위치\n" +
"비어있으면 이 트리거 오브젝트 위치에서 스폰")]
[SerializeField] private Transform bossSpawnPoint; // 변수를 선언할거에요 -> 보스 스폰 위치를 (없으면 트리거 위치 사용)
[Tooltip("Resources 폴더 내 보스 프리팹 경로\n" +
"기본값: BossMonster/BossMonster")]
[SerializeField] private string bossPrefabResourcePath = "BossMonster/BossMonster"; // 변수를 선언할거에요 -> Resources 내 프리팹 경로를
[Tooltip("Resources에서 스폰 시 보스 스케일\n" +
"씬에 배치했을 때의 보스 크기와 동일하게 설정해주세요\n" +
"보스 씬에서 보스 오브젝트의 Scale 값을 여기에 넣으면 돼요")]
[SerializeField] private Vector3 bossSpawnScale = Vector3.one; // 변수를 선언할거에요 -> 스폰 시 보스 스케일을 (기본 1,1,1)
[Header("=== 트리거존 연출 ===")]
[SerializeField] private GameObject entranceWall; // 변수를 선언할거에요 -> 입구 막는 벽을 (플레이어 퇴로 차단)
[SerializeField] private GameObject exitWall; // 변수를 선언할거에요 -> 출구 막는 벽을 (반대편 출구도 차단, 없으면 무시)
[SerializeField] private GameObject bossTriggerZone; // 변수를 선언할거에요 -> 트리거존 자기 자신을 (발동 후 비활성화용, 없으면 Collider만 끔)
[Header("=== 카메라 연출 (선택) ===")]
[SerializeField] private Camera bossCamera; // 변수를 선언할거에요 -> 보스 연출용 카메라를 (포효 중 보스를 비추는 카메라, 없으면 무시)
[SerializeField] private float cameraDuration = 2.5f; // 변수를 선언할거에요 -> 연출 카메라 유지 시간을 (초)
[Header("=== 플레이어 입력 잠금 ===")]
[Tooltip("플레이어의 PlayerInput 컴포넌트\n" +
"연결하면 보스 등장 연출 중 이동/공격 등 모든 입력이 잠겨요")]
[SerializeField] private PlayerInput playerInput; // 변수를 선언할거에요 -> 플레이어 입력 컴포넌트를 (연출 중 잠금용)
[Header("=== 보스전 BGM ===")]
[Tooltip("보스전 시작 시 재생할 BGM 클립\n" +
"AudioManager 싱글톤의 PlayBGM()으로 크로스페이드 전환돼요")]
[SerializeField] private AudioClip bossBGM; // 변수를 선언할거에요 -> 보스전 BGM 클립을
[Tooltip("BGM 페이드 시간 (-1이면 AudioManager 기본값 사용)")]
[SerializeField] private float bgmFadeDuration = 1.5f; // 변수를 선언할거에요 -> BGM 크로스페이드 시간을 (초)
[Tooltip("보스 사망 후 복귀할 필드 BGM (없으면 BGM 정지)")]
[SerializeField] private AudioClip fieldBGM; // 변수를 선언할거에요 -> 보스 사망 후 복귀할 필드 BGM을
// ── 내부 상태 ───────────────────────────────────────────
private bool _isTriggered = false; // 변수를 선언할거에요 -> 이미 발동했는지를 (중복 발동 방지)
// ══════════════════════════════════════════════════════════
// 초기화 — 보스 자동 탐색 + 없으면 Resources에서 스폰
// ══════════════════════════════════════════════════════════
private void Awake() // 가장 먼저 실행할거에요 -> 보스 즉시 탐색을
{
TryFindBoss(); // 실행할거에요 -> 보스 탐색 시도를
}
private void Start() // 시작 시 실행할거에요 -> 보스 재탐색 + 스폰을
{
if (boss == null) // 조건이 맞으면 실행할거에요 -> Awake에서 못 찾았으면
{
TryFindBoss(); // 실행할거에요 -> 다시 시도를
}
if (boss == null) // 조건이 맞으면 실행할거에요 -> 여전히 보스가 없으면 (빌드에서 프리팹 누락)
{
Debug.LogWarning("[BossZone] 씬에 보스 없음 → Resources에서 스폰 시도"); // 경고를 찍을거에요
SpawnBossFromResources(); // 실행할거에요 -> Resources에서 보스 프리팹 로드 + 생성을
}
if (boss == null) // 조건이 맞으면 실행할거에요 -> 스폰까지 실패했으면
{
Debug.LogWarning("[BossZone] 스폰 실패 → 딜레이 후 재탐색 시작"); // 경고를 찍을거에요
StartCoroutine(DelayedFindBoss()); // 시작할거에요 -> 딜레이 후 재탐색 코루틴을
}
}
// ══════════════════════════════════════════════════════════
// Resources에서 보스 프리팹 로드 & 스폰
// ══════════════════════════════════════════════════════════
private void SpawnBossFromResources() // 함수를 정의할거에요 -> Resources에서 보스를 생성하는
{
// ── 1. Resources 폴더에서 프리팹 로드 ──────────────────
GameObject bossPrefab = Resources.Load<GameObject>(bossPrefabResourcePath); // 로드할거에요 -> Resources/BossMonster/BossMonster 프리팹을
if (bossPrefab == null) // 조건이 맞으면 실행할거에요 -> 프리팹 로드 실패 시
{
Debug.LogError($"[BossZone] Resources.Load 실패! 경로: {bossPrefabResourcePath}"); // 에러 로그
Debug.LogError("[BossZone] Assets/08_Resources/BossMonster/ 폴더에 BossMonster.prefab이 있는지 확인하세요!"); // 추가 안내
return; // 종료할거에요 -> 더 이상 진행 불가
}
Debug.Log($"[BossZone] 보스 프리팹 로드 성공: {bossPrefab.name}"); // 성공 로그
// ── 2. 스폰 위치 결정 ──────────────────────────────────
Vector3 spawnPos; // 변수를 선언할거에요 -> 스폰 위치를
Quaternion spawnRot; // 변수를 선언할거에요 -> 스폰 회전값을
if (bossSpawnPoint != null) // 조건이 맞으면 실행할거에요 -> 스폰 포인트가 지정돼있으면
{
spawnPos = bossSpawnPoint.position; // 설정할거에요 -> 지정된 스폰 위치를
spawnRot = bossSpawnPoint.rotation; // 설정할거에요 -> 지정된 스폰 회전을
}
else // 스폰 포인트가 없으면
{
spawnPos = transform.position + transform.forward * 10f; // 설정할거에요 -> 트리거 앞쪽 10m 지점을 (보스 스폰 위치)
spawnRot = Quaternion.identity; // 설정할거에요 -> 기본 회전값을
Debug.LogWarning("[BossZone] bossSpawnPoint가 비어있어서 트리거 앞 10m에 스폰합니다."); // 경고 로그
}
// ── 3. 프리팹 인스턴스 생성 ─────────────────────────────
GameObject bossInstance = Instantiate(bossPrefab, spawnPos, spawnRot); // 생성할거에요 -> 보스 오브젝트를 (지정 위치에)
bossInstance.name = "BossMonster"; // 설정할거에요 -> 이름을 BossMonster로 (Find 등에서 검색 가능)
// ⭐ 스케일 적용 — 프리팹 기본 스케일이 씬 배치 시와 다를 수 있으므로
// Inspector에서 설정한 bossSpawnScale을 적용해줘요
bossInstance.transform.localScale = bossSpawnScale; // 설정할거에요 -> 보스 크기를 (씬 배치 시와 동일하게)
Debug.Log($"[BossZone] 보스 스폰 완료! 위치: {spawnPos}, 스케일: {bossSpawnScale}"); // 성공 로그
// ── 4. NorcielBoss 컴포넌트 연결 ─────────────────────────
boss = bossInstance.GetComponent<NorcielBoss>(); // 가져올거에요 -> 생성된 보스의 NorcielBoss 컴포넌트를
if (boss != null) // 조건이 맞으면 실행할거에요 -> 컴포넌트를 찾았으면
{
Debug.Log($"[BossZone] 보스 자동 연결 완료: {boss.name}"); // 성공 로그
}
else // 컴포넌트가 없으면
{
Debug.LogError("[BossZone] 스폰된 보스에 NorcielBoss 컴포넌트가 없어요!"); // 에러 로그
}
}
// ══════════════════════════════════════════════════════════
// 보스 탐색 유틸리티
// ══════════════════════════════════════════════════════════
/// <summary>
/// 씬에서 NorcielBoss를 찾아 자동 연결
/// </summary>
private void TryFindBoss() // 함수를 정의할거에요 -> 보스 탐색 함수를
{
if (boss != null) return; // 이미 있으면 스킵
// 1차: 활성화된 오브젝트에서 찾기
boss = FindObjectOfType<NorcielBoss>(); // 찾을거에요 -> 씬에 있는 활성 NorcielBoss를
// 2차: 비활성 오브젝트까지 탐색 (FindObjectOfType은 inactive를 못 찾음)
if (boss == null) // 조건이 맞으면 실행할거에요 -> 활성 상태에서 못 찾았으면
{
// FindObjectsOfType(true)는 비활성 포함 전체 탐색
NorcielBoss[] allBosses = Resources.FindObjectsOfTypeAll<NorcielBoss>(); // 찾을거에요 -> 비활성 포함 모든 NorcielBoss를
foreach (var b in allBosses) // 반복할거에요 -> 찾은 모든 보스에 대해
{
// 씬에 있는 오브젝트만 (프리팹 에셋 제외)
if (b.gameObject.scene.isLoaded) // 조건이 맞으면 실행할거에요 -> 씬에 로드된 오브젝트면
{
boss = b; // 설정할거에요 -> 찾은 보스를
boss.gameObject.SetActive(true); // 켤거에요 -> 비활성이었으면 활성화
Debug.Log($"[BossZone] 비활성 보스 발견 → 활성화: {boss.name}"); // 로그를 찍을거에요
break; // 중단할거에요 -> 첫 번째만
}
}
}
if (boss != null) // 조건이 맞으면 실행할거에요 -> 찾았으면
Debug.Log($"[BossZone] 보스 자동 연결 완료: {boss.name}"); // 로그를 찍을거에요
}
/// <summary>
/// 보스가 늦게 활성화될 수 있으니 약간의 딜레이 후 재탐색
/// </summary>
private IEnumerator DelayedFindBoss() // 코루틴을 정의할거에요 -> 딜레이 후 보스 탐색을
{
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
float maxWait = 5f; // 설정할거에요 -> 최대 대기 시간을
float interval = 0.5f; // 설정할거에요 -> 탐색 간격을
2026-02-10 15:29:22 +00:00
while (boss == null && elapsed < maxWait) // 반복할거에요 -> 보스를 찾을 때까지
{
yield return new WaitForSeconds(interval); // 대기할거에요 -> 0.5초씩
elapsed += interval; // 더할거에요 -> 경과 시간에
TryFindBoss(); // 시도할거에요 -> 보스 탐색을
}
2026-02-10 15:29:22 +00:00
if (boss == null) // 조건이 맞으면 실행할거에요 -> 끝까지 못 찾았으면
{
Debug.LogWarning("[BossZone] 딜레이 후에도 보스 없음. Resources에서 한 번 더 스폰 시도..."); // 경고 로그
SpawnBossFromResources(); // 실행할거에요 -> 마지막 스폰 시도를
}
}
// ══════════════════════════════════════════════════════════
// 트리거 진입 감지
// ══════════════════════════════════════════════════════════
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌체가 트리거존에 들어왔을 때
2026-02-10 15:29:22 +00:00
{
if (_isTriggered) return; // 중단할거에요 -> 이미 한 번 발동했으면 (중복 방지)
2026-02-10 15:29:22 +00:00
// 트리거 진입 시점에도 보스 재탐색 (최후의 안전장치)
if (boss == null) // 조건이 맞으면 실행할거에요 -> 보스가 없으면
2026-02-10 15:29:22 +00:00
{
TryFindBoss(); // 실행할거에요 -> 마지막으로 한 번 더 찾기
2026-02-10 15:29:22 +00:00
if (boss == null) // 조건이 맞으면 실행할거에요 -> 여전히 없으면
2026-02-13 09:11:54 +00:00
{
Debug.LogWarning("[BossZone] 트리거 진입 시점에 보스 없음! 긴급 스폰!"); // 경고 로그
SpawnBossFromResources(); // 실행할거에요 -> 긴급 스폰을
2026-02-13 09:11:54 +00:00
}
}
if (!other.CompareTag("Player")) return; // 중단할거에요 -> 플레이어가 아니면 (몬스터, 투사체 등 무시)
_isTriggered = true; // 설정할거에요 -> 발동 완료로 (다시는 발동 안 됨)
Debug.Log($"[BossZone] 플레이어 트리거존 진입! boss={boss?.name ?? "NULL"} → 보스전 연출 시작!"); // 로그를 찍을거에요 -> 진입 알림을
// 코루틴으로 연출 순서를 제어해요 (잠금 → 벽 → 카메라 → 포효 → BGM → 해제)
StartCoroutine(BossIntroSequence()); // 시작할거에요 -> 보스 등장 연출 코루틴을
}
// ══════════════════════════════════════════════════════════
// 보스 등장 연출 시퀀스 (코루틴)
// ══════════════════════════════════════════════════════════
private IEnumerator BossIntroSequence() // 코루틴을 정의할거에요 -> 보스 등장 연출 시퀀스를
{
// ── 1. 플레이어 입력 잠금 (즉시) ──────────────────────
// 연출 중 플레이어가 움직이거나 공격하면 몰입이 깨지니까
// 카메라 줌인 시작 전에 바로 잠가요
if (playerInput != null) // 조건이 맞으면 실행할거에요 -> PlayerInput이 연결돼있으면
{
playerInput.LockInput(); // 실행할거에요 -> 모든 입력 차단을
}
// ── 2. 퇴로 차단: 벽 활성화 ──────────────────────────
if (entranceWall != null) entranceWall.SetActive(true); // 켤거에요 -> 입구 벽을 (플레이어가 도망 못 가게)
if (exitWall != null) exitWall.SetActive(true); // 켤거에요 -> 출구 벽을 (반대편도 차단)
// ── 3. 트리거존 비활성화 (더 이상 필요 없음) ──────────
DisableTriggerZone(); // 실행할거에요 -> 트리거존 콜라이더를 끄기
2026-02-10 15:29:22 +00:00
// ── 4. 보스 연출 카메라 켜기 (줌인) ───────────────────
if (bossCamera != null) // 조건이 맞으면 실행할거에요 -> 연출 카메라가 있으면
{
bossCamera.gameObject.SetActive(true); // 켤거에요 -> 보스 연출 카메라를 (보스를 클로즈업으로 비춤)
}
// ── 5. 보스전 BGM 전환 ────────────────────────────────
// AudioManager 싱글톤이 있으면 크로스페이드로 전환해요
if (bossBGM != null && AudioManager.Instance != null) // 조건이 맞으면 실행할거에요 -> BGM 클립 + AudioManager 둘 다 있으면
{
AudioManager.Instance.PlayBGM(bossBGM, bgmFadeDuration); // 재생할거에요 -> 보스전 BGM을 (크로스페이드)
Debug.Log("[BossZone] 보스전 BGM 시작!"); // 로그를 찍을거에요
}
// ── 6. 보스 전투 시작 (포효 + FSM 가동) ───────────────
// StartBossBattle()은 내부적으로 코루틴(BattleStartRoutine)을 돌려요
// 포효 애니메이션이 끝나면 Chase 상태로 전환됨
if (boss != null) // 조건이 맞으면 실행할거에요 -> 보스가 연결돼있으면
{
boss.StartBossBattle(); // 실행할거에요 -> 보스전 시작을 (포효 애니 → Chase 전환)
}
else // 조건이 틀리면 실행할거에요 -> 보스가 없으면
{
Debug.LogError("[BossZone] boss 참조가 비어있어요! 보스 스폰에 실패했습니다."); // 에러를 찍을거에요
}
// ── 7. 연출 카메라 유지 시간만큼 대기 ──────────────────
// 카메라가 보스를 비추는 동안 플레이어는 입력 잠금 상태
// cameraDuration 동안 보스의 포효 연출을 감상할 수 있어요
yield return new WaitForSeconds(cameraDuration); // 기다릴거에요 -> 카메라 연출 시간만큼
// ── 8. 연출 카메라 끄기 (플레이어 카메라 복귀) ─────────
if (bossCamera != null) // 조건이 맞으면 실행할거에요 -> 연출 카메라가 있으면
{
bossCamera.gameObject.SetActive(false); // 끌거에요 -> 연출 카메라를 (플레이어 카메라로 자동 복귀)
}
// ── 9. 플레이어 입력 잠금 해제 ────────────────────────
// 카메라 연출이 끝나면 바로 전투 시작!
if (playerInput != null) // 조건이 맞으면 실행할거에요 -> PlayerInput이 있으면
{
playerInput.UnlockInput(); // 실행할거에요 -> 입력 잠금 해제를 (이동/공격 가능)
}
Debug.Log("[BossZone] 연출 종료 → 전투 시작!"); // 로그를 찍을거에요
}
// ══════════════════════════════════════════════════════════
// 트리거존 비활성화
// ══════════════════════════════════════════════════════════
private void DisableTriggerZone() // 함수를 선언할거에요 -> 트리거존을 끄는
{
if (bossTriggerZone != null) // 조건이 맞으면 실행할거에요 -> 트리거존 오브젝트가 연결돼있으면
{
bossTriggerZone.SetActive(false); // 끌거에요 -> 트리거존 오브젝트 전체를
}
else // 조건이 틀리면 실행할거에요 -> 연결 안 돼있으면
{
// 자기 자신의 콜라이더만 끄기 (오브젝트는 살려둠)
Collider col = GetComponent<Collider>(); // 가져올거에요 -> 자기 자신의 콜라이더를
if (col != null) col.enabled = false; // 끌거에요 -> 콜라이더를 (더 이상 감지 안 함)
}
}
// ══════════════════════════════════════════════════════════
// 보스 사망 시 벽 해제 + BGM 복귀 (외부에서 호출)
// 보스가 죽으면 이 함수를 호출해서 벽을 열고 BGM을 원래대로 돌려줘요
// 사용법: GetComponent<BossZoneTrigger>().OpenWalls();
// ══════════════════════════════════════════════════════════
public void OpenWalls() // 함수를 선언할거에요 -> 벽을 여는 (보스 사망 시 호출)
{
if (entranceWall != null) entranceWall.SetActive(false); // 끌거에요 -> 입구 벽을 (퇴로 개방)
if (exitWall != null) exitWall.SetActive(false); // 끌거에요 -> 출구 벽을 (출구 개방)
Debug.Log("[BossZone] 벽 해제! 통로가 열렸습니다."); // 로그를 찍을거에요 -> 벽 해제 알림을
// ── 보스전 BGM → 필드 BGM 복귀 ───────────────────────
if (AudioManager.Instance != null) // 조건이 맞으면 실행할거에요 -> AudioManager가 있으면
{
if (fieldBGM != null) // 조건이 맞으면 실행할거에요 -> 복귀할 필드 BGM이 있으면
{
AudioManager.Instance.PlayBGM(fieldBGM, bgmFadeDuration); // 재생할거에요 -> 필드 BGM으로 복귀 (크로스페이드)
Debug.Log("[BossZone] 필드 BGM 복귀!"); // 로그를 찍을거에요
}
else // 필드 BGM이 없으면
{
AudioManager.Instance.StopBGM(bgmFadeDuration); // 정지할거에요 -> BGM을 페이드아웃으로
Debug.Log("[BossZone] BGM 정지 (필드 BGM 미설정)"); // 로그를 찍을거에요
}
2026-02-10 15:29:22 +00:00
}
}
}