본문 바로가기

Unity

[Unity3D]FSM을 활용한 AI개발(6)

1. 서론

상태 값들을 다 다루고 싶지만, 네비 메쉬를 사용하는 Walk부분 만 다루고 이번 ~만들기는 끝을 내도록 하려고 합니다.

이 정도면 유니티를 이용해 FSM을 제작 할 수 있겠다 생각이 들었습니다.

댓글로 궁금한점 물어봐 주시면 제가 아는 한 최대한 답 드리겠다고 약속드리며, 시작해 보도록 하겠습니다.

2. 본론

그림1. Nav Mesh Agent

Walk 상태에 들어가려면, 오브젝트는 어떻게 움직여야 좋을까. 장애물도 피하고, 해당 목적지(타깃)까지 이동을 해야 할 필요가 있다. 그중 가장 간편한 것은 Nav Mesh를 이용하는 것이다. Transform.Translate을 활용하는 법도 있겠지만, 장애물을 피하는 알고리즘이 따로 필요할 것이기에 간편한 NavMesh를 활용해 보도록 하겠다.

 

그림2. Static 체크
그림3. Navgation 설정

그림 2, 그림 3을 참고해서 베이크를 걸어주면 된다. 자세한 설명은 유니티 레퍼런스를 참고하면 좋을 듯하다.

유니티 버전마다 다르니 꼭 레퍼런스를 확인하는 습관을 기르도록 하자. 다시 한번 언급 하지만 귀찮아서 그런 것이 아니다.

https://docs.unity3d.com/kr/current/Manual/Navigation.html

 

내비게이션과 경로 탐색 - Unity 매뉴얼

내비게이션 시스템은 씬 지오메트리에서 자동으로 생성되는 내비게이션 메시를 사용하여 게임 월드에서 지능을 갖고 움직일 수 있는 캐릭터를 생성하는 데 도움을 줍니다. 역동적인 장애물은 런타임 시점에 캐릭터의 내비게이션을 바꾸도록 하며 오프 메시 링크는 문을 열거나 절벽 같은 지형에서 뛰어내릴 수 있도록 특정한 액션을 빌드합니다. 이 섹션은 Unity의 내비게이션과 경로 탐색 시스템을 자세하게 설명합니다.

docs.unity3d.com

설정해주면 파란색으로 경로 표시가 된다. 이제 네비 메쉬 에이전트가 활용될 구간이 설정된 것이다.

//Zombie_Walk.cs
using Global;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

namespace MyFSM
{
    public class Zombie_Walk : FSM<ZombieFSM>
    {
    	//주인 변수
        private ZombieFSM m_Owner;

		//네비 메시 에이전트
        private NavMeshAgent agent;

		//애니메이터
        private Animator m_Animator;

		//생성자
        public Zombie_Walk(ZombieFSM _owner)
        {
            m_Owner = _owner;
        }

		//시작
        public override void Begin()
        {
            Debug.Log("Walk Begin");
            agent = m_Owner.GetComponent<NavMeshAgent>();
            agent.isStopped = false;
            m_Animator = m_Owner.m_Animator;

            m_Owner.m_eCurState = ZOMBIE_STATE.Walk;
            m_Animator.SetBool("Walk", true);
        }

		//동작
        public override void Run()
        {
            if (m_Owner.m_TransTarget == null)
            {
               
                m_Owner.ChangeFSM(ZOMBIE_STATE.Idle);
            }
            

            if (m_Owner.m_TransTarget != null)
                GotoTarget();

            if (m_Owner.m_TransTarget != null && m_Owner.m_fAttackRange >= Vector3.Distance(m_Owner.transform.position, m_Owner.m_TransTarget.position))
            {
                m_Owner.ChangeFSM(ZOMBIE_STATE.Attack);
            }

        }

		//종료
        public override void Exit()
        {
            Debug.Log("Walk Exit");
            agent.isStopped = true;
            m_Owner.m_ePrevState = ZOMBIE_STATE.Walk;
            m_Animator.SetBool("Walk", false);
        }

		//공격범위찾기
        private bool FindRange()
        {
            Collider[] hitColliders = Physics.OverlapSphere(m_Owner.transform.position, m_Owner.m_fFindRange, (1 << LayerMask.NameToLayer("Civilian")) | (1 << LayerMask.NameToLayer("Soldier")));

            if (hitColliders.Length != 0)
            {
                for (int i = 0; i < hitColliders.Length; ++i)
                {
                    if (hitColliders[i].gameObject.layer == LayerMask.NameToLayer("Soldier"))
                    {
                        m_Owner.m_TransTarget = hitColliders[i].transform;
                    }
                }

                if (m_Owner.m_TransTarget == null)
                    m_Owner.m_TransTarget = hitColliders[0].transform;

                return true;
            }
            return false;
        }

		//이동
        private void GotoTarget()
        {
            ZombieInfo info = new ZombieInfo
            {
                _Owner = m_Owner.transform
            };

            agent.SetDestination(m_Owner.m_TransTarget.position);

            if(m_Owner.m_TransTarget.gameObject.layer == LayerMask.NameToLayer("Civilian"))
                m_Owner.m_TransTarget.gameObject.SendMessage("ZombieFollow", info, SendMessageOptions.DontRequireReceiver);
        }
    }

}

 이제 코드를 살펴보도록 하자.

우리가 눈여겨봐야 할 점은, Begin() 함수 안에서의 초기화 부분이다.

Agent변수에 GetComponent <NavMeshAgent>()를 하는데 이 부분은 Zombie_FSM부분에 있어야 하는 것 아니냐!

사실 그렇게 해도 별 문제는 없으나, 필자는 Walk상태가 필요한 좀비가 있고, 필요하지 않은 좀비가 있을 거라 두고 Walk상태에서 설정하도록 하였다. 이는 만드는 사람의 마음이니 편한 대로 작업하길 바란다.

 

GoToTarget() 함수 내에 SetDestination 메서드를 주목하길 바란다. 좀비가 찾은 타깃의 포지션을 넣어주면

그리로 NavMeshAgent컴포넌트 Speed값으로 도달하게 되는 것이다. 길도 알아서 찾고 속도도 알아서 가니 매우 간편하다. 이동하게 되면 시민에게 SendMessage 하는 부분이 있는데 이 SendMessage 설명은 유니티 레퍼런스를 참고하길 바란다.

https://docs.unity3d.com/kr/530/ScriptReference/GameObject.SendMessage.html

 

Unity - 스크립팅 API: GameObject.SendMessage

파라미터를 받지 않음으로써, 수신 메서드는 인자를 무시하는것을 선택할수 있습니다. If options is set to SendMessageOptions.RequireReceiver an error is printed if the message is not picked up by any component. Note that messages will not be sent to inactive objects (ie, those that have been de

docs.unity3d.com

SendMessage의 경우엔 그리 효율이 좋지 않다고 하니, 상황에 맞게 써주면 좋을 것 같다. 타깃의 메서드를 호출하게 되는데 타깃에 그에 맞는 동작을 써주면 된다.

 

 

Run() 함수 안 구문을 더 살펴보자면, 타깃이 없다면, Idle 상태로 값을 바꿔주게 되며, 타겟이 있다면 그 타깃으로 SetDestination 하게 된다. 그리고 좀비와 타깃의 거리를 Vector3.Distance로 계산하여 일정 거리에 도달했다면, Attack상태로 변경되는 것이다.

 

이렇게 한다면 좀비는 Idle상태에서 타깃을 찾고 찾았다면, 다가가서 공격하고 타겟이 죽거나 없어지면, 다시 Idle상태로 돌아가 타겟을 찾고 다가가서 공격하고 를 반복하게 되는 것이다.

 

3. 결론

간단하게 AI를 FSM방식으로 짜 보았다.

부족한 부분도 많고 설명이 미흡한 부분도 많을 텐데, 이 점은 댓글로 남겨주면 수정이나 답변을 하는 식으로 하겠다.

 

다음 글은 코드 전체를 올려놓도록 하겠다. 참고하여 본인 프로젝트에 맞게 수정해서 쓰면 좋겠다.

 

문은 댓글로 적어주시면 최대한 빠르게 답글 달아 드리겠습니다.