주인공이 죽으면  PlayerDie()::PlayerCtrl 함수에서 씬뷰의 모든 몬스터들을 찾아 SendMessage를 이용 하나하나 메세지를 보냈다. 만일 적캐릭터가 엄청 많다면 그닥 효율적인 방법이 아니다.

    void PlayerDie() {
        //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
        GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
        foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
            monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        }
    }

순차적 호출 방식을 이벤트 구동방식으로 변경하자. 주인공이 죽었다는 이벤트를 시스템이 통보해주는 방식이다.

 

델리게이트

델리게이트는 함수(메서드)를 참조하는 변수를 의미한다. C++의 함수 포인터와 같은 의미이다. 사용하기전 아래와 같이 델리게이션 타일을 선언후 델리게이트타입을 이용 변수를 만들고 함수와 연결후 사용해야 한다.

using UnityEngine;

public class DelegateDemo : MonoBehavior
{
	//델리게이트 타입선언
	delegate float SumHandler(float a, float b);
	//델리게이트타입 델리게이트변수
	SumHandler sumHandler;
	//덧셈연산을 하는 함수
	float sum(float a, float b) {
		return a + b;
	}
	void Start() {
		sumHandler = sum;  //델리게이트변수에 함수를 연결
		float sum = sumHandler(10.0f, 5.0f);
		Dubug.Log($"Sum = {sum}"};
	}
}

 

주인공의 사망 이벤트 처리

PlayerCtrl() 함수내 델리게이트와 이벤트를 선언한다.

public delegate void PlayerDieHandler();  //델리게이트타입 선언
public static event PlayerDieHandler OnPlayerDie; //델리게이트 변수선언

PlayerCtrl() 함수내  PlayerDie()함수 내용을 지우고 OnPlayerDie()함수로 이벤트를 콜한다

void PlayerDie() {
    //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
    /*
    GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
    foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
        monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
    }
    */
    OnPlayerDie();  //주인공 사망 이벤트 호출(발생)
}

이제 MonsterCtrl를 변경한다. 우선 애니메이터의 "Die" 파라미터 해시값을 추출해 놓는다.

몬스트의 생명치를 저장할 hp변수를 만들어 놓는다.

 private readonly int hashDie = Animator.StringToHash("Die"); //파라미터해시값 추출
    
 private int hp = 100; //몬스터 생명점수

이제 MonsterCtrl에서 발생하는 이벤트에 반응할 OnPlayerDie()함수를 Player.OnPlayerDie와 연결한다.

PlayerCtrl는 스크립트이름인데 선언없이 사용가능하다. 교재는 PlayerCtrl이다. 변경하려고 했는데 자잘한 에러때문에 그냥 쓴다.

이벤트 스크립트는 반드시 스크립트의 활성화 시점에 연결하고 비활성화될때 해제해야 한다. 

MonsterCtrl Start()함수 앞에 OnEnable() OnDisable()함수를 추가한다.

    private int hp = 20; //몬스터 생명점수
    private void OnEnable() {  //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie; //교재는 PlayerCtrl.
    }
    private void OnDisable() { //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie -= this.OnPlayerDie; //교재는 PlayerCtrl.
    }

 

몬스터의 사망처리

CheckMonsterState()에 state.DIE일 경우 코루틴을 빠져나가는 조건문을 추가한다.

IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            if(state == State.DIE) yield break;  //몬스터 상태가 DIE면 코루틴 종료

OnCollisionEnter()함수에 충돌이 났을때 hp를 -10을 감소시키고 state를 DIE로 바꿔준다.

    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
			//중략
            hp -= 10;
            if(hp <= 0) state = State.DIE;
        }
    }

state가 DIE로 변경되면 MonsterAction()코루틴에서 "DIE" 트리거가 일어나고 Collider가 비활성화돼 죽은 몬스터에 총을 발사했을때 혈흔이 일어나지 않는다.

IEnumerator MonsterAction() {
    while (!isDie) {
            //중략
            case State.DIE: //사망상태
                isDie= true;  
                agent.isStopped = true;  //추적 정지
                anim.SetTrigger(hashDie); // 애니메이트 hasDie 트리거발생
                GetComponent<CapsuleCollider>().enabled = false;  //충돌컴포넌트 비활성화
                break;
        }
        yield return new WaitForSeconds(0.3f);
    }
}

Monster의 die 애니메이션 클립을 MonsterAnim 애니메이터뷰에 추가하고 Any State->Die State로 Transition을 만든다. 조건으로 사용할 파라미터는 Trigger타입으로 die를 추가하고 설정한다. Die State는 마지막 스테이트인 Exit스테이트로 연결하고 각 Transition의 연결조건은 다음과 같다.

 

 

 

실행해보면 죽은 몬스터가 떠 있다. 0.5 모델/Monster폴더의 Monster의 Die 애니메이션의 프리뷰를 봐도 떠 있다.

05.Models>Monster>Monster모델 원본을 눌러 인스펙터에서

Animation 탭을 눌러 die애니메이션을 골라 프리뷰를 봐도 공중에 떠 있다. 몬스터 아래 원이 피봇위치이다. 피봇위치는 Transform.position이다

아래쪽 Root Transform Position(Y)의 Bake Into Pose를 체크하던지 Based Upon값을 Feed로 설정하면  Pivot좌표값이 발위치로 조정된다.

실행하려면 방금 몬스터 모델 원형을 변경했기 때문에 Apply를 물어보면 적용시킨다. 실행해보면 잘된다.

PlayerCtrl.cs
0.00MB
MonsterCtrl.cs
0.01MB

전체파일을 원하시는 분은 출판사의 깃허브에서 다운받으시길.

https://github.com/IndieGameMaker/SpaceShooter2021/releases/tag/6%EC%9E%A5

 

Release 절대강좌! 유니티 - 6장까지 완료된 프로젝트 · IndieGameMaker/SpaceShooter2021

06장 - 적캐릭터 제작 이번 장에서는 유한상태머신을 이용히 적 캐릭터의 인공지능을 구현하고 유연한 애니메이션을 위한 메카님 애니메이션 시스템에 대해 실 습합니다. 또한, 내비게이션 시스

github.com

 

주인공 캐릭터의 hp가 0 이하일때는 죽는 애니메이션을 실행하고 나서 Game Over화면으로 넘어가게 된다.  계속 공격하는 몬스터에게 주인공의 죽음을 알릴 필요가 있다. 몬스터는 공격을 멈추고 추적도 정지하게 된다.

씬뷰의 오브젝트에 접근하는 방법중 Tag를 이용하는 방법이 있다.

Tag로 설정된 오브젝트를 하나만 리턴하는 함수와 여러개를 배열로 리턴하는 2가지 함수가 마련되어 있다.

GameObject.FindGameObjectWithTag(string tag);
GameObject.FindGameObjectsWithTag(string tag);

MOSTER라는 태그를 새로 추가하고 Monster의 태그로 지정한다. 없다면 +를 눌러 만든후 지정해 준다.

Player가 사망했을때 호출되는 PlayerCtrl스크립트의 PlayerDie함수에서 모든 몬스터를 찾아 공격 중지 함수를 호출하는 코드를 다음과 같이 추가한다.

PlayerCtrl - PlayerDie()

SendMessage() 함수는 "OnPlayDie()"가 해당 게임오브젝트의 스크립트에 있다면 실행하라는 거다. 두번째 인자는 함수가 없더라도 없다는 메시지를 반환하지 않겠다는 옵션이다. 

void PlayerDie() {
    //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
    GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTERS");
    foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
        monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
    }
}

이제 MonsterCtrl에 - OnPlayerDie()함수를 추가하겠다.

private readonly int hashPlayerDie = Animator.StringToHash("PlayerDie");

void OnPlayerDie() {
    StopAllCoroutines();
    agent.isStopped= true;
    anim.SetTrigger(hashPlayerDie);
}

MonsterAnim을 더블클릭해 애니메이터를 열고 07.animations폴더에서 Monster@Gangnam Style애니메이션 파일을 드래그에 추가한다. 파라미터 +를 누르고 PlayerDie라는 이름의 Trigger를 만든다. Gangnam Style애니메이션은 주인공이 사망하는 경우에 실행할 것이므로 Any State에서 Gangnam Style 스테이트로 Transition을 연결하고 Gangnam Style에서 Exit로 가는 Transition도 연결한다.

 

 

Gangnam Style state를 한번만 클릭해서 인스펙터에서 Multiplier Parameter를 체크하면 자동으로 Speed가 생긴다

MonsterCtrl 스크립터에 아래 코드를 추가한다.

애니메이터의 Speed 변수를 랜덤으로 설정하고 Any->GangnamStyle transition을 트리거 시킨다.

private readonly int hashSpeed = Animator.StringToHash("Speed");

void OnPlayerDie() {
    StopAllCoroutines();
    agent.isStopped= true;
    anim.SetFloat(hashSpeed, Random.Range(0.5f, 1.2f));
    anim.SetTrigger(hashPlayerDie);
}

하이라키의 몬스터를 프로젝트 프리팹 폴더로 끌어와 프리팹을 만든다. 몬스터 프리팹을 끌어다 씬뷰에 여러개 배치한다.

다가가면 몬스터가 공격을 하고 HP가 0이되면 몬스터들이 승리의 강남스타일을 춤춘다.

처음에 잘안되서 디버깅 해보니 몬스트의 TAG는 MONSTER지만 wrist는 PUNCH로 되어 있어야 HP가 떨어진다.

만일 잘못되어 있다면 하이라키의 몬스터가 아니라 이제는 프로젝트 프리팹의 몬스터 프리팹을 수정해야한다.

 

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;  //for Navigation


public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
    }

    public State state = State.IDLE;  //몬스트 현재상태
    public float traceDist = 10.0f;  //추적 사정거리
    public float attackDist = 2.0f; //공격 사정거리
    public bool isDie = false;  //몬스터사망여부

    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    private Transform playerTr;  
    private NavMeshAgent agent;
    private Animator anim;

    public readonly int hashTrace = Animator.StringToHash("IsTrace");
    public readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");
    private readonly int hashPlayerDie = Animator.StringToHash("PlayerDie");
    private readonly int hashSpeed = Animator.StringToHash("Speed");
    private GameObject bloodEffect;

    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        bloodEffect = Resources.Load<GameObject>("GoopSpray");
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
        
    }

    //일정한 간격으로 몬스터의 행동 상태를 체크
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    //몬스터의 상태에 따라 몬스터의 동작을 수행
    IEnumerator MonsterAction() {
        while (!isDie) {
            transform.LookAt(playerTr);
            switch (state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool(hashTrace, true);
                    anim.SetBool(hashAttack, false); //공격파라메터 변경
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool(hashAttack, true);  //공격파라메터 변경
                    break;
                case State.DIE: //사망상태
                    break;

            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            Destroy(coll.gameObject);             //충돌한 총알을 삭제
            anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
            Vector3 pos = coll.GetContact(0).point;
            Quaternion rot = Quaternion.LookRotation(-coll.GetContact(0).normal);
            ShowBloodEffect(pos, rot);  
        }
    }
    void ShowBloodEffect(Vector3 pos, Quaternion rot) {
        GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);
        blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.
        Destroy(blood, 1.0f);
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {

            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
    void OnPlayerDie() {
        Debug.Log("PlayerDie");
        StopAllCoroutines();
        agent.isStopped= true;
        anim.SetFloat(hashSpeed, Random.Range(0.5f, 1.2f));
        anim.SetTrigger(hashPlayerDie);
    }
}

 

보통의 인체형 모델은 애미메이션을 수행하기 위해 Rig(본 구조, 뼈대)가 설정돼 있다.적 캐릭터 모델도 이러한 관절이 설정돼 있으며 하리라키 뷰에서 확인할 수 있다. 하이라키 뷰상에 표시된 각 관절은 게임오브젝트이며 모드 Transform 컴포넌트를 가지고 있다. 이렇게 하나의 3D모델안의 많은 Transform연산은 로드가 높으므로 최적화가 필요하다. 필요한것만 두고 다른것들은 노출되지 않게 하면 속도 향상에 도움을 줄 수 있다.

몬스터 모델에서 필요한 Rig는 Sphere Collider와 Rigidbody를 추가한 양손이므로 이부분만을 하이라키 뷰에 노출되게 설정해보자. 05.Models/Monster폴더에 있는 Monster원본 모델을 선택후 인스펙터뷰 Rig  탭의 Optimize Game Objects 옵션을 선택하면 하단에 Extra Transforms to Exppose 항목이 나타난다.

 L_wrist와 R_wrist만 선택하고 Apply를 누르면 하이라키뷰의 Monster가 정리된걸 볼 수 있다.

)

본 구조 최적화는 프리팹으로 만들기 전에 해야한다. 하이라키뷰의 Monster는 아직 프리팹으로 만들기 전이므로 원본 Monster모델을 선택하고 본 구조를 최적화 할 수 있었다. 

몬스터 본체의 Capsule Collider와 양손의 Sphere Collider는 걸을때 마다 충돌을 일으킨다. 이를 막기위해 레이어로 분리한다.

인스펙터뷰의 레이어를 누르고 +를 눌러 MONSTER_BODY, MONSTER_PUNCH라는 2개의 레이어를 추가한다.

우선 하이라키뷰의 Monster를 눌러  MONSTER_BODY로 레이어를 선택하면 

하위 하이라키도 바꿀꺼냐고 물어보면 No를 대답한다. L_wrist와 R_Wrist를 눌러 MONSTER_PUNCH로 레이어를 바꾼다.

 

메뉴>Edit>ProjectSetting>Phsics 맨 아래 Layer Collision Matrix 설정에서  MONSTER_BODY와 MONSTER_PUNCH가 XY로 교차하는 부분의 체크박스를 언체크하면 두 레이어간의 물리적인 충돌은 물리엔진에서 처리하지 않는다.

몬스터가 플레이어와 일정 거리 이내로 좁혀지면 공격 애니메이션을 수행한다. 이때 플레이어의 생명력이 줄어드는 코드를 짜보자.

방법은 collider를 통해 처리하는 방법과 주기적으로 데미지를 주는 방법이 있다. 몬스터는 공격 상태가 되면 양손으로 타격하는 애니메이션을 실행하므로 전자의 방식으로 구현해보자.

하이라키뷰에서 L_wrist, R_wrist를 선택한후 다음 속성을 변경하면 동시에 적용된다.

물리적 충격을 주가위해 몬스터의 양속에 Collider와  Rigidbody컴포넌트를 추가한다. 

몬스터본체 Capsule Collider와의 간섭을 없애기 위해 Collider의 Is Trigger check, 움직임은 애니메이션에서 컨트롤 하므로 물리엔진인 Rigidbody의 Is Kinematic check한다.

Tag에서 PUNCH를 만들고 다시 L_wrist, R_wrist를 선택한후 TAG를 PUNCH로 바꿔준다.

주인공 Player에도 Capsule Collider를 추가해 다음과 같이 Center (0,1,0), Height:2을 설정한다. 

Rigidbody를 추가하고 FreezeRotation x,z를 check한다. 이러면 넘어지지 않는다

보통 FPS나 TPS게임을 개발할때 주인공은 character Controller컴포넌트를 사용한다. collider+rigidbody에 물리엔진을 disable해서 떨림현상이나 미림현상을 방지할 수 있다

 

OnTriggerEnter 콜백 함수

몬스터 양손에 Sphere Collider에서 IsTrigger를 체크했기때문에 OnTrigger계열의 콜백함수가 호출된다.

 

PlayerCtrl에 OnTriggerEnter()를 추가한다.

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

public class PlayerCtrl : MonoBehaviour {
    // Start is called before the first frame update
    

    Transform tr;
    private Animation anim;

    private float moveSpeed = 10f; //이동속도
    private float turnSpeed = 0f;  //회전속도

    private readonly float initHP = 100.0f;  //초기 생명값
    public float curHP; //현재생명값

    IEnumerator  Start() {  //start()함수는 코루틴으로 실행할 수 있다.
        curHP = initHP;
        tr = GetComponent<Transform>();
        anim = GetComponent<Animation>();  //추가된 코드
        anim.Play("Idle");
        turnSpeed = 0.0f;  //프로그램 기동시 가비지 마우스값을 막기위해
        yield return new WaitForSeconds(0.3f);  //0.3초 기다린후
        turnSpeed = 100f;  //변수값을 설정
    }

    // Update is called once per frame
    void Update() {
        float h = Input.GetAxis("Horizontal");  //AD 입력 좌우
        float v = Input.GetAxis("Vertical");  //WS 입력 전후
        if (Mathf.Abs(h) > float.Epsilon || Mathf.Abs(v) > float.Epsilon) {  //움직임이 없다면 불필요한 동작을 안한다.
            Vector3 dir = Vector3.right * h + Vector3.forward * v;
            tr.Translate(dir.normalized * moveSpeed * Time.deltaTime);
            PlayerAnim(h, v);
        }
        float r = Input.GetAxis("Mouse X");  //마우스 x축 입력 
        tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);
    }
    void PlayerAnim(float h, float v) {
        GetComponent<NavCtrl>().SetNavStop();
        if (v >= 0.1f) {  //앞으로 달림
            anim.CrossFade("RunF",0.25f);
        } else if(v <= -0.1f) { //뒤로 움직임
            anim.CrossFade("RunB", 0.25f);
        } else if(h >= 0.1f) { //오른쪽으로 움직임
            anim.CrossFade("RunR", 0.25f);
        } else if (h <= -0.1f) { //왼쪽으로 움직임
            anim.CrossFade("RunL", 0.25f);
        } else { //제자리대기
            anim.CrossFade("Idle", 0.25f);
        }
    }
    void OnTriggerEnter(Collider coll) {
        if(curHP >= 0.0f && coll.CompareTag("PUNCH")) {
            curHP -= 10.0f;
            Debug.Log($"Player hp = { curHP / initHP }");
            if (curHP <= 0.0f) {
                PlayerDie();
            }
        }
    }
    void PlayerDie() {
        Debug.Log("Player Die !");
    }
}

실행하다보니 플레이어를 마우스로 좌우로 회전 컨트롤 하는게 잘 안된다. 

처음에는 마우스로 우클릭을 하면 그 지점을 바라보는 걸 할려고 했다.

물론 스크린좌표를 월드좌표로 변환해서 레이를 쏴서 그 오브젝트를 바라보게해도 되는데 간단하게 LookAt()을 이용해보겠다.

스크립트를 하나 만들고  RightClick으로 이름 짓는다. 스크립트를 Player에 적용한다.

Monster 인스펙터  TAG가 MONSTER로 되어 있어야 한다.

FindwidhtTag()로 간단히 찾아 monsterTR에 저장하고 LookAt()을 이용해 방향을 바꾼다. 

다만. 순간적으로 바꿔지기 때문에 부드럽게 바꾸려면 다른 함수를 써야 한다. 

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

public class RightClick : MonoBehaviour
{
    // Start is called before the first frame update
    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    void Start()
    {
        monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            transform.LookAt(monsterTr);
        }
    }
}

다른 방법은  어차피 NavMesh가 적에 적용되어 있으니 그걸 응용해보기로 했다. 목적은 자동추적보다는 플레이어의 자동 회전에 가깝다. 일단 하이리키 Player에 NavMesh Agent를 추가한다.

교재의 프로젝트에 영향을 주지 않게 하기 위해 따로 스크립트도 추가하고 NavCtrl로 이름짓는다.

코드는 간단하다. 마우스 우클릭을 하면 Player가 자동으로 적을 따라가고 다시 우클릭을 하면 멈춘다.

다른 스크립트에도 Navigation을 멈추게 하기 위해  SetNavStop()을 추가한다.

    public void SetNavStop() {
        agent.isStopped = true;
    }

추가된 NavCtrl 전체코드, 대부분 MonsterCtrl에서 NavMesh에 관련된 코드를 복붙했다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;  //for Navigation
public class NavCtrl : MonoBehaviour
{
    // Start is called before the first frame update
    private NavMeshAgent agent;
    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.isStopped = true; //추적을 중지
        monsterTr = GameObject.FindWithTag("MONSTER").GetComponent<Transform>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            if (agent.isStopped == true) {
                agent.SetDestination(monsterTr.position);  //목표설정
                agent.isStopped = false; //추적을 개시
            } else {
                agent.isStopped = true; //추적을 중지
            }
        }
    }
    public void SetNavStop() {
        agent.isStopped = true;
    }
}

WASD키를 눌렀을때 Navigation을 멈추기 위해 다음 코드를 PlayerCtrl스크립트 PlayerAnim()  맨윗에 추가한다.

 GetComponent<NavCtrl>().SetNavStop();로 NavCtrl의 SetNavStop에 접근할 수 있다. 스크립트 이름이 class이름이고 이걸 GetComponet<T>의 인수로 바로 사용할 수 있다.

void PlayerAnim(float h, float v) {
        GetComponent<NavCtrl>().SetNavStop();

좌클릭을 하면 총이 나갈때도 네비게이션을 멈추게 해야하는데 다행이 멈춘다 . 이유는 각자 찾아보세요. ㅎㅎ 아마 애니메이터의 State가 변하면서 뭔가 바뀌는듯.

주인공 캐릭터(Player)가 쏜 총알이 몬스터에 명중하면 몬스터가 피를 흘리는 혈흔 효과(Blood Effect)를 추가해보자. 혈은 효과는 파티클로 구현하며 프로젝트뷰의 03.Prefabs/EffecExamples/Blood/Prefabs폴더에 있는 BloodSprayeffect를 사용한다.  버전의 차이인지 발견하지 못해서 아래 EffectExamples > Goop Effects > Prefabs> GoopSpray를 사용하였다.

Resources 폴더

지금까지는 프리팹을 사용하기 위해 public GameObject xxx를 선언하고 인스펙터에서 연결해놓고 사용하였다. 이번에는 스크립트에서 런모드(동적)으로 로드하는 방법을 사용한다. 이방법을 쓰기 위해서는 프리팹이나 에셋이 Resouces 폴더안에 있어야 한다. 

 Assets root에 Resources 폴더를 하나 만들자. BloodSprayeffect나 GoopSpray를 끌어다 놓는다. 이 Resources폴더는 빌드할때 포함되므로 필요한 것들만 놓기 바랍니다.

MonsterCtrl 스크립트에 다음과 같이 추가한다.  Goopspray효과는 피튀는게 좀 작아서 localScale을 변경해 크게했다

  • blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.

효과의 인스턴스를 만드는데 인수가 4개이다 마지막 monsterTr의 Child로 만들겠다는 뜻이다.

  • GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);

이렇게 child로 만들면 Monster가 총알을 맞은뒤 이동해도 혈흔이  따라다닌다.

private GameObject bloodEffect;
void Start(){
	bloodEffect = Resources.Load<GameObject>("GoopSpray");
}
private void OnCollisionEnter(Collision coll) {
    if (coll.collider.CompareTag("BULLET")) {
        Destroy(coll.gameObject);             //충돌한 총알을 삭제
        anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
        Vector3 pos = coll.GetContact(0).point;
        Quaternion rot = Quaternion.LookRotation(-coll.GetContact(0).normal);
        ShowBloodEffect(pos, rot);  
    }
}
void ShowBloodEffect(Vector3 pos, Quaternion rot) {
    GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);
    blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.
    Destroy(blood, 1.0f);
}

MonsterCtrl 전체코드

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;  //for Navigation


public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
    }

    public State state = State.IDLE;  //몬스트 현재상태
    public float traceDist = 10.0f;  //추적 사정거리
    public float attackDist = 2.0f; //공격 사정거리
    public bool isDie = false;  //몬스터사망여부

    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    private Transform playerTr;  
    private NavMeshAgent agent;
    private Animator anim;

    public readonly int hashTrace = Animator.StringToHash("IsTrace");
    public readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");

    private GameObject bloodEffect;

    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        bloodEffect = Resources.Load<GameObject>("GoopSpray");
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
        
    }

    //일정한 간격으로 몬스터의 행동 상태를 체크
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    //몬스터의 상태에 따라 몬스터의 동작을 수행
    IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool(hashTrace, true);
                    anim.SetBool(hashAttack, false); //공격파라메터 변경
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool(hashAttack, true);  //공격파라메터 변경
                    break;
                case State.DIE: //사망상태
                    break;

            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            Destroy(coll.gameObject);             //충돌한 총알을 삭제
            anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
            Vector3 pos = coll.GetContact(0).point;
            Quaternion rot = Quaternion.LookRotation(-coll.GetContact(0).normal);
            ShowBloodEffect(pos, rot);  
        }
    }
    void ShowBloodEffect(Vector3 pos, Quaternion rot) {
        GameObject blood = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);
        blood.transform.localScale = new Vector3(5, 5, 5);  //효과가 작아서 확대했다.
        Destroy(blood, 1.0f);
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {

            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
}

아래와 같은 유한 상태 머신 구현을 위해 상태를 주기적으로 업데이트하는데 매 프레임 하는 건 부담을 주므로 0.2~0.3초간격으로 해도 된다면 코루틴을 적극 활용하겠다.

적 캐릭터의 상태체크

0.3초 마다 상태를 체크한다. Player와 Monster의 거리를 측정해 추적 사정거리와 공격 사정거리를 판단한 다음 상태를 변경한다. MonsterCtrl 스크립트를 다음과 같이 수정한다.

Enum을 사용하면 인스펙터뷰에 리스트박스로 표시되면 가독성을 높혀준다.

Start()함수를 비롯 2개의 코루틴 CheckMonsterState() MonsterActionState()가 있다.

Start()는 참조조기화및 코루틴을 기동시킨다.

코루틴들은 while(!IsDie) 순환문으로 살아 있는 동안만 감시를 하다가 몬스터가 죽으면 isDie=true로 순환문을 빠져나와 종료한다.

 CheckMonsterState() 에서 float distance = Vector3.Distance(playerTr.position, monsterTr.position);거리를 측정해 state를 IDLE, ATTACK, TRACE로 바꾸준다.

MonsterActionState()에서는 state가 Trace라면 agent.SetDestination(playerTr.position);로 목표물을 지정해주고,  agent.isStopped = false로  추적을 시작한다.; 다른 state에 대해서도 적절한 처리를 한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;  //for Navigation

public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE
    }
    public State state = State.IDLE;
    public float traceDist = 10.0f;
    public float attackDist = 2.0f;
    public bool isDie = false;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;

    public Vector3 pPos, mPos;
    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        //agent.destination = playerTr.position;
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
    }
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            pPos = playerTr.position;
            mPos = monsterTr.position;
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true;
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);
                    agent.isStopped = false;
                    break;
                case State.ATTACK:
                    break;
                case State.DIE:
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {
            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
}

실행하면서 Monster의 인스펙터의 MonsterCtrl 스크립트뷰를 보면 거리가 좁혀지면 IDEL이 TRACE로 바뀌면 Monster가 추적을 시작하고 더 가가워지면 ATTACK로 바뀐다. 막 도망가서 거리를 벌리면 다시 IDLE상태로 된다.

마지막으로  OnDrawGizmos()는 씬뷰에서만 보인다. 디버그용이다.

트레이스 범위에서  범위에 해당하는 파란색 구를 그려주고 공격범위에서는 범위 크기의 빨간색 구를 그려준다.

애니메이션 동기화

몬스터는 유령처럼 움직임없이 따라오는데 애니메이션을 수행시켜 움직여보자. 애니메이터뷰에 IsTrace 변수를 설정해 놓았다. MonsterCtrl 스크립터에서 Monster의 Animator 컴포넌트에 접근하기 위한 변수를 설정및 할당하고 MonsterAction 함수에 Animator에서 생성한 변숫값을 변경한 변숫값을 변경하는 로직을 추가한다.

switch(state)문안에  case State.IDLE과 State.TRACE 때 IsTrace의 파라메터를false, true로 바꿔주는 anim.SetBool()이 추가되었다. 변경된  IsTrace의 파라메터는 애니메이터의 transition을 일어나게 한다.

IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool("IsTrace", false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool("IsTrace", true);
                    break;

MonsterCtrl 전체코드

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;  //for Navigation


public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
    }

    public State state = State.IDLE;  //몬스트 현재상태
    public float traceDist = 10.0f;  //추적 사정거리
    public float attackDist = 2.0f; //공격 사정거리
    public bool isDie = false;  //몬스터사망여부

    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    private Transform playerTr;  
    private NavMeshAgent agent;
    private Animator anim;

    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        //agent.destination = playerTr.position;
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
        
    }

    //일정한 간격으로 몬스터의 행동 상태를 체크
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    //몬스터의 상태에 따라 몬스터의 동작을 수행
    IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool("IsTrace", false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool("IsTrace", true);
                    break;
                case State.ATTACK: //공격상태
                    break;
                case State.DIE: //사망상태
                    break;

            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {

            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
}

 

몬스터 공격 루틴

몬스터에 공격 기능을 부여하자. 먼저 프로젝트 뷰의 MonsterAnim을 더블클릭해 애니메이터 뷰를 열고  attack 애니메이션 클립을 드래그해 배치한다. attack와 walk 스테이트간의 Transition 2개를 배치한다. 

walk->attack transition은 HasExitTime을 언체크하고 Conditions+를 누루고 IsAttack를 선택하고 true로

attack->walk transition은 HasExitTime을 언체크하고 Conditions+를 누루고 IsAttack를 선택하고 false로

MonsterCtrl 스크립터의 코루틴  MonsterAction() 내부의 Trace와 Attack case문에 IsAttack 파라메터 부분을 넣어준다

    IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool("IsTrace", false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool("IsTrace", true);
                    anim.SetBool("IsAttack", false);
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool("IsAttack", true);
                    break;

SetBool()은 파라메테명의 오자가 있으면 오류도 발생하고 호출될때 마다 해시테이블을 검색하므로 파라메터 해시값을 미리 추출해 전달하는 방식이 바람직하다. 우선 다음같이 스트링을 해시값으로 미리 바꿔 놓는다 "Hit"는 몬스터가 피격받을때 상태인데 미리 넣어두겠다

    public readonly int hashTrace = Animator.StringToHash("IsTrace");
    public readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");

코루팅 switch()문안의 스트링을 해시값으로 변경한다.

IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool(hashTrace, true);
                    anim.SetBool(hashAttack, false); //공격파라메터 변경
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool(hashAttack, true);  //공격파라메터 변경
                    break;

실행해 보면 이제 몬스터가 걷고 공격한다. 이제부터는 몬스터가 피격받을때 취할 리액션을 붙여 넣겠다.

게임뷰탭옆 Ani탭을 누른후 Hit Trigger 파라메터를 하나 만든후 몬스터 모델에서 gothit을 끌어다 놓고 다음과 transition을 만든다.

Any->gothit transition은 HasExitTime 언체크, condition +를 누르고 Hit로 설정한다

gothit에서 idle 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 isTrace false로 한다.

gothit에서 walk 로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 condition + IsTrace true, IsAttack false로 한다.

gothit에서 attack로 단방향 transition을 만든다 HasExitTime는 체크, ExitTime 0.9 condition + IsAttack true로 한다.

몬스터와 총알이 충돌이 나면 총알을 없애주고 hasHit을 Trigger시켜준다(setTrigger) 그러면 Any->gotHit transition이 일어난다.

private readonly int hashHit = Animator.StringToHash("Hit");

private void OnCollisionEnter(Collision coll) {
    if (coll.collider.CompareTag("BULLET")) {
        Destroy(coll.gameObject);             //충돌한 총알을 삭제
        anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
    }
}

현재까지의 최종코드

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;  //for Navigation


public class MonsterCtrl : MonoBehaviour
{
    //몬스트 상태 
    public enum State {  //인스펙터뷰에 콤보박스로 표시됨
        IDLE, PATROL, TRACE, ATTACK, DIE ,DANCE
    }

    public State state = State.IDLE;  //몬스트 현재상태
    public float traceDist = 10.0f;  //추적 사정거리
    public float attackDist = 2.0f; //공격 사정거리
    public bool isDie = false;  //몬스터사망여부

    private Transform monsterTr;  //컴포넌트의 캐시를 처리할 변수
    private Transform playerTr;  
    private NavMeshAgent agent;
    private Animator anim;

    public readonly int hashTrace = Animator.StringToHash("IsTrace");
    public readonly int hashAttack = Animator.StringToHash("IsAttack");
    private readonly int hashHit = Animator.StringToHash("Hit");

    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        //agent.destination = playerTr.position;
        StartCoroutine(CheckMonsterState());
        StartCoroutine(MonsterAction());
        
    }

    //일정한 간격으로 몬스터의 행동 상태를 체크
    IEnumerator CheckMonsterState() {
        while(!isDie) {
            yield return new WaitForSeconds(0.3f);
            //Player와 Monster간의 거리측정 스칼라값
            float distance = Vector3.Distance(playerTr.position, monsterTr.position);
            if(distance <= attackDist) {
                state = State.ATTACK; //Attack로 상태변경
            } else if(distance <= traceDist) {
                state = State.TRACE;  //Trace로 상태변경
            } else {
                state = State.IDLE;  //Idle로 상태변경
            }
        }
    }
    //몬스터의 상태에 따라 몬스터의 동작을 수행
    IEnumerator MonsterAction() {
        while (!isDie) {
            switch(state) {
                case State.IDLE:
                    agent.isStopped= true; //추적을 중지
                    anim.SetBool(hashTrace, false);
                    break;
                case State.TRACE:
                    agent.SetDestination(playerTr.position);  //목표설정
                    agent.isStopped = false; //추적을 재개
                    anim.SetBool(hashTrace, true);
                    anim.SetBool(hashAttack, false); //공격파라메터 변경
                    break;
                case State.ATTACK: //공격상태
                    anim.SetBool(hashAttack, true);  //공격파라메터 변경
                    break;
                case State.DIE: //사망상태
                    break;

            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            Destroy(coll.gameObject);             //충돌한 총알을 삭제
            anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
        }
    }
    private void OnDrawGizmos() {  //상태에 따라 기즈모 색깔 변경
        if(state == State.TRACE) {

            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, traceDist);
        }
        if(state == State.ATTACK) {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, attackDist);
        }
    }
}

적 캐릭터가 장애물을 피해서 가는 걸 구현한다면 PathFinding같은 알고리즘이 필요하다. 유니티가 3D모델을 분석해 추적할 수 있는  내비게이션 기능을 제공하기 이전 많이 사용되었다. 유니티의 내비게이션 기능을 이용해 주인공을 추적하는 로직을 구현해 보자. 추적을 시작하면 walk애니메이션으로 변경하고 플레이어에 근접했을때 attack 애니메이션으로 변경되도록 메카님을 구성해본다.

네비게이션은 스테이즈를 구성하고 있는 3D메시(지형:Geometry)를 분석해 네비메시(NavMesh) 데이터를 미리 생성하는 방식이다. 추적할 수 있는 영역과 지나갈수 없는 영역을 미리 메시로 만들어 동작을 연출할 수 있다.

 

네비게이션 설정 - Navigation Static Flag

네비게이션을 만들(Bake)려면 그 대상이 무엇인지 지정해야 한다. 우선 바닥으로 사용되는 Floor의 static 옵션을 설정한다.  Floor 인스펙터의 이름 오른쪽의  Static옆 삼각형을 눌러 Navigation Static만 체크한다.

프리팹폴더의 Barrel도 Wall도 Static중 Navigation Static만 체크하고 다이얼로그가 나오면 children까지 변경해준다.

Monster를 선택하고. window>AI>Navigation을 선택하면 Navigation뷰가 나오고 Bake를 누르고 마치면 파란색 영역이 만들어진다. 드럼통 주위는 파란색 메시로 채워지지 않고 구멍이 나있다. 이 영영읍 지나갈 수 없는 영억으로 인식해 추적한 대상이 장애물로 판단한다. Bake는 씬뷰에 플레이전 있는 것들만 인식한다. 동적으로 생성되는 것은 예측할수 없는 것 같다. 따라서 스테이지에 적용된  Barrel 스크립트를 제거한고 프리팹 Barrel을 여기저기 배치하고 Bake를 한번 더 누른다.

플레이어와 몬스터를 떨어트려 놓과 사이에 배치해주기 바란다. 몬스터가 피해가는지

NavMeshAgent 컴포넌트

NavMeshAgent 컴포넌트는 목표를 향해 움직일 때 서로를 피해가는 캐릭터 생성에 유용합니다. 에이전트는 내비메시를 이용하여 게임 월드에 대해 추론하고 서로 또는 기타 움직이는 장애물을 피할 방법을 이해하고 있습니다. 내비메시 에이전트의 스크립팅 API를 이용하여 경로를 찾거나 공간을 추론할 수 있습니다.

 

하이라키에서 Monster를 선택하고 메뉴의 컴포넌트>Navigation>Nav Mesh Agent를 선택한다.

NaveMeshAgent의 각 속성은 다음과 같다.

프로퍼티 기능
에이전트 크기
Radius 에이전트의 반경은 장애물과 다른 에이전트 간의 충돌 계산하기 위해 사용됩니다.
Height 에이전트가 장애물 밑으로 지나갈 있는 높이 간격입니다.
Base offset 트랜스폼 피봇 포인트와 관련한 충돌 실린더의 오프셋입니다.
스티어링
Speed 최대 이동 속도(초당 월드 단위로)
Angular Speed 최대 회전 속도(초당 각도)
Acceleration 최대 가속(제곱 초당 월드 단위로)
Stopping distance 에이전트는 목표 위치에 가까워졌을 정지합니다.
Auto Braking 활성화 에이전트는 목적지에 다다를 속도를 줄입니다. 에이전트가 멀티플 포인트 사이에서 부드럽게 움직여야 하는 순찰과 같은 동작을 때에는 반드시 비활성화 시켜야 합니다.
장애물 회피
Quality 장애물 회피 품질입니다. 에이전트의 수가 많다면 장애물 회피 품질을 줄임으로써 CPU 시간을 절약할 있습니다. 회피를 없음으로 설정할 경우 충돌만 해결할 있을 다른 에이전트와 장애물에 대한 적극적인 회피는 하지 않습니다.
Priority 낮은 우선 순위의 에이전트는 에이전트의 회피 대상에서 제외됩니다. 값은 0에서 99사이에서 설정되어야 하며 낮은 숫자가 높은 우선 순위임을 의미합니다.
경로 찾기
Auto Traverse OffMesh Link 자동적으로 오프 메시 링크를 횡단하려면 트루로 설정해야 합니다. 애니메이션을 사용하거나 오프메시 링크를 횡단하는 특정한 방법을 사용하고 싶다면 반드시 이를 꺼놔야 합니다.
Auto Repath 활성화 에이전트가 경로 일부분의 끝에 도달하면 경로를 재탐색 합니다. 목적지까지 경로가 없다면 목적지에서 제일 가깝게 도달할 있는 위치까지 부분적인 경로가 생성됩니다.
Area Mask 영역 마스크는 에이전트가 경로 탐색에 어떠한 영역 타입 고려할 것인지를 설명합니다. 내비메시 베이킹를 위해 메시를 준비할 각각의 메시 영역 타입을 설정할 있습니다. 예를 들어 계단을 특별한 영역 타입으로 표시하고 몇몇 캐릭터 타입의 계단 이용을 금지할 있습니다.

세부 정보

에이전트는 수직으로 서 있는 실린더에 의해 정의되며 실린더의 크기는 Radius  Height 프로퍼티에 의해 특정됩니다. 실린더는 오브젝트와 함께 움직이지만 오브젝트가 회전한다 해도 계속 수직으로 서 있습니다. 실린더의 모양은 다른 에이전트와 장애물간의 충돌 감지와 대응에 사용됩니다. 게임 오브젝트의 앵커 포인트가 실린더의 베이스에 없을 때 높이의 차이를 메우기 위해 베이스 오프셋 프로퍼티를 사용할 수 있습니다.

실린더의 높이와 반경은 실제로 두 개 의 다른 장소에 특정됩니다. 내비메시 베이크 설정과 각 에이전트의 프로퍼티 입니다.

내비메시 베이크 설정 은 내비메시 에이전트가 어떻게 정적인 월드 지오메트리와 충돌하고 또는 어떻게 회피하는지를 설명합니다. 메모리 여유량을 유지하고 CPU 로드를 지속적으로 체크하기 위해 오직 하나의 크기만이 베이크 설정에서 특정될 수 있습니다. -내비메시 에이전트 프로퍼티 값은 에이전트가 움직이는 장애물 및 다른 에이전트와 어떻게 충돌하는지를 설명합니다.

대부분의 경우 두 장소 모두에 에이전트 크기를 똑같이 설정합니다. 하지만 예를 들어 크기가 큰 에이전트의 반경이 더 넓다면 다른 에이전트는 그 에이전트 주변에 공간을 더 많이 남겨둡니다. 하지만 그렇지 않다면 크기가 큰 에이전트도 마찬가지로 환경을 무시합니다.

 

하이라키뷰에서 Player를 선택하고  TAG에서 PLAYER를 생성하고 적용한다

Nave Mesh Agent는 인스펙터에서 추적대상을 연결할 수 없고 스크립트를 생성해야한다.

MonsterCtrl스크립트를 생성하자.  Monster에 적용한다.

코드는 간단한다 다음한줄로 자동으로 추적한다. 다만 명령을 실행할 당시의 위치만이다. 명령실행후 플레이어가 움직이면 자동 갱신되지 않는다.

agent.destination = playerTr.position;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;  //for Navigation

public class MonsterCtrl : MonoBehaviour
{
    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    void Start()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        agent.destination = playerTr.position;  //목표를 플레이어로 설정
    }
}

잘 된다. 처음에는 PLAYER TAG를 Monster에도 설정했다가 뺑글뺑글 도는 걸 보고 풀어줬더니 잘되었다. PLAYER TAG는 Player에만

 

Monster는 NavMeshAgent가 미리 베이크된 NavMesh에 붙어서 이동하기 때문에 살짝 떠 있다. 방법은 NavMesh를 Bake할대 낮춰주던지 Monster NavMeshAgent의 BaseOffset을 -0.1로 낮춰주는 거다

오른쪽 BaseOffset이 -0.1 로 적용되어 있음
Monster가 NavMesh밑으로 가라앉어있다.

 

적 캐릭터는 NPC(Non Playable Character)의 일종으로 게임에 참여한 플레이어가 조작할 수 없는 캐릭터를 말한다.

 

유한 상태 머신의 정의

NPC의 인공지능을 구현하는 방식에는 여러가지가 있다. 그중 하나가 유한상태머신(FSM:Finite State Machine)으로 적캐릭터가 스스로 알아서 주변 환경에 적응하거나 들어오는 반응에 적절하게 반작용하도록 구현한 것을 말한다. 

적 캐릭터가 아래와 같이 Spwan되고 일정 범위를 순찰하다가 주인공을 추적하고 사정거리 이내에 근접하면 공격하는 것과 같은 구조를 상태머신이라고 한다. 또한 일정 데미지가 누적되며 사망하고 소멸되는 구조이기에 상태가 유한하여 유한상태머신 이라고 부른다.

상태가 많아 질수록 유지보수가 어려워진다. 이런점을 개선하기 위해 모듈화하고 계층적으로 분류하는 계층적 유한상태머신과 행동트리방식도 개발에 많이 적용한다.

 

메카님

FSM의 상태에 따라  적절한 애니메이션 동작을 취할 수 있게 메카님을 사용해 유연한 애니메이션을 제어해보자.

메카님은 리타게팅 기능을 제공해 Humanoid 모델의 필수 본 구조와 일치하면 다른 모델의 애니메이션을 바로 적용할 수 있다.

 

적캐릭터 3D 모델 임포트

다운한 Resources/Models폴더에서 Monster피키지를 임포트해 프로젝트의 05.Models폴더로 옮긴다.

Monster아이콘 삼각형을 눌러 펼쳐보면 삼각형아이콘이 애니메이션이다. idel, walk, attack,die, fall, gothit등 기본적인 애니메이션이 포함되어 있다.

메카님 애니메이션으로 전화

인스펙터뷰의 Rig탭을 눌러보면 Animation Type이 Generic으로 설정되어 있다.

애니메이션은 레거시, 제너릭, 휴머노이드 3가지 유형이 있으며,  각 타입특성은 다음과 같다.

레거시 : 유니티 4.0이전 버전

제너릭 : 메카님 애니메이션(비인간형모델), 리타게팅 불가

휴머노이드 : 메카님 애니메이션(인간형모델), 리타게팅 가능

 

Monster는 2족 보행(Biped) 모델로 휴머노이드로 설정해 구현할 수 있다. 애니메이션 타입을 Humanoid로 변경후 하단의 Apply후 자동 매핑이 되고 Configure버튼이 활성화 된다. 실패하면 x표시가 나타나고 수동으로 본을 연결해야 한다.

 

Config 버튼을 누르면 Bone구조의 매핑 정보를 확인할 수 있는 Avatar Confg화면이 나타난다. Avatar  Asset은 모델+본구조매핑정보로 본 구조가 동일하다면 재사용이 가능하다.

실선으로 돼 있는 원은 필수로 15개가 있으며 점선은 추가적인 본으로 연결하지 않아도 상관없다. 

 

인스펙트뷰의 좌하 Head탭을 누르면 머리의 본 연결 정보를 볼수 있고 Left Hand를 누르면 손가락 관절의 정보를 볼 수도 있다.

Monster Avatar상단의 Muscles&Settings 탭을 선택하면 각 관절의 정상적인 동작 여부를 슬라이더로 조절하며 시각적으로 확인할 수 있다. Pre-Muscle Settings에서  각 관절의 회전 범위를 설정할 수 있다.

맨밑의 Revert를 눌러 만질걸 복구시키고 Done을 눌러 변경을 마친다. 다시  인스펙터창이 보이고 Animation탭을 선택한다. idel, walk,attack 애니메이션만 Loop Time을 체크한후 Apply버튼을 누른다

애니메이션 리타게킹

맥사모 www.maxamo.com  사이트에 가입후 무료 에니메이션을 가져오자.

사이트에서 gangnam으로 검색후 싸이의 강남스타일 애니메이션을 가져오자.

지금 작업중인 적 캐릭터 모델파일(Monster.fbx)를 업로드해 애니메이션을 테스트도 할 수 있다.

Resources/Animations 폴더에 미리 다운받은 monster@Gangnam Style.fbx파일이 있으니 이걸 프로젝트뷰의 07.Animation폴더로 끌어온다.

메카님 리타게팅을 사용하려면 적용하려는 모델과 애니메이션 클립의 Type이 둘다 Humanoid로 설정해야 한다. 따라서 monster@Gangnam Style.fbx도 Rig탭에서 타입을 Humanoid로 바꾸고 apply한다. Animation탭을 누루고  Loop Time도 체크후 Apply해 준다.

같은 Animation탭 맨밑의 프리뷰창에 05.Model/Monster폴더의 Monster를 끌어다 놓고 Play시켜보면 춤을 추는걸 확인할수 있다.

Animator 컴포넌트

Monster모델을 씬뷰또는 하이라키로 끌어다 Player옆에 놓는다. 

씬뷰에 추가된  Monster는 기본적으로 Animator가 추가되어 있다. 3D모델의 애니메이션  타입을 메카님으로 설정하면 Animator컴포넌트가 기본적으로 추가된다.  앞서 제작한 Player모델은 애니메이션 타입을 레거시로 설정했기 때문에 Animation 컴포넌트가 추가된것이다. Animator는 Animation을 컨트롤 하는 컴포넌트로 레가시는 Animation컴포넌트가 그 역활을 한다.

Animation컴포넌트의 각 속성의 기능은 다음과 같다.

프로퍼티: 기능:
Controller 캐릭터에 부여된 애니메이터 컨트롤러.
Avatar 캐릭터의 Avatar입니다(애니메이터가 휴머노이드 캐릭터를 애니메이션화하기 위해 사용되는 경우).
Apply Root Motion 캐릭터의 포지션과 회전을 애니메이션 자체에서 제어할지 또는 스크립트에서 제어할지 선택합니다.
Update Mode 애니메이터 업데이트를 언제 할지, 어떤 타임스케일을 사용할지 선택할 있게 해줍니다.
  Normal 애니메이터는 업데이트 호출과 싱크되어 업데이트되며, 애니메이터의 속도는 현재의 타임스케일과 일치합니다. 타임스케일이 느려지면 애니메이션도 따라서 느려집니다.
  Animate Physics 애니메이터는 FixedUpdate 호출에 따라(, 물리 시스템과 동시에) 업데이트됩니다. 리지드바디 오브젝트를 밀어 움직일 있는 캐릭터와 같이 물리 상호작용을 하는 오브젝트의 모션을 애니메이션화하는 경우에 모드를 사용해야 합니다.
  스케일되지 않은 시간(Unscaled Time) 애니메이터는 업데이트 호출에 따라 업데이트되지만 애니메이터의 속도는 현재의 타임스케일을 무시하고 항상 100% 속도로 애니메이션화합니다. 게임플레이를 일시정지하거나 특수한 효과를 위해 수정된 타임스케일을 사용하면서 GUI 시스템을 노멀 속도로 애니메이션화하는 경우에 유용합니다.
Culling Mode 애니메이션에 사용할 있는 컬링 모드.
  항상 애니메이션화(Always Animate) 항상 애니메이션화하고 오프스크린일 경우에도 컬링을 하지 않습니다.
  업데이트 트랜스폼 컬링(Cull Update Transforms) 렌더러가 보이지 않을 리타겟, IK 트랜스폼의 쓰기를 비활성화합니다.
  완전히 컬링(Cull Completely) 렌더러가 보이지 않을 애니메이션을 완전히 비활성화합니다.

 

애니메이터 컨트롤러

애니메이터 컨트롤러를 사용하여 캐릭터나 오브젝트의 애니메이션 클립 세트와 관련 애니메이션 전환을 정렬하고 관리할 수 있습니다. 대부분의 경우, 여러 애니메이션을 이용하여 게임 내에서 특정 상황이 일어났을 때 에셋을 바꿔가면서 사용합니다. 예를 들어 스페이스바를 누를 때마다 걷기 애니메이션 클립에서 점프 애니메이션 클립으로 전환할 수 있습니다. 하지만 애니메이션 클립이 하나만 있을 때에도 이 클립을 게임 오브젝트에 사용하기 위해서는 애니메이터 컨트롤러에 배치해야 합니다.

애니메이터 컨트롤러는 그 안에서 사용되는 애니메이션 클립에 대한 레퍼런스를 포함하고 있으며, 애니메이션 클립과 전환의 순서도 또는 Unity 에디터 내에서 비주얼 프로그래밍 언어로 작성된 간단한 프로그램이라고 생각할 수 있는 상태 머신 을 사용하여 다양한 애니메이션 클립과 각 클립 사이의 전환을 관리합니다. 상태 머신에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

프로젝트뷰의 07.Animation폴더 하위에 Controllers라는 서브폴더를 생성하고 Assts>Create>Animator Controller를 선택해 이름을 MonsterAnim으로 지정한다.

생성한 MonsterAnim을 더블클릭하면 다음 같은 애니메이터뷰가 열린다.

BaseLayer에 3개의 State가 있다.

Any State : 어떤 상태라도 조건이 맞으면 다른 스테이트로 분기시킬 수 있다.

Entry : 시작 스테이트 최초 진입점

Exit : 종료 스테이트 모든 스테이트가 종료되는 마지막 스테이트

Models>Monster폴더의 Monster를 눌러 삼각형 아이콘의 Idle 을 끌어다  애니메이터뷰에 끌어다 놓으면

다음 처럼된다 추가된 Idle은 Default를 의미하는 주황색을 띤다. 다른 스테이트도 우클릭으로 Default State로 변경가능.

하이라키뷰의 Monster를 선택하고 Animation>Controller폴더의 MonsterAnim를 Animator컴포넌트의 Controller에 끌어다 놓는다.

실행해보면 Idel에 파란색 진행 상태가바 채워지는 모습을 확인할 수 있다. 한번만 실행된다면 Loop를 체크해야한다.

이제 Walk도 끌어다 놔보자 아이들을 우클릭후 Maker Transition선택하면 화살표가 나오는데 이걸 walk에 연결한다. 이선을 transition이라고 한다.

애니메이터의 좌상 Parameters 탭을 선택하고 +를 눌러 Bool을 선택하고 이름을 IsTrace로 지정한다.

새로만든 IsTrace변수가 true면 walk, false면 Idel로 되돌아오게 해보겠다.

Idel과 walk사이의 transition을 선택하고 인스펙터뷰의 Conditions속성에 +를 눌러 조건을 추가하면 IsTrue가 true로 되어있다.   HasExitTime은 언체크한다. 이건 현재실행중인 애니메이션을 즉시 멈추고 실행한다는 뜻이다.

Idle>Walk, Walk>Idle transition 설정

이번에는 walk를 우클릭한후 transition을Idel에 연결해주고 transition을 눌러 IsTrace 를 fasle로 한다. 역시 HasExitTime은 언체크한다.

Play를 누르고 게임뷰에서 Animator탭으로 전환하고 왼쪽의 IsTrace를 체크/언체크해보면  스테이지가 변화한다.

 

+ Recent posts