메카님 애니메이션 제어,내비게이션-길찾기, 유한상태머신(FSM)
2023. 8. 23.

 

 

-메카님(Mecanim)을 사용해 애니메이션 제어

변경 전/후 몬스터 모델

-애니메이션 타입에 Legacy, Generic, Humanoid가 있다.

몬스터가 2족 보행을 하므로 Humanoid로 변경한다.

-Configure 버튼을 누르면 본 구조의 매핑 정보를 확인할 수 있는 씬 뷰가 Avatar Configuration 화면으로 바뀐다.

avatar configuration 화면/ 아바타 에셋(MonsterAvatar)

Avatar 에셋은 본 구조의 매핑 정보를 저장한다.

본 구조가 동일하다면 다른 모델에서 사용한 아바타 에셋을 재사용할 수 있다.

-> 필수 본만 같다면 다른 애니메이션 클립 연결 가능

-Muscles & Settings 탭은 각 관절의 정상적인 동작 여부를 시각적으로 확인할 수 있다.

-Pre-Muscle Settings에서 관절의 회전 범위를 설정한다.

mixamo-upload character

mixamo에서 upload character후 애니메이션이 되는지 확인해 보았다.

다운로드 후 애니메이션 폴더에 추가
Humanoid로 변경해주어야함

-애니메이션 클립을 선택하고 모델을 드래그앤 드롭해주면 미리볼 수 있다.

-새 애니메이션 컨트롤러 생성, Idle 애니메이션 클립 추가: 처음에 추가하면 주황색으로 된다.(기본값)

=> 다른 애니메이션 클립을 기본값으로 변경하려면 Set as Layer Default State해주어야함

-몬스터에 MonsterController 할당

-walk 애니메이션 추가하고 transition 설정. IsTrace를 bool타입으로 추가.

 

유니티에서 제공하는 내비게이션의 구현 방식은 스테이지를 구성하고있는 3D 메시를 분석해 내비메시 데이터를 미리 생성하는 것이다.

=> 추적할 수 있는 영역(Walkable Area)과

     장애물이라 지나갈 수 없는 영역(Non Walkable area)의 데이터를 메시로 미리 생성한다.

 

AI Navigation  패키지 설치

 

NavMeshSurface

새로운 내비게이션은 navigation static 플래그 설정이 필요없고, NavMeshSurface 컴포넌트를 추가하면 된다.

floor에 navMeshSurface 추가후 Bake하면 바닥에 닿아있는 부분은 뚫린다.

-파란색 메시로 채워지지 않고 구멍이 나 있는 영역은 지나갈 수 없는 영역으로 인식해 장애물로 판단한다.

-use Geometry 속성을 Physics Colliders로 설정하면 콜라이더 컴포넌트가 포함된 게임오브젝트만 내미메시를 생성한다.

-플레이어는 장애물로 인식하지 않게 하기위해 Y축으로 올려 Floor에서 분리시킨뒤 다시 베이크하고 내렸다.

-비활성화하고 해도 된다.

-Object Collection/Include Layers 속성에서 별도의 layer설정으로 장애물로 인식하지 말아야할 객체를 제외시킬 수 있다.

 

NavMeshAgent

-NavMeshAgent 컴포넌트는 내미메시 데이터를 기반으로 목적지까지 최단거리를 계산해 이동하게하며, 장애물과 다른 NPC간의 충돌을 회피하는 기능도 제공한다.

=>A Pthfinding 알고리즘을 사용한다.

 

-agent Type 속성을 수정할수 있다.

 

destionation 속성으로 이동할 좌푯값을 지정하는 방법

agent.destination = playerTr.positions;

agent.SetDestination(playerTr.position);

-둘중 한가지를 쓰면 된다.

* Find ~ 계열의 함수는 처리 속도가 느리므로 update말고 Awake,Start 함수에서 변수에 할당한 후 사용하자.

-base offset -0.1로 설정

 

유한상태 머신(FSM: Finite State Machine)

-캐릭터가 알아서 스스로 주변 환경에 적응하거나 반응에 적절하게 반작용하도록 구현한 것

=>피격을 당해 일정 데미지가 누적되면 사망하고 소멸하는 구조이기 때문에 상태가 유한해서 "유한 상태 머신"이라 함.

-FSM은 자신의 상태 값을 갖고 있어야 하며, 현재 어떤 상태인지 갱신하고, 해당 상태에 맞는 행동을 취한다.

 

적 캐릭터의 상태를 순찰, 추적, 공격, 사망으로 정의하자.

행동시나리오: 처음 생성 시 불규칙적으로 순찰하다 Player가 근접하면 추적을 시작, 공격 사정거리 이내일 때 공격

=> 몬스터와 Player의 거리를 측정, 몬스터의 상태를 주기적 업데이트, 상태에 따른 동작을 취하게 한다.

-Update 함수에서 처리하면 오버헤드를 줄 수 있으므로 코루틴을 활용해본다.

(굳이 매프레임마다 상태를 확인하지는 않아도 되기 때문이다.)

 

적 캐릭터의 상태 체크

- 0.3초마다 상태를 체크해 갱신한다.

-Player와 자신의 Distance를 측정해 추적 사정거리와 공격 사정거리 내에 들어왔는지 판단 => 상태변경

몬스터의 상태: 멈춤, 추적, 공격, 죽음

Distance에 따라 상태 변화

적 캐릭터의 행동 구현

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

public class MonsterController : MonoBehaviour
{
    
    public enum eState
    {//몬스터 상태 정의
        IDLE, TRACE, ATTACK, DIE
    }
    public eState state = eState.IDLE;//몬스터의 현재 상태
    private bool isDie;//몬스터의 사망 여부
    private NavMeshAgent agent;
    private Transform playerTrans;
    private Animator anim;
    [SerializeField]private float traceDistance = 10.0f;//추적 사거리
    [SerializeField]private float attackDistance = 2.0f;//공격 사거리
    // Start is called before the first frame update
    void Start()
    {
        this.anim = GetComponent<Animator>();
        this.agent = this.GetComponent<NavMeshAgent>();//agent 컴포넌트 가져옴
        GameObject playerGo = GameObject.FindGameObjectWithTag("Player");
        this.playerTrans = playerGo.GetComponent<Transform>();
       // Vector3 playerPosition = playerTrans.position;
       // this.agent.destination = playerPosition;//destination 속성을 사용해 이동할 좌푯값 지정

        StartCoroutine(this.CheckState());//상태 갱신
        StartCoroutine(this.MonsterAction());//상태에따라 몬스터 행동 수행
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private IEnumerator CheckState()
    {//상태 갱신 코루틴 함수: 일정 간격으로 몬스터의 행동상태 체크
        while (!isDie)
        {
            yield return new WaitForSeconds(0.3f);
            float distance = Vector3.Distance(this.transform.position, this.playerTrans.position);
            Debug.Log(distance);
           
         
            if (distance <= this.traceDistance)//추적 사거리에 들어왔다면
            {
                this.state = eState.TRACE;//추적상태로 변경
                if (distance <= this.attackDistance) //공격 사거리에 들어왔다면
                {
                    this.state = eState.ATTACK;//공격상태로 변경
                }
            }           
            else//멈춤
            {
                this.state = eState.IDLE;
            }
        }
        
    }

    private IEnumerator MonsterAction()
    {
        while (!isDie)
        {
            switch (state)
            {
                case eState.IDLE:
                    this.agent.isStopped = true;//추적 중지
                    anim.SetBool("IsTrace", false);
                    break;

                case eState.TRACE:
                    this.agent.SetDestination(this.playerTrans.position);
                    this.agent.isStopped = false;
                    anim.SetBool("IsTrace", true);
                    break;

                case eState.ATTACK:
                    break;

                case eState.DIE:
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnDrawGizmos()
    {
        if(this.state == eState.TRACE)
        {
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(this.transform.position, traceDistance);
        }
        else if(this.state == eState.ATTACK)
        {
            Gizmos.color= Color.green;
            Gizmos.DrawWireSphere(this.transform.position,attackDistance);
        }
    }
}

 

myoskin