- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP) - ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트 - 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom) - 씬, 프리팹, 애니메이션 등 전체 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
356 lines
23 KiB
C#
356 lines
23 KiB
C#
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// BossZoneTrigger — 보스방 입장 트리거
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 사용법:
|
|
// 1. 빈 GameObject에 BoxCollider(isTrigger=true) + 이 스크립트 부착
|
|
// 2. Inspector에서 boss, entranceWall, playerInput 연결
|
|
// 3. 플레이어가 트리거존에 들어오면:
|
|
// 입구 벽 활성화 → 입력 잠금 → 카메라 줌인 → 보스 포효 → BGM 전환
|
|
// → 입력 해제 → 전투 시작
|
|
//
|
|
// ★ 빌드에서 보스 프리팹이 누락되는 문제 대응:
|
|
// 보스가 씬에 없으면 Resources/BossMonster/BossMonster 에서
|
|
// 프리팹을 로드하여 런타임에 직접 생성합니다.
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
public class BossZoneTrigger : MonoBehaviour // 클래스를 선언할거에요 -> 보스방 입장 트리거를
|
|
{
|
|
// ── 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; // 설정할거에요 -> 탐색 간격을
|
|
|
|
while (boss == null && elapsed < maxWait) // 반복할거에요 -> 보스를 찾을 때까지
|
|
{
|
|
yield return new WaitForSeconds(interval); // 대기할거에요 -> 0.5초씩
|
|
elapsed += interval; // 더할거에요 -> 경과 시간에
|
|
TryFindBoss(); // 시도할거에요 -> 보스 탐색을
|
|
}
|
|
|
|
if (boss == null) // 조건이 맞으면 실행할거에요 -> 끝까지 못 찾았으면
|
|
{
|
|
Debug.LogWarning("[BossZone] 딜레이 후에도 보스 없음. Resources에서 한 번 더 스폰 시도..."); // 경고 로그
|
|
SpawnBossFromResources(); // 실행할거에요 -> 마지막 스폰 시도를
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// 트리거 진입 감지
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 충돌체가 트리거존에 들어왔을 때
|
|
{
|
|
if (_isTriggered) return; // 중단할거에요 -> 이미 한 번 발동했으면 (중복 방지)
|
|
|
|
// 트리거 진입 시점에도 보스 재탐색 (최후의 안전장치)
|
|
if (boss == null) // 조건이 맞으면 실행할거에요 -> 보스가 없으면
|
|
{
|
|
TryFindBoss(); // 실행할거에요 -> 마지막으로 한 번 더 찾기
|
|
|
|
if (boss == null) // 조건이 맞으면 실행할거에요 -> 여전히 없으면
|
|
{
|
|
Debug.LogWarning("[BossZone] 트리거 진입 시점에 보스 없음! 긴급 스폰!"); // 경고 로그
|
|
SpawnBossFromResources(); // 실행할거에요 -> 긴급 스폰을
|
|
}
|
|
}
|
|
|
|
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(); // 실행할거에요 -> 트리거존 콜라이더를 끄기
|
|
|
|
// ── 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 미설정)"); // 로그를 찍을거에요
|
|
}
|
|
}
|
|
}
|
|
}
|