- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP) - ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트 - 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom) - 씬, 프리팹, 애니메이션 등 전체 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
326 lines
22 KiB
C#
326 lines
22 KiB
C#
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|
using System.Collections; // 코루틴 기능을 불러올거에요 -> IEnumerator를
|
|
|
|
// ============================================================
|
|
// NorcielBoss.FSM.cs — FSM 상태 핸들러 (partial class)
|
|
//
|
|
// [역할]
|
|
// ① RunFSM — 현재 상태에 따라 적절한 핸들러 호출
|
|
// ② FSM_Chase — NavMesh 추격 + 카이팅/도망 감지 → 대시
|
|
// ③ FSM_PreAttack — 사거리 안 쿨타임 대기 + 뒤잡기 감지 + 패턴 실행
|
|
// ④ FSM_Retrieving — 쇠공 회수 이동 + 줍기 트리거
|
|
// ⑤ SetState — FSM 상태 전환 + 로그
|
|
//
|
|
// [설계]
|
|
// partial class로 NorcielBoss의 private 필드에 직접 접근.
|
|
// Update()에서 RunFSM() 한 줄로 호출됩니다.
|
|
// ============================================================
|
|
|
|
public partial class NorcielBoss : MonsterClass // 부분 클래스를 선언할거에요 -> NorcielBoss의 FSM 부분으로
|
|
{
|
|
// ══════════════════════════════════════════════════════════
|
|
// FSM 메인 디스패처
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>현재 FSM 상태에 따라 적절한 핸들러를 호출</summary>
|
|
private void RunFSM() // 함수를 선언할거에요 -> 현재 FSM 상태에 따라 행동을 처리하는
|
|
{
|
|
if (!_isBattleStarted || _target == null) return; // 중단할거에요 -> 전투 전이거나 타겟 없으면
|
|
|
|
switch (_state) // 분기할거에요 -> 현재 상태에 따라
|
|
{
|
|
case BossState.Chase: FSM_Chase(); break; // 실행할거에요 -> 추격 로직을
|
|
case BossState.PreAttack: FSM_PreAttack(); break; // 실행할거에요 -> 공격 대기 로직을
|
|
case BossState.Resting: /* 아무것도 안 함 */ break; // 코루틴이 처리하므로 -> 휴식 중 아무것도 하지 않음
|
|
case BossState.Retrieving: FSM_Retrieving(); break; // 실행할거에요 -> 쇠공 회수 로직을
|
|
// Idle / Attacking / Recovering / Dead → 코루틴이 처리하므로 여기서 아무것도 안 함
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Chase: NavMesh로 플레이어 추격
|
|
//
|
|
// [우선순위]
|
|
// 1. 쇠공 착지 → 회수로 전환
|
|
// 2. 카이팅 감지 → 대시 갭클로저
|
|
// 3. 공격 사거리 진입 → PreAttack
|
|
// 4. 원거리 도망 감지 → 대시 추격
|
|
// 5. 일반 NavMesh 추격
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
private void FSM_Chase() // 함수를 선언할거에요 -> 추격 상태 처리를
|
|
{
|
|
if (_isResting) return; // 중단할거에요 -> 휴식 중이면 (휴식 코루틴이 처리)
|
|
|
|
// ── [v6.5] NavMesh 에이전트 안전 복구 ─────────────────
|
|
// ChargeMonster 방식: 에이전트가 꺼져있으면 강제로 켜기
|
|
// 대시 후 복구가 불완전했을 때를 대비한 안전장치
|
|
if (agent != null && !agent.enabled) // 조건이 맞으면 실행할거에요 -> 에이전트가 꺼져있으면
|
|
{
|
|
agent.enabled = true; // 켤거에요 -> NavMesh 에이전트를 강제로 (대시 후 미복구 대비)
|
|
Debug.LogWarning("[Boss] FSM_Chase: agent.enabled=false 감지 → 강제 복구!"); // 경고를 찍을거에요
|
|
}
|
|
|
|
// ── [v6.5] Rigidbody 안전 복구 ─────────────────
|
|
// Chase 상태에서는 반드시 Kinematic이어야 해요 (NavMesh 이동 모드)
|
|
// 대시 후 isKinematic=false가 남아있으면 미끄러짐 발생
|
|
if (_rb != null && !_rb.isKinematic) // 조건이 맞으면 실행할거에요 -> 물리 모드가 살아있으면
|
|
{
|
|
_rb.velocity = Vector3.zero; // 멈출거에요 -> 잔여 속도를
|
|
_rb.isKinematic = true; // 끌거에요 -> 물리 연산을 강제로
|
|
Debug.LogWarning("[Boss] FSM_Chase: isKinematic=false 감지 → 강제 복구!"); // 경고를 찍을거에요
|
|
}
|
|
|
|
if (!CheckNavMeshReady()) return; // 중단할거에요 -> NavMesh 비정상이면
|
|
|
|
float dist = GetDist(); // 가져올거에요 -> 거리를
|
|
|
|
// ── 1. 쇠공 착지 상태 → 회수 우선 ──────────────
|
|
if (ironBall != null && ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 착지 상태면
|
|
{
|
|
StopAgent(); // 멈출거에요
|
|
SetState(BossState.Retrieving); // 전환할거에요 -> 회수로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
// ── 2. 공격 사거리 진입 → PreAttack ──────────────
|
|
if (dist <= throwRange) // 조건이 맞으면 실행할거에요 -> 사거리 안이면
|
|
{
|
|
StopAgent(); // 멈출거에요
|
|
SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════
|
|
// 3. [v8] 거리 기반 즉시 대시 추격
|
|
//
|
|
// [v8 재설계] 기존 문제:
|
|
// - dashCooldownTime=4초 동안 NavMesh 걷기 → 보스가 이상한 곳 돌아다님
|
|
// - 대시 → Recover → PreAttack → Chase 돌아올 때 쿨다운 남아있음
|
|
// - 4초 걷기 + 대시 + 4초 걷기 → 지루한 반복
|
|
//
|
|
// v8 변경: 플레이어가 멀면 쿨다운 무시하고 즉시 대시!
|
|
// - dist >= dashChaseRange(12m) → 무조건 즉시 대시 (쿨다운 체크 안 함)
|
|
// - dist < dashChaseRange → 쿨다운 적용 (근거리에서는 대시 남발 방지)
|
|
// → 멀리 도망가면 보스가 즉시 달려옴 = 지루함 해소
|
|
// → 가까이서는 적절한 간격 유지 = 게임성 보존
|
|
// ══════════════════════════════════════════════════════
|
|
|
|
// ── [v8] 원거리: 즉시 대시 (쿨다운 무시!) ──────────────
|
|
// 플레이어가 dashChaseRange 이상 멀면 → 걷기 없이 바로 대시
|
|
// Recover 끝나고 돌아와도 즉시 다시 대시 → "이상한 곳 돌아다니기" 원천 차단
|
|
bool farEnough = dist >= dashChaseRange; // 판단할거에요 -> 대시 거리 이상인지를
|
|
bool cooldownReady = _dashCooldownTimer <= 0f; // 판단할거에요 -> 쿨다운 끝났는지를
|
|
bool shouldDash = farEnough || (dist >= meleeRange && cooldownReady); // 판단할거에요 -> 대시 발동 여부를 (멀면 무조건 / 중거리면 쿨다운 후)
|
|
|
|
if (shouldDash && dist >= meleeRange) // 조건이 맞으면 실행할거에요 -> 대시 발동이면
|
|
{
|
|
_dashCooldownTimer = dashCooldownTime; // 설정할거에요 -> 쿨다운을 (근거리 복귀 시 남발 방지용)
|
|
_kitingTimer = 0f; // 초기화할거에요 -> 카이팅 타이머를
|
|
|
|
Debug.Log($"[Boss] v8 대시 추격! dist={dist:F1}m 원거리즉시={farEnough} 쿨다운통과={cooldownReady}"); // 로그를 찍을거에요
|
|
|
|
// Phase/상황에 따라 DashSmash 또는 Dash 선택
|
|
float dsmashRoll = Random.value; // 굴릴거에요 -> 확률 주사위를
|
|
float dsmashThreshold = (phaseManager != null) // 계산할거에요 -> DashSmash 확률을
|
|
? phaseManager.GetDashSmashChance(dashSmashChance) // phaseManager에서 Phase별 확률
|
|
: dashSmashChance; // 기본값
|
|
|
|
if (dsmashRoll < Mathf.Clamp01(dsmashThreshold)) // 조건이 맞으면 실행할거에요 -> DashSmash 당첨이면
|
|
{
|
|
Debug.Log("[Boss] → DashSmash로 추격!"); // 로그를 찍을거에요
|
|
StartCoroutine(Pattern_DashSmash()); // 실행할거에요 -> 대시+내려찍기를
|
|
}
|
|
else // 아니면
|
|
{
|
|
Debug.Log("[Boss] → Dash로 추격!"); // 로그를 찍을거에요
|
|
StartCoroutine(Pattern_Dash()); // 실행할거에요 -> 대시 돌진을
|
|
}
|
|
return; // 중단할거에요 -> FSM_Chase를
|
|
}
|
|
|
|
// ── 4. 일반 NavMesh 추격 ────────────────────────────
|
|
// 근접 사거리 안이거나, 대시 조건 미충족 시 걸어서 추격
|
|
|
|
// ── [버그 수정] NavMesh 자동회전 비활성화 ─────────────
|
|
// 원인: agent.updateRotation=true + FaceTarget() 동시 실행
|
|
// → NavMesh 경로가 곡선일 때 agent가 옆/뒤를 바라보며 회전
|
|
// → FaceTarget()이 플레이어 방향으로 Slerp 충돌 → 빙빙 도는 현상
|
|
// 수정: updateRotation=false로 NavMesh 자동회전 차단 → FaceTarget()만 회전 담당
|
|
if (agent != null) agent.updateRotation = false; // 끌거에요 -> NavMesh 자동회전을 (FaceTarget이 회전 전담)
|
|
|
|
agent.isStopped = false; // 켤거에요 -> 이동을
|
|
agent.SetDestination(_target.position); // 설정할거에요 -> 플레이어 위치로
|
|
FaceTarget(); // 바라볼거에요 -> 플레이어를 (updateRotation=false이므로 이것만 회전 담당)
|
|
PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// PreAttack: 사거리 안, 쿨타임 대기 후 패턴 실행
|
|
//
|
|
// [우선순위]
|
|
// 1. 쇠공 착지 → 회수
|
|
// 2. 사거리 이탈 → 추격 복귀
|
|
// 3. 카이팅 긴급 대응 → 대시 갭클로저
|
|
// 4. 뒤잡기 감지 → 긴급 Sweep
|
|
// 5. 쿨타임 대기 중 접근 이동
|
|
// 6. 쿨타임 만료 → SelectAndFire
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
private void FSM_PreAttack() // 함수를 선언할거에요 -> 공격 대기 상태 처리를
|
|
{
|
|
if (_isResting) return; // 중단할거에요 -> 휴식 중이면 (휴식 코루틴이 처리)
|
|
float dist = GetDist(); // 가져올거에요 -> 거리를
|
|
|
|
// ── 1. 쇠공 착지 상태 → 회수 우선 ──────────────
|
|
if (ironBall != null && ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 착지면
|
|
{
|
|
SetState(BossState.Retrieving); // 전환할거에요 -> 회수로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
// ── 2. 플레이어가 사거리 밖으로 나가면 → 다시 추격 ──
|
|
if (dist > throwRange) // 조건이 맞으면 실행할거에요 -> 사거리 밖이면
|
|
{
|
|
StartAgent(); // 켤거에요 -> NavMesh를
|
|
SetState(BossState.Chase); // 전환할거에요 -> 추격으로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
// ── 3. [카이팅 긴급 대응] ───────────────────────
|
|
// [v6 수정] 직접 대시 발동 → 쿨타임 빠르게 소모로 변경 (이슈5: 대시 빈도 과다)
|
|
// v5 문제: PreAttack에서 직접 Pattern_Dash/DashSmash 호출 → 대시 트리거가 5개나 되어 체감 빈도 과다
|
|
// v6 수정: 카이팅 감지 시 _attackTimer를 빠르게 단축 → SelectAndFire에서 자연스럽게 Dash 선택되게 유도
|
|
// → 대시 발동 경로 1개 제거 (5개→4개, Throw후 제거 포함하면 3개)
|
|
// → SelectAndFire의 가중치 시스템이 카이팅 상황에서 알아서 Dash 가중치를 높여줌
|
|
bool preAttackKiting = _kitingTimer >= kitingDetectTime && dist > meleeRange; // 판단할거에요 -> PreAttack 카이팅 발동 조건을 (v6: 대시쿨다운 체크 제거 — 직접 대시 안 하니까)
|
|
if (preAttackKiting) // 조건이 맞으면 실행할거에요 -> 카이팅 감지면
|
|
{
|
|
Debug.Log($"[Boss] PreAttack 카이팅 감지 → 쿨타임 단축! (dist={dist:F1}m, _attackTimer → 0.3s로 단축)"); // 로그를 찍을거에요
|
|
_attackTimer = Mathf.Min(_attackTimer, 0.3f); // 단축할거에요 -> 공격 쿨타임을 최대 0.3초로 (빠르게 SelectAndFire 도달)
|
|
_kitingTimer = 0f; // 초기화할거에요 -> 카이팅 타이머를 리셋 (중복 감지 방지)
|
|
// v6: 직접 대시 발동 안 함 → SelectAndFire가 카이팅 상황에서 Dash/DashSmash 가중치를 올려줌
|
|
}
|
|
|
|
// ── 4. [반응형] 뒤잡기 감지 → 긴급 Sweep ──────────
|
|
if (counterSystem != null && counterSystem.ShouldReactToBehind() && _state == BossState.PreAttack) // 조건이 맞으면 실행할거에요 -> 뒤잡기 반응 필요하면
|
|
{
|
|
counterSystem.ResetBehindTimer(); // 초기화할거에요 -> 뒤잡기 타이머를
|
|
Debug.Log("[Boss] 뒤잡기 감지 → 긴급 Sweep!"); // 로그를 찍을거에요
|
|
StartCoroutine(Pattern_Sweep()); // 시작할거에요 -> 긴급 휩쓸기를
|
|
return; // 중단할거에요 -> PreAttack을
|
|
}
|
|
|
|
FaceTarget(); // 바라볼거에요 -> 플레이어를
|
|
_attackTimer -= Time.deltaTime; // 줄일거에요 -> 쿨타임을
|
|
|
|
// ── 5. 쿨타임 대기 중 이동 판단 ─────────────────
|
|
// [v5 개선] 기본 정지 거리를 Smash 실제 타격 범위 기준으로 수정
|
|
//
|
|
// 기존 문제 (v3): midStopRange = 6.75m → Smash 범위(5.5m) 밖
|
|
// → 보스가 6.75m에서 멈추고 Smash/Sweep 가중치 0 → Dash/DashSmash만 선택 → 반복
|
|
// → "원거리에서 제자리 공격" 현상 발생
|
|
//
|
|
// 수정: 기본 정지 거리를 actualSmashRange(5.5m) 이내로 설정
|
|
// → 보스가 Smash 타격 범위 안에서 멈추므로 근접 패턴 선택 가능
|
|
// → 인스펙터에서 smashRadius/smashForwardOffset 조정 시 자동 반영
|
|
if (_attackTimer > 0f) // 조건이 맞으면 실행할거에요 -> 아직 쿨타임 중이면
|
|
{
|
|
// [v5] actualSmashRange 기반 정지 거리 — Smash가 닿는 거리에서 멈추게
|
|
float actualSmashRange = smashForwardOffset + smashRadius + 1f; // 계산할거에요 -> Smash 실제 타격 최대 거리를 (1.5+3+1=5.5m)
|
|
float midStopRange = Mathf.Min(actualSmashRange - 0.5f, (meleeRange + throwRange) * 0.5f); // 계산할거에요 -> 정지 거리를 (v5: Smash 범위 안으로, 기존 중간 거리보다 가까이)
|
|
|
|
// [v5] 플레이어가 정지 + 피격 OR 쿨타임 중 접근 필요 → 근접까지 접근
|
|
bool playerCamping = _playerStillTimer >= stillPunishTime && _recentHitCount >= 1; // 판단할거에요 -> 플레이어 캠핑 여부를 (정지 + 보스 피격 중)
|
|
float approachTarget = playerCamping ? meleeRange : midStopRange; // 결정할거에요 -> 접근 목표를 (캠핑이면 근접, 아니면 Smash 범위 내)
|
|
|
|
if (dist <= meleeRange) // 조건이 맞으면 실행할거에요 -> 이미 근접 사거리 안이면
|
|
{
|
|
StopAgent(); // 멈출거에요 -> NavMesh를
|
|
PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를
|
|
}
|
|
else if (dist > approachTarget) // 조건이 맞으면 실행할거에요 -> 목표 거리보다 멀면
|
|
{
|
|
if (CheckNavMeshReady()) // 조건이 맞으면 실행할거에요 -> NavMesh 정상이면
|
|
{
|
|
// ── [버그 수정] PreAttack 이동 중 자동회전 차단 ──
|
|
// Chase와 동일한 원인: NavMesh 곡선 경로 → 자동회전 충돌 → 빙빙 돔
|
|
agent.updateRotation = false; // 끌거에요 -> NavMesh 자동회전을 (FaceTarget이 회전 전담)
|
|
agent.isStopped = false; // 켤거에요 -> 이동을
|
|
Vector3 dirToPlayer = (_target.position - transform.position).normalized; // 계산할거에요 -> 플레이어 방향을
|
|
Vector3 stopDest = _target.position - dirToPlayer * approachTarget; // 계산할거에요 -> 목표 지점을 (캠핑: 근접 / 일반: 중거리)
|
|
agent.SetDestination(stopDest); // 설정할거에요 -> 목표 지점으로 이동
|
|
}
|
|
PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를
|
|
}
|
|
else // 목표 거리 이내에 도착했으면
|
|
{
|
|
StopAgent(); // 멈출거에요 -> NavMesh를
|
|
PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를
|
|
}
|
|
return; // 중단할거에요 -> 쿨타임 끝날 때까지
|
|
}
|
|
|
|
// ── 6. 쿨타임 만료 → 현재 dist 기반 패턴 실행 ──────
|
|
StopAgent(); // 멈출거에요 -> NavMesh를 (패턴 실행 전 정지)
|
|
PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를
|
|
_attackTimer = patternInterval + Random.Range(-intervalVariance, intervalVariance); // 재설정할거에요 -> 쿨타임을 (±분산 적용)
|
|
SelectAndFire(dist); // 실행할거에요 -> 현재 dist 기반 패턴 선택 + 실행
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Retrieving: 쇠공 위치로 이동 후 줍기
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
private void FSM_Retrieving() // 함수를 선언할거에요 -> 쇠공 회수 상태 처리를
|
|
{
|
|
// [D-5 수정] Retrieving 진입 시 updateRotation 명시적 복원
|
|
// 이전 패턴(Dash/BossRest 등)에서 false로 설정된 채 남아있을 수 있음
|
|
// → true로 되돌려야 FaceTarget(ironBall)이 정상 회전을 가져감
|
|
if (agent != null) agent.updateRotation = true; // 복원할거에요 -> NavMesh 자동회전을 (이전 패턴에서 꺼진 채 남아있을 수 있음)
|
|
|
|
if (ironBall == null || !ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 없거나 착지 안 됐으면
|
|
{
|
|
SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
// ── 도달 불가능 높이 체크 ──────────────────────────
|
|
float ballHeightDiff = ironBall.transform.position.y - transform.position.y; // 계산할거에요 -> 높이 차이를
|
|
if (ballHeightDiff > 3f) // 조건이 맞으면 실행할거에요 -> 3m 이상 위면
|
|
{
|
|
Debug.LogWarning($"[Boss] 쇠공이 도달 불가능한 높이에요! (높이차={ballHeightDiff:F1}m) → 강제 회수!"); // 경고를 찍을거에요
|
|
ironBall.ForceReturn(handHolder); // 실행할거에요 -> 강제 회수를
|
|
SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로
|
|
return; // 중단할거에요
|
|
}
|
|
|
|
if (!CheckNavMeshReady()) return; // 중단할거에요 -> NavMesh 비정상이면
|
|
|
|
agent.isStopped = false; // 켤거에요 -> 이동을
|
|
agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 쇠공 위치로
|
|
PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를
|
|
FaceTarget(ironBall.transform.position); // 바라볼거에요 -> 쇠공을
|
|
|
|
// 충분히 가까우면 줍기 코루틴 시작
|
|
float distToBall = Vector3.Distance(transform.position, ironBall.transform.position); // 계산할거에요 -> 쇠공까지 거리를
|
|
if (distToBall <= 2.0f) // 조건이 맞으면 실행할거에요 -> 2m 이내면
|
|
{
|
|
SetState(BossState.Attacking); // 잠글거에요 -> 중복 진입 방지
|
|
StartCoroutine(PickupBall()); // 시작할거에요 -> 줍기 코루틴을
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// FSM 상태 변경 유틸리티
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
/// <summary>FSM 상태를 변경하고 디버그 로그를 출력</summary>
|
|
private void SetState(BossState s) // 함수를 선언할거에요 -> FSM 상태를 변경하는
|
|
{
|
|
if (_state == s) return; // 중단할거에요 -> 같은 상태면 중복 설정 방지
|
|
_state = s; // 설정할거에요 -> 새 상태로
|
|
Debug.Log($"[BossFSM] → {s}"); // 로그를 찍을거에요
|
|
}
|
|
}
|