본문 바로가기
유니티 심화

유니티 절대강좌 06 (적 캐릭터 제작)

by 노재두내 2023. 9. 27.

오늘 할일 절대강좌 6장 끝내기


유한 상태 머신 : 적 캐릭터가 스스로 주변 환경에 적응하거나 들어오는 반응에 적절하게 반작용하도록 구현하는 것 , 일정 범위를 순찰하다가 주인공을 추적하고 사정거리 이내에 근접하면 공격하는 것이 상태머신, 또한 피격을 당해 일정 데미지가 누적되면 사망하고 소멸하는 구조이기 때문에 상태가 유한해서 유한 상태 머신이라고 부른다.

 

상태가 많아질수록 상태와 상태 간의 연결이 복잡해지고 코드의 확장과 유지보수가 어려워지는 것이 단점이다. 이 점을 개선하기 위해서 상태를 모듈화하고 계층적으로 분류하는 계층적 유한 상태머신(HFSM) 방식을 도입할 수 있다 . 또한 최근에는 행동 트리(BT)방식도 개발에 많이 적용된다.

 

메카님의 장점 중 하나인 리타깃 기능, 리타게팅은 다른 애니메이션 동작을 가져와 재사용하는 기능

 

Has Exit Time : 만약에 체크되어있다면 IsTrace 변수가 true가 되어도 현재 실행하는 Idle애니메이션을 다 수행하고 나서 Walk 애니메이션으로 전이된다. 하지만 우리는 IsTrace가 true (player가 감지되면) 바로 상태가 전이되어야하기 때문에 언체크 한다.


<내비게이션 - 적 캐릭터의 순찰 및 추척>

길 찾기 알고리즘, 가장 많이 알려진 알고리즘은 A* Pathfinding

내비메시 데이터를 미리 생성, 추척할 수 있는 영역과/ 장애물로 판단해 지나갈 수 없는 영역의 데이터를 메시로 미리 만드는 것. 또한 높은 곳에서 낮은 곳으로 연결하는 오프 메시 링크 기능을 이용해 뛰어내리는 동작을 연출할 수 있다. 이렇게 유니티 에디터 모드에서 미리 베이크(빌드)해서 내비메시를 생성한 다음 그 정보에 따라 최단 거리를 계산해 추적 또는 이동할 수 있게 한다.

 

pakage Manager-> ai 검색 -> aiNavigation -> install  , 2022 버전 이후는 static 설정할 필요 없음

bake할 객체(floor)에 NavMeshSurface 컴포넌트 추가-> Bake 버튼 누르기 

=> Player 주변에도 구멍 나 있음 

=> Player 에 Layer 지정해서 내비메시를 생성할 때 포함할 레이어에서 player는 제외하고 다시 베이크

=>그래도 자꾸 베이크가 뚫림

Player의 자식에 mesh Renderer 컴포넌트를 가진 오브젝트들이 있어서 그랬음

거기에도 layer를 player로 설정해주니까 해결됨

 

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

 

Find~ 함수는 처리속도가 느리기때문에 update에서 사용 x

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

public class MonsterController : MonoBehaviour
{
    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;

    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();

        agent.destination = playerTr.position;
    }

}

 


<적 캐릭터 상태 체크>

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

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        IDLE,TRACE,ATTACK,DIE
    }
    public eState state;
    [SerializeField]
    private float attackRange = 2.0f;
    [SerializeField]
    private float traceRange = 10.0f;
    public bool isDie = false;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();

        //agent.destination = playerTr.position;

        this.StartCoroutine(this.CheckMonsterState());
    }
    //일정한 간격으로 몬스터 상태체크
    IEnumerator CheckMonsterState()
    {
        while (!isDie)//죽지 않는 동안 상태체크, 죽으면 상태체크 x
        {
            //0.3초 동안 중지하는 동안 제어권을 메시지 루프에 양보
            yield return new WaitForSeconds(0.3f);

            float distance = Vector3.Distance(this.playerTr.position, this.monsterTr.position);

            if (distance <= attackRange)
            {
                state = eState.ATTACK;
            }
            else if (distance <= traceRange)
            {
                state = eState.TRACE;
            }
            else
            {
                state = eState.IDLE;
            }
        }
    }

    //거리 표시 원그리기
    private void OnDrawGizmos()
    {
        if (state == eState.TRACE)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward,360,this.traceRange);
        }
        else if (state == eState.ATTACK)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
    }
}
using UnityEngine;

public class GizmosExtensions
{
    private GizmosExtensions() { }

    /// <summary>
    /// Draws a wire arc.
    /// </summary>
    /// <param name="position"></param>
    /// <param name="dir">The direction from which the anglesRange is taken into account</param>
    /// <param name="anglesRange">The angle range, in degrees.</param>
    /// <param name="radius"></param>
    /// <param name="maxSteps">How many steps to use to draw the arc.</param>
    public static void DrawWireArc(Vector3 position, Vector3 dir, float anglesRange, float radius, float maxSteps = 20)
    {
        var srcAngles = GetAnglesFromDir(position, dir);
        var initialPos = position;
        var posA = initialPos;
        var stepAngles = anglesRange / maxSteps;
        var angle = srcAngles - anglesRange / 2;
        for (var i = 0; i <= maxSteps; i++)
        {
            var rad = Mathf.Deg2Rad * angle;
            var posB = initialPos;
            posB += new Vector3(radius * Mathf.Cos(rad), 0, radius * Mathf.Sin(rad));

            Gizmos.DrawLine(posA, posB);

            angle += stepAngles;
            posA = posB;
        }
        Gizmos.DrawLine(posA, initialPos);
    }

    static float GetAnglesFromDir(Vector3 position, Vector3 dir)
    {
        var forwardLimitPos = position + dir;
        var srcAngles = Mathf.Rad2Deg * Mathf.Atan2(forwardLimitPos.z - position.z, forwardLimitPos.x - position.x);

        return srcAngles;
    }
}


<적 캐릭터 행동 구현>

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

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        IDLE,TRACE,ATTACK,DIE
    }
    public eState state;
    [SerializeField]
    private float attackRange = 2.0f;
    [SerializeField]
    private float traceRange = 10.0f;
    public bool isDie = false;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    private Animator anim;

    private readonly int hashTrace = Animator.StringToHash("IsTrace");
    private readonly int hashAttack = Animator.StringToHash("IsAttack");
    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();
        this.anim = this.GetComponent<Animator>();
        //agent.destination = playerTr.position;

        this.StartCoroutine(this.CheckMonsterState());
        this.StartCoroutine(this.MonsterAction());
    }
    //일정한 간격으로 몬스터 상태체크
    IEnumerator CheckMonsterState()
    {
        while (!isDie)//죽지 않는 동안 상태체크, 죽으면 상태체크 x
        {
            //0.3초 동안 중지하는 동안 제어권을 메시지 루프에 양보
            yield return new WaitForSeconds(0.3f);

            float distance = Vector3.Distance(this.playerTr.position, this.monsterTr.position);

            if (distance <= attackRange)
            {
                state = eState.ATTACK;
            }
            else if (distance <= traceRange)
            {
                state = eState.TRACE;
            }
            else
            {
                state = eState.IDLE;
            }
        }
    }

    IEnumerator MonsterAction()
    {
        while (!isDie)
        {
            switch (state)
            {
                case eState.IDLE:
                    //추적 중지
                    this.agent.isStopped = true;
                    anim.SetBool(hashTrace,false);
                    break;
                case eState.TRACE:
                    agent.SetDestination(this.playerTr.position);
                    this.agent.isStopped = false;
                    anim.SetBool(hashTrace,true);
                    anim.SetBool(hashAttack, false);
                    break;
                case eState.ATTACK:
                    anim.SetBool(hashAttack, true);
                    break;
                case eState.DIE:
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    //거리 표시 원그리기
    private void OnDrawGizmos()
    {
        if (state == eState.TRACE)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward,360,this.traceRange);
        }
        else if (state == eState.ATTACK)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
    }
}

private readonly int hashTrace = Animator.StringToHash("IsTrace");

private readonly int hashAttack = Animator.StringToHash("IsAttack");

애니메이션 컨트롤에 정의한 파라미터는 모두 해시 테이블로 관리, 문자열로 호출하면 속도면에서 불리, 해시값을 미리 추출해 인자로 전달

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

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        IDLE,TRACE,ATTACK,DIE
    }
    public eState state;
    [SerializeField]
    private float attackRange = 2.0f;
    [SerializeField]
    private float traceRange = 10.0f;
    public bool isDie = false;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    private Animator anim;

    private readonly int hashTrace = Animator.StringToHash("IsTrace");
    private readonly int hashAttack = Animator.StringToHash("IsAttack");
    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();
        this.anim = this.GetComponent<Animator>();
        //agent.destination = playerTr.position;

        this.StartCoroutine(this.CheckMonsterState());
        this.StartCoroutine(this.MonsterAction());
    }
    //일정한 간격으로 몬스터 상태체크
    IEnumerator CheckMonsterState()
    {
        while (!isDie)//죽지 않는 동안 상태체크, 죽으면 상태체크 x
        {
            //0.3초 동안 중지하는 동안 제어권을 메시지 루프에 양보
            yield return new WaitForSeconds(0.3f);

            float distance = Vector3.Distance(this.playerTr.position, this.monsterTr.position);

            if (distance <= attackRange)
            {
                state = eState.ATTACK;
            }
            else if (distance <= traceRange)
            {
                state = eState.TRACE;
            }
            else
            {
                state = eState.IDLE;
            }
        }
    }

    IEnumerator MonsterAction()
    {
        while (!isDie)
        {
            switch (state)
            {
                case eState.IDLE:
                    //추적 중지
                    this.Idle();
                    break;
                case eState.TRACE:
                    this.Trace();
                    break;
                case eState.ATTACK:
                    this.Attack();
                    break;
                case eState.DIE:
                    this.Die();
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }
    //거리 표시 원그리기
    private void OnDrawGizmos()
    {
        if (state == eState.TRACE)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward,360,this.traceRange);
        }
        else if (state == eState.ATTACK)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
    }
    private void Idle()
    {
        this.agent.isStopped = true;
        anim.SetBool(hashTrace, false);
    }
    private void Attack()
    {
        anim.SetBool(hashAttack, true);
    }
    private void Trace()
    {
        agent.SetDestination(this.playerTr.position);
        this.agent.isStopped = false;
        anim.SetBool(hashTrace, true);
        anim.SetBool(hashAttack, false);
    }
    private void Die()
    {

    }
}

메서드로 만들기 


총 맞으면 총 맞는 애니메이션

<monsterController>

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

private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("BULLET"))
        {
            Destroy(collision.gameObject);
            anim.SetTrigger(hashHit);
        }
    }

혈흔 효과

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

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        IDLE,TRACE,ATTACK,DIE
    }
    public eState state;
    [SerializeField]
    private float attackRange = 2.0f;
    [SerializeField]
    private float traceRange = 10.0f;
    public bool isDie = false;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    private Animator anim;

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

    private GameObject bloodEffect;
    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();
        this.anim = this.GetComponent<Animator>();
        this.bloodEffect = Resources.Load<GameObject>("BloodSprayEffect");
        //agent.destination = playerTr.position;

        this.StartCoroutine(this.CheckMonsterState());
        this.StartCoroutine(this.MonsterAction());
    }
    //일정한 간격으로 몬스터 상태체크
    IEnumerator CheckMonsterState()
    {
        while (!isDie)//죽지 않는 동안 상태체크, 죽으면 상태체크 x
        {
            //0.3초 동안 중지하는 동안 제어권을 메시지 루프에 양보
            yield return new WaitForSeconds(0.3f);

            float distance = Vector3.Distance(this.playerTr.position, this.monsterTr.position);

            if (distance <= attackRange)
            {
                state = eState.ATTACK;
            }
            else if (distance <= traceRange)
            {
                state = eState.TRACE;
            }
            else
            {
                state = eState.IDLE;
            }
        }
    }

    IEnumerator MonsterAction()
    {
        while (!isDie)
        {
            switch (state)
            {
                case eState.IDLE:
                    //추적 중지
                    this.Idle();
                    break;
                case eState.TRACE:
                    this.Trace();
                    break;
                case eState.ATTACK:
                    this.Attack();
                    break;
                case eState.DIE:
                    this.Die();
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("BULLET"))
        {
            Destroy(collision.gameObject);
            anim.SetTrigger(hashHit);

            //총알 맞은 부위에 혈흔 이펙트 생성하기
            Vector3 pos = collision.GetContact(0).point;
            Quaternion rot = Quaternion.LookRotation(-collision.GetContact(0).normal);//맞은 부위의 법선벡터
            ShowBloodEffect(pos, rot);
        }
    }
    private void ShowBloodEffect(Vector3 pos,Quaternion rot)
    {
        GameObject bloodGo = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr);
        Destroy(bloodGo, 1.0f);
    }
    //거리 표시 원그리기
    private void OnDrawGizmos()
    {
        if (state == eState.TRACE)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward,360,this.traceRange);
        }
        else if (state == eState.ATTACK)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
    }
    private void Idle()
    {
        this.agent.isStopped = true;
        anim.SetBool(hashTrace, false);
    }
    private void Attack()
    {
        anim.SetBool(hashAttack, true);
    }
    private void Trace()
    {
        agent.SetDestination(this.playerTr.position);
        this.agent.isStopped = false;
        anim.SetBool(hashTrace, true);
        anim.SetBool(hashAttack, false);
    }
    private void Die()
    {

    }
}

GameObject bloodGo = Instantiate<GameObject>(bloodEffect, pos, rot, monsterTr); //monsterTr은 부모 게임오브젝트이다. 따라서 생성한 혈한 프리팹의 복사본은 Monster 하위로 생성된다.


<player가 데미지를 입어 생명력이 줄어듬>

몬스터 양손에 rigidbody와 collider 컴포넌트 추가하기

 

*FPS나 TPS 게임을 개발할 때 주인공 캐릭터는 Character Controller 컴포넌트를 추가해 사용한다. capsule collider 와 rigidbody 컴포넌트의 역할을 하며, 벽에 부딪혔을 때 떨림 현상이나 외부의 힘으로 밀리는 현상이 발생하지 않는다. 

플레이어 사망이 두번 출력됨


몬스터의 몸과 양손이 충돌 계속 감지함=> layer추가 몸에는 monsterbody 손에는 monsterpunch

레이어가 교차하는 부분 언체크

-> 아군과 적군이 있는 게임에서 유용함


<몬스터 공격 중지>


사용자 정의 이벤트( delegate) 대리자,  몬스터 사망

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

public class PlayerCtrl : MonoBehaviour
{
    public enum eAnimState
    {
        Idle,
        RunB,
        RunF,
        RunL,
        RunR
    }
    private Transform tr;
    public float moveSpeed = 10.0f;
    public float turnSpeed = 80.0f;
    private Animation anim;
    private bool isDown;
    private Vector3 downPosition;
    private float initHp = 100.0f;
    //현재 생명값
    public float currHp;

    //델리게이트 선언
    public delegate void PlayerDieHandler();
    //이벤트 선언
    public static event PlayerDieHandler OnPlayerDie;
    // Start is called before the first frame update
    void Start()
    {
        this.tr = GetComponent<Transform>();
        this.anim = this.GetComponent<Animation>();
        //방법1
        //anim.Play("Idle");
        //방법2
        this.anim.clip = this.anim.GetClip(eAnimState.Idle.ToString());
        this.anim.Play();
        this.currHp = this.initHp;
    }

    // Update is called once per frame
    void Update()
    {
        //수평
        float h = Input.GetAxis("Horizontal");
        //수직
        float v = Input.GetAxis("Vertical");
        float r = Input.GetAxis("Mouse X");

        //Debug.Log("h="+h);
        //Debug.Log("v="+ v);

        Vector3 moveDir = (Vector3.forward * v) + (Vector3.right * h);
        this.tr.Translate(moveDir.normalized* Time.deltaTime * moveSpeed);//v는 키보드 입력값
        //tr.Rotate(Vector3.up * turnSpeed * Time.deltaTime * r);

        //마우스 드래그한만큼 회전하기
        if (Input.GetMouseButtonDown(0))
        {
            this.isDown = true;
            this.downPosition = Input.mousePosition;
        }
        else if (Input.GetMouseButtonUp(0))
        {
            this.isDown = false;
        }

        if (this.isDown)//isDown==true
        {
            if (this.downPosition != Input.mousePosition)
            {
                var rotDir = Mathf.Sign(r);//부호반환함수 
                this.transform.Rotate(Vector3.up, rotDir * Time.deltaTime * this.turnSpeed);//y축방향으로 회전
                this.downPosition = Input.mousePosition;
            }
        }

        this.PlayerAnim(h, v);
    }

    void PlayerAnim(float h,float v)
    {
        if (v >= 0.1f)
        {
            anim.CrossFade("RunF", 0.25f);//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);
        }
    }
    private void OnTriggerEnter(Collider other)
    {
        if (currHp >= 0.0f && other.CompareTag("PUNCH"))
        {
            currHp -= 10.0f;
            Debug.LogFormat($"hp:{currHp/this.initHp}");

            if (currHp <= 0.0f)
            {
                Debug.Log("플레이어 죽음");
                PlayerDie();
            }
        }
    }
    private void PlayerDie()
    {
        Debug.Log("플레이어 사망했습니다");
        //플레이어 사망하면 monster태그가 달린 모든 게임오브젝트들을 찾고
        //GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
        //모든 몬스터의 onPlayerDie 함수를 순차적으로 호출한다. 
        //foreach(GameObject monster in monsters)
        //{
        //monster 게임 오브젝트 스크립트에 OnPlayDie함수가 있다면 실행, SendMessageOptions.DontRequireReceiver은 호출한 함수가 없더라도 함수가 없다는 메시지를 반환받지 않겠다는 옵션, 빠른 실행을 위해 사용
        //monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        //}
        OnPlayerDie();
    }
}
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        IDLE,TRACE,ATTACK,DIE
    }
    public eState state;
    [SerializeField]
    private float attackRange = 2.0f;
    [SerializeField]
    private float traceRange = 10.0f;
    public bool isDie = false;
    private int monsterHp = 100;

    private Transform monsterTr;
    private Transform playerTr;
    private NavMeshAgent agent;
    private Animator anim;

    private readonly int hashTrace = Animator.StringToHash("IsTrace");
    private 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 readonly int hashDie = Animator.StringToHash("Die");

    private GameObject bloodEffectPrefab;

    //이벤트 연결
    private void OnEnable()
    {
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie;
    }
    //이벤트 해지
    private void OnDisable()
    {
        PlayerCtrl.OnPlayerDie -= this.OnPlayerDie;
    }
    // Start is called before the first frame update
    void Start()
    {
        this.monsterTr = this.gameObject.GetComponent<Transform>();
        this.playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
        this.agent = this.GetComponent<NavMeshAgent>();
        this.anim = this.GetComponent<Animator>();
        this.bloodEffectPrefab = Resources.Load<GameObject>("BloodSprayEffect");
        //agent.destination = playerTr.position;

        this.StartCoroutine(this.CheckMonsterState());
        this.StartCoroutine(this.MonsterAction());
    }
    //일정한 간격으로 몬스터 상태체크
    IEnumerator CheckMonsterState()
    {
        while (!isDie)//죽지 않는 동안 상태체크, 죽으면 상태체크 x
        {
            //0.3초 동안 중지하는 동안 제어권을 메시지 루프에 양보
            yield return new WaitForSeconds(0.3f);

            //죽었으면 여기서 코루틴 종료 
            if (state == eState.DIE) yield break;

            float distance = Vector3.Distance(this.playerTr.position, this.monsterTr.position);

            if (distance <= attackRange)
            {
                state = eState.ATTACK;
            }
            else if (distance <= traceRange)
            {
                state = eState.TRACE;
            }
            else
            {
                state = eState.IDLE;
            }
        }
    }

    IEnumerator MonsterAction()
    {
        while (!isDie)
        {
            switch (state)
            {
                case eState.IDLE:
                    //추적 중지
                    this.Idle();
                    break;
                case eState.TRACE:
                    this.Trace();
                    break;
                case eState.ATTACK:
                    this.Attack();
                    break;
                case eState.DIE:
                    this.Die();
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.CompareTag("BULLET"))
        {
            Destroy(collision.gameObject);
            anim.SetTrigger(hashHit);

            //총알 맞은 부위에 혈흔 이펙트 생성하기
            Vector3 pos = collision.GetContact(0).point;
            Quaternion rot = Quaternion.LookRotation(-collision.GetContact(0).normal);//맞은 부위의 법선벡터
            ShowBloodEffect(pos, rot);

            this.monsterHp -= 10;
            if (monsterHp <= 0)
            {
                state = eState.DIE;
            }
        }
    }
    private void ShowBloodEffect(Vector3 pos,Quaternion rot)
    {
        GameObject bloodGo = Instantiate<GameObject>(bloodEffectPrefab, pos, rot, monsterTr);
        Destroy(bloodGo, 1.0f);
    }
    //거리 표시 원그리기
    private void OnDrawGizmos()
    {
        if (state == eState.TRACE)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward,360,this.traceRange);
        }
        else if (state == eState.ATTACK)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
    }
    private void Idle()
    {
        this.agent.isStopped = true;
        anim.SetBool(hashTrace, false);
    }
    private void Attack()
    {
        anim.SetBool(hashAttack, true);
    }
    private void Trace()
    {
        agent.SetDestination(this.playerTr.position);
        this.agent.isStopped = false;
        anim.SetBool(hashTrace, true);
        anim.SetBool(hashAttack, false);
    }
    private void Die()
    {
        isDie = true;
        this.agent.isStopped = true;
        anim.SetTrigger(hashDie);
        //죽으면 총을 맞아도 혈흔 효과 일어나지 않음 
        GetComponent<CapsuleCollider>().enabled = false;
    }
    void OnPlayerDie()
    {
        StopAllCoroutines();//몬스터의 상태를 체크하는 코루틴 함수를 모두 정지시킴

        agent.isStopped = true;
        anim.SetFloat(hashSpeed, Random.Range(0.8f, 1.2f));//몬스터 강남스타일 애니메이션 속도 조절
        anim.SetTrigger(hashPlayerDie);
    }
}