내일배움캠프 8주차 2일차 TIL - 도주 AI

백흰범·2024년 6월 5일
0

오늘 한 일

  • 팀프로젝트 진행하기 ( 토끼 만들기 )
  • AI 내비게이터를 좀 더 깊게 파보기

오늘은 AI 내비게이터를 활용한 도주 AI를 만들어볼 생각이다.


도주 AI

목표

  • 기본적으로는 플레이어로 부터 잘 멀어져야한다.
  • 모서리에 붙었을 경우에 대한 이동 처리도 생각해줘야한다.
  • 도주에 어느 정도 랜덤성을 부여해줘야한다.


기초적인 프레임워크

 // 필요한 컴포넌트
 protected Animator animator;
 protected NavMeshAgent agent;

 // 플레이어 거리 구하기
 protected float playerDistance;

 // 상태
 protected AIState aiState;

 // SO
 public AnimalSO statSO;

 void Awake()
 {
     agent = GetComponent<NavMeshAgent>();
     animator = GetComponent<Animator>();
 }

 void Start()
 {
     ChangeState(AIState.Idle);
 }

 protected virtual void Update()
 {
     if (aiState == AIState.Dead) { return; }
   
     // 플레이어 거리 감지
     playerDistance = (transform.position - CharacterManager.Instance.Player.transform.position).sqrMagnitude; // sqr을 활용했으니 제곱을 하는 걸 잊지 맙시다.

     animator.SetBool("Moving", aiState != AIState.Idle);

     switch (aiState)
     {
         case AIState.Idle:
             // IdleState();
             break;
         case AIState.Flee:
             FleeState();
             break;
     }

     animator.speed = agent.speed / statSO.walkSpeed;
 }

 // 상태 변환
 protected virtual void ChangeState(AIState state)
 {
     aiState = state;

     switch (aiState)
     {
         case AIState.Idle:
             agent.isStopped = true;
             agent.speed = statSO.walkSpeed;
             break;
         case AIState.Flee:
             agent.isStopped = false;
             agent.speed = statSO.runSpeed;
             break;
         case AIState.Dead: // 초기화 작업
             agent.isStopped = true;
             agent.speed = 0f;
             animator.speed = 1f;
             agent.ResetPath();
             animator.SetBool("Dead", true);
             _collider.isTrigger = true; // 통과 시키게 하기 위해서
             Invoke("Destroy", 30f);
             break;
     }
 }

 ... IdleState 부분 생략...

 // 도주 상태 시
 void FleeState()
 {
     if (agent.remainingDistance < 1f)
     {
         agent.SetDestination(NewFleePoint());
     }
     else
     {
         ChangeState(AIState.Idle);
     }
 }

// 도주 목적지 설정
 Vector3 NewFleePoint()
 {
목적지 설정 코드
 }

// 술래잡기 테스트용
 private void OnCollisionEnter(Collision collision)
 {
     if (collision.gameObject.CompareTag("Player"))
     {
         if (aiState == AIState.Dead) { return; }
         ChangeState(AIState.Dead);
     }
 }

 protected virtual void Destroy()
 {
     Destroy(this.gameObject);
 }
  • 스파르타 코딩클럽에서 제공된코드를 살짝 고쳐줬다.


구현하는 과정

반대 방향으로만 도망

코드

    void FleeState()
    {
        if (agent.remainingDistance < 1f)
        {
            agent.SetDestination(NewFleePoint());
        }
        else
        {
            ChangeState(AIState.Wandering);
        }
    }

    Vector3 NewFleePoint()
    {
        Vector3 dir = transform.position - CharacterManager.Instance.Player.transform.position.normalized;
        return transform.position + dir;
    }

  • 모서리를 만나면 너무 약하다.

랜덤으로 돌리지만 특정 부분만 거르기

코드

    void FleeState()
    {
        if (agent.remainingDistance < 1f)
        {
            agent.SetDestination(NewFleePoint());
        }
        else
        {
            ChangeState(AIState.Wandering);
        }
    }

    Vector3 NewFleePoint()
    {
        NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * statSO.safeDistance), out NavMeshHit hit, statSO.safeDistance, NavMesh.AllAreas);
        int i = 0;
        while (GetDestinationAngle(hit.position) > 90 || playerDistance < statSO.safeDistance)
        {
            NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * statSO.safeDistance), out hit, statSO.safeDistance, NavMesh.AllAreas);
            i++;
            if (i == 30)
                break;
        }
        return hit.position;
    }

    float GetDestinationAngle(Vector3 targetPos)
    {
        return Vector3.Angle(transform.position - CharacterManager.Instance.Player.transform.position, transform.position + targetPos);
    }

  • 전보다 낫지만 뭔가 보충이 좀 더 필요해보인다.
    플레이어에게 꼴아박는 경우가 있다;

최종 개선

  • FleeState에 조건을 추가하여 좀 더 능동적인 도주 패턴을 덧붙여줬다.

코드

    void FleeState()
   {
       float destinationDistance = (agent.destination - CharacterManager.Instance.Player.transform.position).sqrMagnitude;
// 조건에다가 목적지가 플레이랑 가깝지 않은 지 그리고 당장 플레이어와 너무 가깝지 않은지에 따른 조건을 추가해뒀다.
       if (agent.remainingDistance < 1f || destinationDistance < statSO.safeDistance * statSO.safeDistance || playerDistance < 1f)
       {
           agent.SetDestination(NewFleePoint());
       }
       else
       {
           ChangeState(AIState.Wandering);
       }
   }
... 아랫 부분은 랜덤 방향 도주와 동일하다.


개선 후 결과

  • 빨간색이 랜덤 도주, 파란색이 추가적인 조건을 덧붙여준 토끼다
  • 체감상 느끼기로는 그냥 뭉쳐있으면 도주 능력이 어느 정도 약해질 수 밖에 없는 것 같다.
  • 실제 적용으로는 다를 수도 있으니, 되도록이면 코드는 간결하게 만들수록 좋을 것이다.


오늘 알게된 코드

Vector

.sqrMagnitude

  • 루트를 씌우지 않은 벡터의 스칼라값으로 기존 magnitude보다 연산은 빠르지만 조건 덧붙여줄 때에는 조건을 제곱해줘야한다.

.FindClosetEdge(out NavMeshHit edgeHit)

  • 현재 위치에서 가장 가까운 NavMesh의 바깥 테두리 위치를 가져온다.



느낀점

확실히 한 사람이 여러 역할을 분담받는 것보다는 파트를 나눠 한 사람씩 역할을 맡으니까. 좀 더 세부적인 사항을 신경 쓸 수 있어서 좋은 것 같다.

profile
게임 개발 꿈나무

0개의 댓글