몬스터 전투 영역 설계 (추적 · 공격 · 판정 동기화)

목차

1. 요구 사항

2. 설계 목표

3. 흐름도

4. 구현

        4.1. CHASE(플레이어 추적) 상태

       4.2. ATTACK(공격) 상태

       4.3. 공격 판정 구조

       4.4. 실제 데미지 적용 (애니메이션 이벤트 기반)

       4.5. 회전 보조 함수

5. 개발 의도

1. 시스템 요구 사항

몬스터는 플레이어를 인지한 이후, 단순히 상태만 변경되는 것이 아니라 실제로 플레이어를 추적하고 공격해야 한다.

추적은 NavMesh 기반 이동으로 처리하되, 최대 활동 영역을 벗어나지 않도록 시작 위치 기준 거리(distFromStart)를 항상 검사해야 한다.

또한 공격은 단순 거리 조건 충족 시 즉시 데미지를 주는 방식이 아니라, 애니메이션 타이밍과 동기화되어야 하며, 공격 판정은 물리 충돌(Trigger) 기반으로 관리되어야 한다.

이 시스템은 다음을 만족해야 한다.

플레이어 인지 시 CHASE로 전환되며, 공격 거리(attackDist) 이내에 진입하면 ATTACK 상태로 전환되어 이동이 중단되어야 한다.

공격 판정은 애니메이션 이벤트 시점에만 적용되어야 하며, 플레이어가 범위 밖으로 벗어나면 즉시 추적으로 복귀하거나 영역 복귀 상태로 전환되어야 한다.

모든 전이는 상태 우선순위를 명확히 유지해야 하며, 순찰 상태와 충돌하지 않아야 한다.

2. 설계  목표

- CHASE 상태에서 플레이어 실시간 추적

- 최대 추적 거리(maxChaseDist) 초과 시 즉시 복귀

- ATTACK 상태에서 이동 정지 및 회전 유지

- 애니메이션 이벤트 기반 실제 데미지 적용

- Trigger 기반 공격 유효 범위 판정

3. 흐름도

IDLE / MOVE

   ↓ (dist ≤ chaseDist)

CHASE

   ↓ (dist ≤ attackDist)

ATTACK

   ↓ (dist > attackDist)

CHASE

   ↓ (영역 초과 or dist > chaseDist)

RETURN

전투는 '추적 ↔ 공격' 의 순환 구조이며, 이 루프는 항상 영역 제한 조건을 최상위 가드로 둔다.

전이 우선순위는 다음과 같다.

  1. 활동 영역 초과
  2. 타겟 인지 범위 이탈
  3. 공격 거리 진입
  4. 일반 추적

이 순서를 유지함으로써, 전투는 항상 영역 규칙을 침범하지 않는다.

4. 구현

4.1. CHASE(플레이어 추적) 상태
protected void UpdateChase(float dist, float distFromStart)
{
    if (distFromStart >= maxChaseDist - 0.1f)
    {
        ReturnToStart();
        return;
    }

    if (dist > chaseDist)
    {
        ReturnToStart();
        return;
    }

    if (dist <= attackDist)
    {
        state = State.ATTACK;
        agent.isStopped = true;
        Animation(false, false, false, true);
        return;
    }

    agent.isStopped = false;
    agent.speed = speed * 1.5f;
    agent.SetDestination(target.position);
    LookAtTarget(target.position, 10f);
    Animation(false, false, true, false);
}

UpdateChase 함수는 전투의 핵심이다.

가장 먼저 검사하는 조건은 distFromStart >= maxChaseDist - 0.1f이다.

이는 플레이어와의 거리보다 자기 활동 영역을 우선적으로 보존하는 설계다.

- 0.1f는 부동소수 오차와 경계값에서의 반복 전이(상태 떨림)를 방지하기 위한 마진이다.

NavMesh 이동과 float 연산 특성상 경계값에서 조건이 반복 충족/해제되는 현상을 방지하기 위해 소량의 여유를 둔다.

두 번째 조건은 dist > chaseDist로, 플레이어가 인지 범위를 벗어났을 경우 즉시 복귀한다.

이는 무한 추적을 방지하는 안전 장치다.

영역 초과(첫번째 조건)와 타겟 이탈은 모두 ReturnToStart를 호출하지만, 의미적 구분을 위해 조건을 분리하였다.

* 결과

추적 실패 시 빠르게 복귀

세 번째 조건은 공격 전환 조건이다.

플레이어가 공격 범위 안에 들어왔으면, agent.isStopped = true를 설정하여 이동을 완전히 멈춘다.

NavMeshAgent는 기본적으로 목표 지점을 계속 향해 움직이므로, 이동을 멈추지 않으면 공격 애니메이션 중에도 미세 이동이 발생해 위치가 밀리는 현상이 생길 수 있다.

추적 중에는 agent.speed = speed * 1.5f로 기본 이동보다 빠르게 설정해, 전투 상태임을 시각적으로 표현한다.

이는 순찰 속도와 체감 차이를 만들기 위한 설계다.

일반 추적 구간에서는 매 프레임 agent.SetDestination(target.position)을 호출한다.

이는 플레이어가 지속적으로 이동하기 때문이다.

목적지를 갱신하지 않으면 추적이 지연되거나 부자연스러워진다.

현재 프로젝트 규모에서는 매 프레임 갱신에 따른 성능 부담이 크지 않다고 판단하였다.

다만, 대규모 AI 환경에서는 일정 거리 변화 이상일 때만 목적지를 갱신하거나, 일정 주기 기반 갱신 방식으로 최적화할 수 있다.

LookAtTarget은 수평 회전만 수행하여 플레이어를 자연스럽게 바라보도록 하며, Quaternion.Slerp를 사용해 즉각 회전이 아닌 부드러운 회전 전환을 만든다.

4.2. ATTACK(공격) 상태
protected virtual void UpdateAttack(float dist, float distFromStart)
{
    if (distFromStart >= maxChaseDist - 0.1f || dist > chaseDist)
    {
        ReturnToStart();
        return;
    }

    if (dist > attackDist)
    {
        state = State.CHASE;
        return;
    }

    LookAtTarget(target.position, 15f);
}

ATTACK 상태는 이동을 수행하지 않는다.

CHASE에서 이미 agent.isStopped = true로 설정했기 때문에 NavMesh 이동은 정지된 상태다.

이 상태에서는 플레이어를 계속 바라보도록 LookAtTarget만 수행한다.

이는 공격 애니메이션 중 방향이 틀어지지 않도록 하기 위함이다.

첫 조건은 영역 초과 또는 인지 범위 초과 여부이다.

이 경우 공격보다 복귀가 우선이다.

이때, NavMesh 이동과 float 연산 오차로 인해 경계값에서 상태가 반복 전이되는 것을 방지하기 위해 소량의 마진을 두었다.

두 번째 조건은 공격 거리 이탈이다.

이 경우 다시 CHASE로 돌아가 재추적을 시작한다.

이 구조는 공격은 조건이 유지되는 동안만 유효하다는 것을 보장한다.

4.3. 공격 판정 구조
protected void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Player"))
        isAtk = true;
}

protected void OnTriggerStay(Collider other)
{
    if (other.CompareTag("Player"))
        isAtk = true;
}

protected void OnTriggerExit(Collider other)
{
    if (other.CompareTag("Player"))
        isAtk = false;
}

Trigger 기반 판정은 물리 충돌 이벤트다.

Collider가 isTrigger로 설정되어 있을 경우, 물리 충돌이 아닌 이벤트 방식으로 겹침 여부를 감지한다.

OnTriggerEnter는 최초 접촉 시 호출되고, OnTriggerStay는 겹쳐 있는 동안 매 물리 프레임마다 호출된다.

OnTriggerStay를 추가한 이유는, 빠른 이동이나 물리 갱신 타이밍 차이로 인해 Enter가 누락되는 상황을 방지하기 위함이다.

중요한 점은 Trigger는 데미지를 직접 주지 않는다.

isAtk는 단지 현재 공격 범위 안에 있는가를 나타내는 플래그일 뿐이다.

4.4. 실제 데미지 적용 (애니메이션 이벤트 기반)
public void PlayerDamage()
{
    if (isAtk)
    {
        StatusManager.instance.CurrentHp -= atk;
    }
}

PlayerDamage는 ATTACK 애니메이션의 특정 프레임에서만 호출된다.

즉, 데미지는 상태 전이와 무관하며, 애니메이션 이벤트에서만 발생한다.

ATTACK 상태가 되었다고 즉시 데미지가 들어가지 않는다.

공격 모션의 타격 프레임에서만 PlayerDamage가 실행된다.

또한 PlayerDamage는 ATTACK 애니메이션에서만 호출되므로, 상태를 중복 검사하지 않고 범위 플래그(isAtk)만 확인하도록 설계하였다.

상태와 판정을 분리하여 중복 조건 의존을 줄였다.

이 구조는 연출과 판정의 동기화를 보장한다.

플레이어는 맞은 순간과 데미지가 들어간 순간을 동일하게 체감한다.

4.5.  회전 보조 함수
protected void LookAtTarget(Vector3 targetPos, float rotSP)
{
    Vector3 dir = targetPos - transform.position;
    dir.y = 0;

    Quaternion rot = Quaternion.LookRotation(dir);
    transform.rotation = Quaternion.Slerp(
        transform.rotation,
        rot,
        Time.deltaTime * rotSP
    );
}

LookAtTarget은 플레이어를 바라보는 함수이다.

여기서, dir.y를 0으로 고정하여 상하 기울기를 제거하고 수평 회전만 수행하도록 제한했다.

부드러운 회전을 위해 Quaternion.Slerp를 사용하였다.

Quaternion.Slerp는 두 회전값 사이를 구면 선형 보간하는 함수다.

즉각적인 회전 대신 시간 기반 부드러운 회전을 만들 수 있다.

Slerp의 t 값은 0~1 사이가 적절하며, 여기서는 Time.deltaTime * rotSP를 사용해 시간 기반 회전 속도를 구현했다.

deltaTime을 곱지 않으면 프레임 수에 따라 회전 속도가 달라질 수 있다.

5. 개발 의도

전투 영역은 단순 추적 로직이 아니라, 영역 보존, 상태 전이 우선순위, 이동 제어, 애니메이션 동기화가 결합된 실행 계층이다.

CHASE는 이동 중심 상태이고, ATTACK은 이동을 멈추고 방향 유지에 집중하는 상태다.

상태별 책임을 명확히 분리하였다.

영역 초과 조건을 최상위 가드로 배치하여 전투가 월드 규칙을 침범하지 않도록 설계했다.

공격 판정은 거리 기반 즉시 데미지가 아니라 Trigger 기반 범위 감지와 애니메이션 이벤트 호출을 결합하여 연출과 판정의 정확한 일치를 확보했다.

또한 NavMesh 목적지를 매 프레임 갱신하여 동적 타겟 추적을 보장하되, 프로젝트 규모를 고려해 현재는 단순 구조를 유지하였다.

확장 시에는 갱신 최적화가 가능하다.

이 전투 구조는 순찰 시스템과 자연스럽게 연결되며, 전투 종료 시 RETURN을 통해 안정적으로 원점 복귀가 이루어진다.

결과적으로 이 설계는 행동 중심이 아닌, 상태 소비 기반 실행 구조 위에 안정적인 전투 루프를 구축한 것이다.