본문 바로가기
게임 프로그래밍/유니티 활용

[유니티 활용] StateMachine 을 활용한 3D 몬스터 AI 구현

by 레오란다 2023. 1. 12.
반응형

아래에 링크한 이전 글에서는 유니티에서 제공하는 NavMeshAgent 를 이용해 몬스터가 플레이어를 따라다니도록 했습니다.

 

이번 글에서는 한단계 더 나아가서 처음엔 가만히 있다가 플레이어가 근처에 오면 따라가고 공격 범위안에 들어오면 공격을 하고 다시 멀어지면 다시 따라가다가 더 멀어지면 추적을 포기하고 그 자리에 가만히 있는 몬스터의 AI 를 만들어 보도록 하겠습니다.

 

[유니티 활용] NavMeshAgent 사용법 (Simple Monster AI)

NavMeshAgent 는 유니티에서 제공하는 네비게이션 시스템입니다. 이 기능을 이용해 몬스터 객체가 플레이어 캐릭터를 자동으로 따라다니 게 하는 방법에 대해 알아보도록 하겠습니다. 플레이어 캐

ugames.tistory.com

* 이전 포스팅 하단에서 현 시점까지의 작업 내용이 포함된 unitypackage 도 다운로드 받으실 수 있습니다.


 

State Machine

State Machine을 만드는 방법에는 여러 가지가 있지만 이 글에서는 Coroutine 을 이용해 만들어 보도록 하겠습니다. State 는 너무 복잡하지 않게 [IDLE / CHASE / ATTACK / KILLED] 이렇게 4 가지 상태를 이용하겠습니다. 

 

  IDLE   아무것도 하지 않고 대기
  CHASE   플레이어 캐릭터를 따라다님
  ATTACK   플레이어 캐릭터를 공격
  KILLED   사망

 

아래는 State Machine의 다이어그램입니다.

 

StateMachine Diagram
StateMachine Diagram


 

Monster Animator 만들기

이 예제는 Monster 스크립트에서 StateMachine 에 따른 animation 을 직접 제어하기 때문에 Animator 설정은 간단하게 됩니다.

 

Project 창에서 Asset 폴더를 선택하고 마우스 우클릭 [Create > Animator Controller] 를 선택해 새로운 Animator Controller를 생성한 다음 이름을 Monster-Slime 으로 변경합니다. Monster-Slime 을 더블클릭해서 Animator 창을 열어 줍니다.

 

Slime의 애니메이션을 animator 에 등록하기 위해 Project 창에서 경로 [Assets > RPG Monster Duo PBR Polyart > Animations > Slime] 으로 이동합니다. 이곳에 Slime 과 관련된 animation 들이 있습니다.

 

이 중 "IdleNormal_Slime_Anim" 을 드래그해서 Animator 창에 놓으면 다음과 같이 같은 이름의 state 가 만들어집니다. 오른쪽 Inspector 창의 붉은색 테두리 위치에서 이름을 아래와 같이 변경해 줍니다.

animation 등록
Animator 에 animation state 등록

 

이와 같은 방법으로 "WalkFWD_Slime_Anim", "Attack01_Slime_Anim", "IdleBattle_Slime_Anim" 을 등록하고 아래와 같아 지도록 이름을 변경합니다.

 

완성된 Animator 설정
완성된 Animator 화면

Attack01 을 선택하고 마우스 우클릭 [Make Transition]을 선택한 다음 IdleBattle 을 선택해 transition을 만드는 것도 잊으시면 안 됩니다.


 

Slime 에 Sphere Collider 추가

Slime 객체를 선택하고 Inspector 창에서 [Add Component > Physics > Sphere Collider] 를 선택해 추가하고 다음과 같이 설정해 줍니다. 

 

Sphere Collider 설정
Sphere Collider

 

이렇게 하면 몬스터 주변에 반지름 5의 크기를 갖는 구형태의 Collider 가 생성됩니다. 이 Collider 는 플레이어 캐릭터가 근처에 왔는지 감지하기 위한 목적으로 사용됩니다. 몬스터가 플레이어를 감지할 수 있는 범위는 Radius의 크기를 조절하시면 됩니다.


 

Monster Script 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Monster : MonoBehaviour
{
   Transform target;   
   NavMeshAgent nmAgent;
   Animator anim;

   float HP = 0;
   public float lostDistance;

   enum State
   {
      IDLE,
      CHASE,
      ATTACK,
      KILLED
   }

   State state;

   // Start is called before the first frame update
   void Start()
   {
      anim = GetComponent<Animator>();
      nmAgent = GetComponent<NavMeshAgent>();

      HP = 10;
      state = State.IDLE;
      StartCoroutine(StateMachine());
   }

   IEnumerator StateMachine()
   {      
      while (HP > 0)
      {
         yield return StartCoroutine(state.ToString());
      }
   }

   IEnumerator IDLE()
   {
      // 현재 animator 상태정보 얻기
      var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);

      // 애니메이션 이름이 IdleNormal 이 아니면 Play
      if (curAnimStateInfo.IsName("IdleNormal") == false)
         anim.Play("IdleNormal", 0, 0);

      // 몬스터가 Idle 상태일 때 두리번 거리게 하는 코드
      // 50% 확률로 좌/우로 돌아 보기
      int dir = Random.Range(0f, 1f) > 0.5f ? 1 : -1;

      // 회전 속도 설정
      float lookSpeed = Random.Range(25f, 40f);

      // IdleNormal 재생 시간 동안 돌아보기
      for (float i=0; i< curAnimStateInfo.length; i+=Time.deltaTime)
      {
         transform.localEulerAngles = new Vector3(0f, transform.localEulerAngles.y + (dir) * Time.deltaTime * lookSpeed, 0f);

         yield return null;
      }
   }

   IEnumerator CHASE()
   {
      var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);

      if (curAnimStateInfo.IsName("WalkFWD") == false)
      {
         anim.Play("WalkFWD", 0, 0);
         // SetDestination 을 위해 한 frame을 넘기기위한 코드
         yield return null;
      }

      // 목표까지의 남은 거리가 멈추는 지점보다 작거나 같으면
      if (nmAgent.remainingDistance <= nmAgent.stoppingDistance)
      {
         // StateMachine 을 공격으로 변경
         ChangeState(State.ATTACK);
      }
      // 목표와의 거리가 멀어진 경우
      else if (nmAgent.remainingDistance > lostDistance)
      {
         target = null;         
         nmAgent.SetDestination(transform.position);
         yield return null;
         // StateMachine 을 대기로 변경
         ChangeState(State.IDLE);
      }
      else
      {  
         // WalkFWD 애니메이션의 한 사이클 동안 대기
         yield return new WaitForSeconds(curAnimStateInfo.length);
      }
   }

   IEnumerator ATTACK()
   {
      var curAnimStateInfo = anim.GetCurrentAnimatorStateInfo(0);
      
      // 공격 애니메이션은 공격 후 Idle Battle 로 이동하기 때문에 
      // 코드가 이 지점에 오면 무조건 Attack01 을 Play
      anim.Play("Attack01", 0, 0);

      // 거리가 멀어지면
      if (nmAgent.remainingDistance > nmAgent.stoppingDistance)
      {
         // StateMachine을 추적으로 변경
         ChangeState(State.CHASE);
      }
      else
         // 공격 animation 의 두 배만큼 대기
         // 이 대기 시간을 이용해 공격 간격을 조절할 수 있음.
         yield return new WaitForSeconds(curAnimStateInfo.length * 2f);
   }

   IEnumerator KILLED()
   {
      yield return null;
   }

   void ChangeState(State newState)
   {
      state = newState;
   }

   private void OnTriggerEnter(Collider other)
   {      
      if (other.name != "Player") return;
      // Sphere Collider 가 Player 를 감지하면      
      target = other.transform;
      // NavMeshAgent의 목표를 Player 로 설정
      nmAgent.SetDestination(target.position);
      // StateMachine을 추적으로 변경
      ChangeState(State.CHASE);
   }

   // Update is called once per frame
   void Update()
   {
      if (target == null) return;
      // target 이 null 이 아니면 target 을 계속 추적
      nmAgent.SetDestination(target.position);
   }
}

 

 

코드의 길이가 조금 길긴하지만 요약하면 다음과 같습니다.

 

  1. enum 을 이용해 state 정의
  2. Coroutine 을 이용해 StateMachine 가동
  3. 각 state의 이름을 이용해 해당 코루틴 함수를 호출 (state.ToString())
  4. 각 state의 코루틴 함수에서 state 에 맞는 행동을 할 수 있는 코드 작성

반응형

실행

실행 화면
실행 화면

실제 플레이 화면입니다. 글 초반에 있는 State Machine의 다이어그램과 같이 몬스터가 동작하고 있습니다. 이제 여기에 플레이어가 공격을 당했을 때 모션을 추가하고 플레이어도 몬스터를 공격할 수 있는 코드를 추가하면 간단한 전투 시스템이 만들어지게 됩니다. 

 

지금까지 작업한 내용이 포함된 패키지입니다.

 

StateMachineMonsterAI.unitypackage
0.02MB

※ 프로젝트에서 사용한 무료 에셋을 먼저 다운로드 -> Import 한 다음 위의 패키지를 import 하셔야 합니다.


 

 무료 에셋 목록

플레이어 캐릭터

 

RPG Tiny Hero Duo PBR Polyart | 3D 휴머노이드 | Unity Asset Store

Elevate your workflow with the RPG Tiny Hero Duo PBR Polyart asset from Dungeon Mason. Find this & other 휴머노이드 options on the Unity Asset Store.

assetstore.unity.com

 

몬스터

 

RPG Monster Duo PBR Polyart | 3D 생물 | Unity Asset Store

Elevate your workflow with the RPG Monster Duo PBR Polyart asset from Dungeon Mason. Find this & other 생물 options on the Unity Asset Store.

assetstore.unity.com

 

발소리

 

Footsteps - Essentials | 기타 효과음 효과음 | Unity Asset Store

Layer in the sounds of Footsteps - Essentials from Nox_Sound for your next project. Browse all audio options on the Unity Asset Store.

assetstore.unity.com

 

 

반응형

댓글