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

[유니티 활용] StateMachine 과 몬스터 AI

by 레오란다 2022. 12. 26.
반응형

이번 글에서는 주변을 배회하다가 캐릭터를 발견하면 캐릭터를 따라다니고 멀어지면 다시 주변을 배회하는 몬스터의 AI 를 간단한 StateMachine 을 이용해 구현하는 방법에 대해 알아보도록 하겠습니다. 이 글은 이전에 진행한 다음의 글에 이어서 작성하도록 하겠습니다.

 

 

[유니티 활용] 부드러운 카메라 이동과 경계 값 설정

이번 글에서는 지정한 경계 값 안에서 대상 객체를 따라 이동하는 카메라 구현 방법에 대해 알아보도록 하겠습니다. 이 글은 이전에 작성한 다음 글의 연장선상에서 진행하도록 하겠습니다. 기

ugames.tistory.com

 

▶ 에셋 정보

위의 링크 글에 있긴 하지만 귀찮으신 분들을 위해 여기를 클릭하시고 다운로드 받으시면 됩니다.

 

 

▶ 몬스터 만들기

몬스터는 위의 에셋에 있는 몬스터 Dog를 사용하겠습니다.

  • Project 창에서 경로 [Assets > SunnyLand Artwork > Sprites > Enemies > Dog] 로 이동합니다.
  • dog1 ~ 4 의 Pixels Per Unit 을 33 으로 설정합니다. (파일 4개를 모두 선택하고 Inspector 창에서 한 번에 변경할 수 있습니다.)
  • dog1 을 Hierarchy 창으로 드래그하고 이름을 Monster-Dog 로 변경합니다.
  • Monster-Dog를 선택하고 Inspector 창에서 [Add Component > Miscellaneous] 에서 Animator 를 추가합니다.
  • Animator의 Controller 필드에 [Assets > SunnyLand Artwork > Sprites > Enemies > Dog] 에 있는 dog1 controller 를 연결해 줍니다.

Animator 에 Controller 연결
dog1 animator controller 연결

  • [Add Component > Physics2D] 에서 Rigidbody2D 와 BoxCollider2D 를 추가합니다. 
  • Player를 감지하기 위해 Sensor를 만듭니다.
    • Monster-Dog 객체에 빈 자식 객체를 만들고 이름을 Sensor 로 변경합니다. 이 객체에 [Add Component > Physics2D] 에 있는 CircleCollider2D 를 추가합니다.
    • CircleCollider2D의 Is Trigger 필드를 체크하고 Radius 필드를 3으로 설정합니다. 
  • 이름을 Monster 로 해서 새 스크립트를 추가합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Monster : MonoBehaviour
{
   int HP = 1;

   Rigidbody2D rig;
   SpriteRenderer sr;

   float move = -1;

   enum State
   {
      Idle,
      MoveLeft,
      MoveRight,      
      Chase
   }

   State state;
   Player player;

   bool cancelWait;

   // Start is called before the first frame update
   IEnumerator Start()
   {
      rig = GetComponent<Rigidbody2D>();
      sr = GetComponent<SpriteRenderer>();
      cancelWait = false;

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

   void StateChange(State next)
   {
      state = next;
      cancelWait = true;
   }

   void SetNextRoaming()
   {
      // 현재의 state 가 Idle, MoveLeft, MoveRight 이면 다음 로밍 설정
      if (state == State.Idle || state == State.MoveLeft || state == State.MoveRight)
      {
         state = (State)Random.Range(0, 3);
      }
   }

   IEnumerator CancelableWait(float t)
   {
      var d = Time.deltaTime;
      cancelWait = false;

      // 지정한 시간 t 만큼 대기
      // cancelWait == true 면 중단
      while (d < t && cancelWait == false)
      {
         d += Time.deltaTime;
         yield return null;
      }

      if (cancelWait == true) print("Cancled");
   }

   IEnumerator Idle()
   {
      // 움직이지 않음
      move = 0;
      // 1~2 초 대기
      yield return StartCoroutine(CancelableWait((Random.Range(1f, 3f))));

      SetNextRoaming();
   }

   IEnumerator MoveLeft()
   {      
      move = -1;
      sr.flipX = false;      
      yield return StartCoroutine(CancelableWait((Random.Range(3f, 6f))));

      SetNextRoaming();
   }

   IEnumerator MoveRight()
   {
      move = 1;
      sr.flipX = true;
      yield return StartCoroutine(CancelableWait((Random.Range(3f, 6f))));

      SetNextRoaming();
   }

   IEnumerator Chase()
   {
      if (player != null)
      {
         Vector3 vec;

         do
         {
            yield return new WaitForSeconds(0.5f);

            // Player와의 방향 벡터
            vec = player.transform.position - transform.position;

            // Player 가 오른쪽에 있으면
            if (vec.x > 0)
            {
               sr.flipX = true;
               move = 2f;
            }
            // Player 가 왼쪽에 있으면
            else
            {
               sr.flipX = false;
               move = -2f;
            }
         }
         // 거리가 6 이내인 동안 따라 다니기
         while (vec.magnitude < 6f); 

         print("End of chase");

         // Player 가 멀어지면 State 를 Idle 로 변경
         StateChange(State.Idle);
      }
   }

   // Update is called once per frame
   void FixedUpdate()
   {
      // move 값에 따라 x 축으로 이동
      rig.velocity = new Vector2(move, rig.velocity.y);
   }

   // Sensor 객체의 CircleCollider2D와 충돌시 발생하는 이벤트
   private void OnTriggerEnter2D(Collider2D collision)
   {           
      // 충돌한 객체의 이름이 Player 가 아니면 return
      if (collision.gameObject.name != "Player") return;
      player = collision.gameObject.GetComponent<Player>();
      if (state != State.Chase) StateChange(State.Chase);
   }
}
  • StateMachine 의 구현은 유니티에서 제공하는 Coroutine 을 이용했습니다.
  • IEnumerator Start() 로 선언하면 Start 함수가 코루틴 함수로 동작합니다.
  • Idle, MoveLeft, MoveRight 는 로밍용 state 입니다.
  • Chase 중에 Player 객체와의 거리가 6 보다 멀어지면 Idle state 가 되고 이 후 근처를 로밍합니다.

실행화면

 

 

반응형

댓글