2026-02-19 18:14:55 +00:00
|
|
|
|
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|
|
|
|
|
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
{
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 인스펙터 설정
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
[Header("두뇌 연결")]
|
|
|
|
|
|
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 패턴 결정용 카운터 시스템을
|
|
|
|
|
|
|
|
|
|
|
|
[Header("패턴 설정")]
|
|
|
|
|
|
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을
|
|
|
|
|
|
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 공격 가능 사거리를
|
|
|
|
|
|
|
|
|
|
|
|
[Header("쇠공 설정")]
|
|
|
|
|
|
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던질 쇠공 오브젝트를
|
|
|
|
|
|
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 쇠공을 쥘 손 위치를
|
|
|
|
|
|
|
|
|
|
|
|
[Header("UI")]
|
|
|
|
|
|
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스 체력바 UI를
|
|
|
|
|
|
|
|
|
|
|
|
[Header("애니메이션 State 이름")]
|
|
|
|
|
|
[SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 줍기 애니 이름을
|
2026-02-13 09:11:54 +00:00
|
|
|
|
[SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니 이름을
|
2026-02-19 18:14:55 +00:00
|
|
|
|
[SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 찍기 애니 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니 이름을
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 내부 상태 변수
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private Rigidbody rb; // 변수를 선언할거에요 -> 보스 물리 몸체를
|
|
|
|
|
|
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공 물리 몸체를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
private Transform target; // 변수를 선언할거에요 -> 공격 대상(플레이어)을
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임 타이머를
|
|
|
|
|
|
|
|
|
|
|
|
private bool isBattleStarted = false; // 변수를 선언할거에요 -> 전투 시작 여부를
|
|
|
|
|
|
private bool isWeaponless = false; // 변수를 선언할거에요 -> 쇠공 분실 여부를
|
|
|
|
|
|
private bool isPerformingAction = false; // 변수를 선언할거에요 -> 행동 수행 중 여부를
|
|
|
|
|
|
|
|
|
|
|
|
// 애니메이션 이벤트 신호용 플래그
|
|
|
|
|
|
private bool _animEventFired = false; // 변수를 선언할거에요 -> OnAnimEnd 수신 여부를
|
|
|
|
|
|
private bool _throwEventFired = false; // 변수를 선언할거에요 -> OnThrowRelease 수신 여부를
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 초기화
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
protected override void Awake() // 함수를 실행할거에요 -> 컴포넌트 초기화를
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Awake(); // 실행할거에요 -> 부모 Awake를
|
|
|
|
|
|
rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 보스 Rigidbody를
|
|
|
|
|
|
ballRb = ironBall != null ? ironBall.GetComponent<Rigidbody>() : null; // 가져올거에요 -> 쇠공 Rigidbody를
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void Init() // 함수를 실행할거에요 -> 스탯/상태 초기화를
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Init(); // 실행할거에요 -> 부모 Init를
|
|
|
|
|
|
|
|
|
|
|
|
_timer = 0f; // 초기화할거에요 -> 타이머를
|
|
|
|
|
|
isBattleStarted = isWeaponless = isPerformingAction = false; // 초기화할거에요 -> 전투/무기/행동 상태를
|
|
|
|
|
|
IsAggroed = true; // 설정할거에요 -> 어그로를 항상 켠 상태로
|
|
|
|
|
|
mobRenderer = null; // 초기화할거에요 -> 렌더러 참조를 (보스는 최적화 안 함)
|
|
|
|
|
|
optimizationDistance = 9999f; // 설정할거에요 -> 최적화 거리를 무한대로
|
|
|
|
|
|
|
|
|
|
|
|
// 플레이어 탐색
|
|
|
|
|
|
GameObject playerObj = GameObject.FindWithTag("Player"); // 찾을거에요 -> Player 태그 오브젝트를
|
|
|
|
|
|
if (playerObj != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
target = playerTransform = playerObj.transform; // 설정할거에요 -> 타겟과 부모 참조를
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
PlayerMovement pm = FindObjectOfType<PlayerMovement>(); // 찾을거에요 -> PlayerMovement 스크립트로
|
|
|
|
|
|
if (pm != null) target = playerTransform = pm.transform; // 설정할거에요 -> 타겟을
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (target == null) Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다!"); // 에러를 찍을거에요
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
|
|
|
|
|
currentHP = maxHP; // 설정할거에요 -> 체력을 최대로
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
|
|
|
|
|
|
if (ballRb != null) ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
StartCoroutine(SafeInitNavMesh()); // 시작할거에요 -> NavMesh 안전 초기화를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private IEnumerator SafeInitNavMesh() // 코루틴을 정의할거에요 -> NavMesh 위에 올라올 때까지 대기하는
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return null; // 대기할거에요 -> 1프레임을
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (agent == null) yield break; // 중단할거에요 -> 에이전트가 없으면
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
|
|
|
|
|
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> NavMesh 위에 올라올 때까지
|
|
|
|
|
|
{ retry++; yield return new WaitForSeconds(0.1f); } // 기다릴거에요 -> 0.1초씩
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = true; // 정지시킬거에요 -> NavMesh 위면
|
|
|
|
|
|
agent.enabled = false; // 끌거에요 -> 전투 시작 전까지 비활성화
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private void Start() // 함수를 실행할거에요 -> 게임 시작 시
|
|
|
|
|
|
{
|
|
|
|
|
|
StartBossBattle(); // 실행할거에요 -> 보스전 시작을
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
protected override void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Update(); // 실행할거에요 -> 부모 Update를
|
|
|
|
|
|
if (!isDead) ExecuteAILogic(); // 실행할거에요 -> 살아있으면 AI를
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 🎬 Animation Event 수신 함수
|
|
|
|
|
|
//
|
|
|
|
|
|
// Unity 에서 등록 방법:
|
|
|
|
|
|
// FBX 선택 → Animation 탭 → 클립 선택 →
|
|
|
|
|
|
// 마지막 프레임 클릭 → Add Event → Function 입력
|
|
|
|
|
|
//
|
|
|
|
|
|
// OnAnimEnd : Roar / Attack_Throw / Skill_Pickup /
|
|
|
|
|
|
// Skill_Dash_Ready / Skill_Dash_Go /
|
|
|
|
|
|
// Skill_Smash_Impact / Skill_Sweep 마지막 프레임
|
|
|
|
|
|
// OnThrowRelease : Attack_Throw 에서 공이 손을 떠나는 프레임
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
public void OnAnimEnd() // 함수를 선언할거에요 -> 애니 종료 신호 수신을
|
|
|
|
|
|
{
|
|
|
|
|
|
_animEventFired = true; // 설정할거에요 -> 종료 신호를
|
|
|
|
|
|
Debug.Log("🎬 [BossEvent] OnAnimEnd"); // 로그를 찍을거에요 -> 수신 확인용으로
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void OnThrowRelease() // 함수를 선언할거에요 -> 공 발사 신호 수신을
|
|
|
|
|
|
{
|
|
|
|
|
|
_throwEventFired = true; // 설정할거에요 -> 발사 신호를
|
|
|
|
|
|
Debug.Log("🎱 [BossEvent] OnThrowRelease"); // 로그를 찍을거에요 -> 수신 확인용으로
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 이벤트 대기 코루틴
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerator PlayAndWaitEvent(string animName, float maxWait = 10f) // 코루틴을 정의할거에요 -> 이벤트 대기 재생을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!animator.HasState(0, Animator.StringToHash(animName))) // 조건이 맞으면 실행할거에요 -> State가 없다면
|
|
|
|
|
|
{ Debug.LogError($"❌ [Boss] State \"{animName}\" 없음!"); yield break; } // 에러를 찍고 중단할거에요
|
|
|
|
|
|
|
|
|
|
|
|
_animEventFired = false; // 초기화할거에요 -> 이전 신호를
|
|
|
|
|
|
animator.Play(animName, 0, 0f); // 재생할거에요 -> 해당 애니메이션을
|
|
|
|
|
|
yield return null; // 대기할거에요 -> 1프레임 (상태 전환 대기)
|
|
|
|
|
|
|
|
|
|
|
|
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
|
|
|
|
|
while (!_animEventFired && elapsed < maxWait) // 반복할거에요 -> 이벤트 올 때까지
|
|
|
|
|
|
{ elapsed += Time.deltaTime; yield return null; } // 기다릴거에요 -> 1프레임씩
|
|
|
|
|
|
|
|
|
|
|
|
if (!_animEventFired) // 조건이 맞으면 실행할거에요 -> 이벤트가 안 왔다면
|
|
|
|
|
|
Debug.LogWarning($"⚠️ [Boss] \"{animName}\" OnAnimEnd 미수신 (maxWait={maxWait}s)"); // 경고를 찍을거에요
|
|
|
|
|
|
|
|
|
|
|
|
_animEventFired = false; // 초기화할거에요 -> 신호를
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 🤖 AI 로직
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
protected override void ExecuteAILogic() // 함수를 정의할거에요 -> 매 프레임 AI 행동을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isPerformingAction || !isBattleStarted || target == null) return; // 중단할거에요 -> 행동 중/시작 전/타겟 없으면
|
|
|
|
|
|
if (isAttacking || isHit || isResting) return; // 중단할거에요 -> 전투 중간 상태면
|
|
|
|
|
|
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> NavMesh 상태 비정상이면
|
|
|
|
|
|
|
|
|
|
|
|
if (isWeaponless) { RetrieveWeaponLogic(); return; } // 실행할거에요 -> 무기 없으면 회수 우선
|
|
|
|
|
|
|
|
|
|
|
|
float dist = Vector3.Distance(transform.position, target.position); // 계산할거에요 -> 플레이어와의 거리를
|
|
|
|
|
|
|
|
|
|
|
|
if (dist > attackRange) // 조건이 맞으면 실행할거에요 -> 사거리 밖이면 추격
|
|
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
agent.isStopped = false; // 켤거에요 -> 이동을
|
2026-02-19 18:14:55 +00:00
|
|
|
|
agent.SetDestination(target.position); // 설정할거에요 -> 목적지를
|
|
|
|
|
|
LookAt(target.position); // 바라볼거에요 -> 플레이어를
|
|
|
|
|
|
UpdateMoveAnim(); // 실행할거에요 -> 이동 애니를
|
|
|
|
|
|
}
|
|
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 사거리 안이면 공격 판정
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.isStopped = true; // 멈출거에요 -> 이동을
|
|
|
|
|
|
agent.velocity = Vector3.zero; // 제거할거에요 -> 관성을
|
|
|
|
|
|
LookAt(target.position); // 바라볼거에요 -> 플레이어를
|
|
|
|
|
|
|
|
|
|
|
|
if (animator.GetCurrentAnimatorStateInfo(0).IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이면
|
|
|
|
|
|
animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
|
|
|
|
|
|
|
|
|
|
|
_timer -= Time.deltaTime; // 줄일거에요 -> 쿨타임을
|
|
|
|
|
|
if (_timer <= 0f) // 조건이 맞으면 실행할거에요 -> 쿨타임이 끝났으면
|
|
|
|
|
|
{
|
|
|
|
|
|
_timer = patternInterval; // 재설정할거에요 -> 쿨타임을
|
|
|
|
|
|
Debug.Log($"⏰ [Boss] 공격 시작! 거리={dist:F1}m"); // 로그를 찍을거에요
|
|
|
|
|
|
DecideAttack(); // 실행할거에요 -> 패턴 결정을
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 전투 시작
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
public void StartBossBattle() // 함수를 선언할거에요 -> 보스전 시작을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isBattleStarted) return; // 중단할거에요 -> 이미 시작했으면
|
|
|
|
|
|
StartCoroutine(BattleStartRoutine()); // 시작할거에요 -> 전투 시작 코루틴을
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
|
|
|
|
|
private IEnumerator BattleStartRoutine() // 코루틴을 정의할거에요 -> 전투 시작 연출을
|
2026-02-19 18:14:55 +00:00
|
|
|
|
{
|
|
|
|
|
|
isPerformingAction = true; // 설정할거에요 -> 행동 중으로
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 켤거에요 -> 체력바를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (counterSystem != null) counterSystem.InitializeBattle(); // 초기화할거에요 -> 카운터 시스템을
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
Debug.Log("🔥 [Boss] 포효 시작!"); // 로그를 찍을거에요
|
|
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_Roar, 10f)); // 대기할거에요 -> 포효 애니가 끝날 때까지
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
animator.Play(anim_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
isBattleStarted = true; // 설정할거에요 -> 전투 시작 상태로
|
|
|
|
|
|
Debug.Log("⚔️ [Boss] 전투 시작!"); // 로그를 찍을거에요
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMesh 에이전트가 있다면
|
|
|
|
|
|
{
|
2026-02-13 09:11:54 +00:00
|
|
|
|
agent.enabled = true; // 켤거에요 -> 에이전트를
|
|
|
|
|
|
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh 위에 올라올 때까지
|
|
|
|
|
|
{ retry++; yield return new WaitForSeconds(0.1f); } // 기다릴거에요 -> 0.1초씩
|
|
|
|
|
|
|
|
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = false; // 켤거에요 -> 이동을
|
|
|
|
|
|
else Debug.LogError("❌ [Boss] NavMesh 연결 실패!"); // 에러를 찍을거에요
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 패턴 결정 & 실행
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
private void DecideAttack() // 함수를 선언할거에요 -> 공격 패턴을 결정하는
|
|
|
|
|
|
{
|
|
|
|
|
|
LookAt(target.position); // 바라볼거에요 -> 공격 전 플레이어를
|
|
|
|
|
|
string pattern = counterSystem != null ? counterSystem.SelectBossPattern() : "Normal"; // 선택할거에요 -> 패턴을
|
|
|
|
|
|
Debug.Log($"🧠 [Boss] 패턴: {pattern}"); // 로그를 찍을거에요
|
|
|
|
|
|
|
|
|
|
|
|
switch (pattern) // 분기할거에요 -> 패턴에 따라
|
|
|
|
|
|
{
|
|
|
|
|
|
case "DashAttack": StartCoroutine(Pattern_Dash()); break; // 실행할거에요 -> 돌진 공격을
|
|
|
|
|
|
case "Smash": StartCoroutine(Pattern_Smash()); break; // 실행할거에요 -> 찍기 공격을
|
|
|
|
|
|
case "ShieldWall": StartCoroutine(Pattern_Sweep()); break; // 실행할거에요 -> 휩쓸기 공격을
|
|
|
|
|
|
default: StartCoroutine(Pattern_Throw()); break; // 실행할거에요 -> 기본: 쇠공 던지기를
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── 쇠공 던지기 ─────────────────────────────────
|
|
|
|
|
|
private IEnumerator Pattern_Throw() // 코루틴을 정의할거에요 -> 쇠공 던지기 패턴을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
|
|
|
|
|
_throwEventFired = false; // 초기화할거에요 -> 발사 신호를
|
|
|
|
|
|
|
|
|
|
|
|
if (!animator.HasState(0, Animator.StringToHash(anim_Throw))) // 조건이 맞으면 실행할거에요 -> State가 없으면
|
|
|
|
|
|
{ Debug.LogError($"❌ [Boss] State \"{anim_Throw}\" 없음!"); OnAttackEnd(); yield break; } // 에러 후 중단할거에요
|
|
|
|
|
|
|
|
|
|
|
|
animator.Play(anim_Throw, 0, 0f); // 재생할거에요 -> 던지기 애니를
|
|
|
|
|
|
yield return null; // 대기할거에요 -> 1프레임을
|
|
|
|
|
|
|
|
|
|
|
|
// 발사 이벤트 대기
|
|
|
|
|
|
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
|
|
|
|
|
while (!_throwEventFired && elapsed < 10f) // 반복할거에요 -> OnThrowRelease 신호 올 때까지
|
|
|
|
|
|
{ elapsed += Time.deltaTime; yield return null; } // 기다릴거에요 -> 1프레임씩
|
|
|
|
|
|
|
|
|
|
|
|
// 공 실제 발사
|
|
|
|
|
|
if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공이 있다면
|
|
|
|
|
|
{
|
|
|
|
|
|
ironBall.transform.SetParent(null); // 분리할거에요 -> 손에서 쇠공을
|
|
|
|
|
|
ballRb.isKinematic = false; // 켤거에요 -> 물리를
|
|
|
|
|
|
Vector3 dir = (target.position - transform.position).normalized; // 계산할거에요 -> 발사 방향을
|
|
|
|
|
|
ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 던질거에요 -> 힘을 줘서
|
|
|
|
|
|
}
|
|
|
|
|
|
isWeaponless = true; // 설정할거에요 -> 무기 없음 상태를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// 애니 끝 이벤트 대기
|
|
|
|
|
|
_throwEventFired = false; // 초기화할거에요 -> 발사 신호를 (OnAnimEnd 신호와 혼동 방지)
|
|
|
|
|
|
elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
|
|
|
|
|
while (!_animEventFired && elapsed < 10f) // 반복할거에요 -> OnAnimEnd 신호 올 때까지
|
|
|
|
|
|
{ elapsed += Time.deltaTime; yield return null; } // 기다릴거에요
|
|
|
|
|
|
_animEventFired = false; // 초기화할거에요 -> 종료 신호를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ─── 돌진 ─────────────────────────────────────────
|
|
|
|
|
|
private IEnumerator Pattern_Dash() // 코루틴을 정의할거에요 -> 돌진 공격 패턴을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
|
|
|
|
|
if (agent != null) agent.enabled = false; // 끌거에요 -> NavMesh를 (물리 이동 위해)
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_DashReady, 5f)); // 대기할거에요 -> 준비 애니가 끝날 때까지
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 날아갈거에요 -> 전방으로
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_DashGo, 5f)); // 대기할거에요 -> 돌진 애니가 끝날 때까지
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 멈출거에요 -> 속도를
|
|
|
|
|
|
if (agent != null && isBattleStarted) agent.enabled = true; // 켤거에요 -> NavMesh를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ─── 찍기 ──────────────────────────────────────────
|
|
|
|
|
|
private IEnumerator Pattern_Smash() // 코루틴을 정의할거에요 -> 찍기 공격 패턴을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
|
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_SmashImpact, 8f)); // 대기할거에요 -> 찍기 애니가 끝날 때까지
|
|
|
|
|
|
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ─── 휩쓸기 ───────────────────────────────────────
|
|
|
|
|
|
private IEnumerator Pattern_Sweep() // 코루틴을 정의할거에요 -> 휩쓸기 공격 패턴을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
|
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_Sweep, 8f)); // 대기할거에요 -> 휩쓸기 애니가 끝날 때까지
|
|
|
|
|
|
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 무기 회수
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 쇠공 회수 로직을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 상태가 비정상이면
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 쇠공 위치로
|
|
|
|
|
|
agent.isStopped = false; // 켤거에요 -> 이동을
|
|
|
|
|
|
UpdateMoveAnim(); // 실행할거에요 -> 이동 애니를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
float distToBall = Vector3.Distance(transform.position, ironBall.transform.position); // 계산할거에요 -> 쇠공과의 거리를
|
|
|
|
|
|
if (distToBall <= 3f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 충분히 가까워지면
|
|
|
|
|
|
StartCoroutine(PickUpRoutine()); // 시작할거에요 -> 줍기 코루틴을
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private IEnumerator PickUpRoutine() // 코루틴을 정의할거에요 -> 쇠공 줍기 과정을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를 (이동 정지용)
|
|
|
|
|
|
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 에이전트를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
yield return StartCoroutine(PlayAndWaitEvent(anim_Pickup, 8f)); // 대기할거에요 -> 줍기 애니가 끝날 때까지
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
ironBall.transform.SetParent(handHolder); // 붙일거에요 -> 쇠공을 손에
|
|
|
|
|
|
ironBall.transform.localPosition = Vector3.zero; // 초기화할거에요 -> 상대 위치를
|
|
|
|
|
|
ironBall.transform.localRotation = Quaternion.identity; // 초기화할거에요 -> 상대 회전을
|
|
|
|
|
|
if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 고정할거에요 -> 쇠공 물리를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
isWeaponless = false; // 해제할거에요 -> 무기 없음 상태를
|
|
|
|
|
|
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 재개할거에요 -> 이동을
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 공격 시작 / 종료 / 회복
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
public override void OnAttackStart() // 함수를 정의할거에요 -> 공격 시작 공통 처리를
|
|
|
|
|
|
{
|
|
|
|
|
|
isAttacking = isPerformingAction = true; // 설정할거에요 -> 공격/행동 중으로
|
|
|
|
|
|
if (agent != null) agent.isStopped = true; // 멈출거에요 -> 이동을
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
public override void OnAttackEnd() // 함수를 정의할거에요 -> 공격 종료 공통 처리를
|
|
|
|
|
|
{
|
|
|
|
|
|
StartCoroutine(RecoverRoutine()); // 시작할거에요 -> 회복 코루틴을
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerator RecoverRoutine() // 코루틴을 정의할거에요 -> 공격 후 휴식을
|
|
|
|
|
|
{
|
|
|
|
|
|
isAttacking = false; // 해제할거에요 -> 공격 상태를
|
|
|
|
|
|
isResting = true; // 설정할거에요 -> 휴식 상태를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
|
|
|
|
|
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 패턴 간격만큼
|
2026-02-13 09:11:54 +00:00
|
|
|
|
isResting = false; // 해제할거에요 -> 휴식 상태를
|
|
|
|
|
|
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 유틸 함수
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private void LookAt(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟 방향 바라보기를
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 dir = targetPos - transform.position; // 계산할거에요 -> 방향 벡터를
|
|
|
|
|
|
dir.y = 0f; // 무시할거에요 -> 높이 차이를
|
|
|
|
|
|
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 있다면
|
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation,
|
|
|
|
|
|
Quaternion.LookRotation(dir), Time.deltaTime * 5f); // 회전할거에요 -> 부드럽게
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
private void UpdateMoveAnim() // 함수를 선언할거에요 -> 이동 애니메이션 갱신을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 중단할거에요 -> 행동 불가 상태면
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
bool isMoving = agent != null && agent.velocity.magnitude > 0.1f; // 판단할거에요 -> 이동 중인지를
|
|
|
|
|
|
var state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 애니 상태를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (isMoving && !state.IsName(anim_Walk)) animator.Play(anim_Walk); // 재생할거에요 -> 걷기 애니를
|
|
|
|
|
|
if (!isMoving && !state.IsName(anim_Idle) && !state.IsName(anim_Roar)) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
|
|
|
|
|
}
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
|
// 사망 / 피격 오버라이드
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
2026-02-19 18:14:55 +00:00
|
|
|
|
protected override void OnStartHit() { } // 함수를 비워둘거에요 -> 보스 슈퍼아머 (피격 시 행동 안 끊김)
|
2026-02-13 09:11:54 +00:00
|
|
|
|
|
|
|
|
|
|
protected override void OnDie() // 함수를 정의할거에요 -> 사망 시 처리를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
{
|
|
|
|
|
|
isBattleStarted = isPerformingAction = false; // 해제할거에요 -> 전투/행동 상태를
|
2026-02-13 09:11:54 +00:00
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
|
2026-02-19 18:14:55 +00:00
|
|
|
|
if (ObsessionSystem.instance != null) ObsessionSystem.instance.AddRunXP(100); // 지급할거에요 -> 경험치를
|
|
|
|
|
|
else Debug.LogError("[Boss] ObsessionSystem 인스턴스 없음!"); // 에러를 찍을거에요
|
|
|
|
|
|
base.OnDie(); // 실행할거에요 -> 부모 사망 처리를
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|