유니티 심화

절대강좌 유니티 - 혈흔효과, 몬스터 공격효과, 델리게이트, 죽으면 강남스타일, 본 구조, 충돌감지, 몬스터 사망

다모아 2023. 8. 24. 12:56

정리 -> 코드 -> 생각 -> 정답

 

1. 정리하면서 사용할 api 찾아보고

 

2. 코드 작성해보고

 

3. 모르면 생각해보고

 

4. 그래도 모르면 정답을 본다


MonsterController

 


몬스터 공격효과

 

PlayerController

 


몬스터의 사망처리
1. Animator.StringToHash를 이용해 파라미터 해시값 추출한다

2. 몬스터에게 생명 변수를 추가한다.

3. 몬스터 상태가 DIE일 때 코루틴 종료 yield break

4. 사망상태일 때 사망 애니메이션과 추적 정지 SetTrigger

5. 총 맞으면 몬스터의 피 차감시킴

6. 몬스터의 콜리더도 해제시킴

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

public class MonsterController : MonoBehaviour
{
    public enum eState
    {
        Idle, Trace, Attack, Die
    }

    [SerializeField]
    private NavMeshAgent agent;
    private Transform playerTrans;

    [SerializeField]
    private float attackRange = 2f; //공격사거리
    [SerializeField]
    private float traceRange = 10f; //추적사거리

    [SerializeField]
    private eState state; //상태 저장 변수

    private bool IsDie = false;
    private float hp = 10.0f;
    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()
    {
        //구독
        PlayerController.OnPlayerDie += this.OnPlayerDie;
    }

    //까먹고 구독취소 안했으면 이벤트가 쌓이고 OnPlayerDie가 여러번 호출 될 수 있음

    private void OnDisable()
    {
        //구독 취소 해야함
        PlayerController.OnPlayerDie -= this.OnPlayerDie;
    }

    // Start is called before the first frame update
    void Start()
    {
        //애니메이션 컴포넌트 가져오기
        this.anim = this.GetComponent<Animator>();
        //agent컴포넌트를 가져오고
        this.agent = this.GetComponent<NavMeshAgent>();
        //BloodSprayEffect 프리팹 로드
        this.bloodEffectPrefab = Resources.Load<GameObject>("BloodSprayEffect");
        ////타겟의 위치
        //GameObject playerGo = GameObject.FindWithTag("Player");
        //Transform playerTrans = playerGo.GetComponent<Transform>();
        //Vector3 playerPosition = playerTrans.position;

        this.playerTrans = GameObject.FindWithTag("Player").GetComponent<Transform>();

        //this.playerTrans = GameObject.FindWithTag("Player").transform;

        //this.agent.destination = this.playerTrans.position;

        this.StartCoroutine(this.CoCheckMonsterState());

        this.StartCoroutine(this.CoMonsterAction());
    }

    private IEnumerator CoCheckMonsterState()
    {
        while(true)
        {
            yield return new WaitForSeconds(0.3f);

            //플레이어와 거리계산
            float dis = Vector3.Distance(this.transform.position, this.playerTrans.position);
            
            if (dis <= attackRange)
            {
                //공격사거리에 들어왔는지
                this.state = eState.Attack;
                
            }
            else if(this.state == eState.Die)
            {
                yield break;
            }
            else if(dis <= this.traceRange)
            {
                //아니면 추적사거리에 들어왔는지
                this.state = eState.Trace;
            }
            else
            {
                //공격사거리에도 추적사거리에도 안들어왔는지
                this.state = eState.Idle;
            }
        }
    }

    private IEnumerator CoMonsterAction()
    {
        while (!this.IsDie)
        {
            yield return new WaitForSeconds(0.3f);

            switch(this.state)
            {
                case eState.Idle:
                    //추적 중지
                    this.agent.isStopped = true;
                    this.anim.SetBool(hashTrace, false);
                    break;
                case eState.Trace:
                    //플레이어 좌표로 이동
                    this.agent.SetDestination(this.playerTrans.position);
                    this.agent.isStopped = false;
                    this.anim.SetBool(hashTrace, true);
                    this.anim.SetBool(hashAttack, false);
                    break;
                case eState.Attack:
                    this.anim.SetBool(hashAttack, true);
                    break;
                case eState.Die:
                    this.IsDie = true;
                    //추적정지
                    this.agent.isStopped = true;
                    //사망애니메이션실행
                    this.anim.SetTrigger(hashDie);
                    //콜리더 해제
                    this.GetComponent<CapsuleCollider>().enabled = false;
                    break;
            }
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if(collision.collider.CompareTag("Bullet"))
        {
            //충돌한 총알 삭제
            Destroy(collision.gameObject);
            //피격 리액션 애니메이션 실행
            this.anim.SetTrigger(hashHit);
            //충돌지점
            Vector3 hitPoint = collision.GetContact(0).point;
            ContactPoint cp = collision.GetContact(0);
            //법선벡터
            DrawArrow.ForDebug(hitPoint, -1 * cp.normal, 5f, Color.green, ArrowType.Solid);
            //회전 값
            Vector3 dir = -1 * cp.normal;
            Quaternion rot = Quaternion.LookRotation(dir);
            this.HitDamage();
            this.ShowBloodEffect(hitPoint, rot);
        }
    }

    private void ShowBloodEffect(Vector3 hitPoint, Quaternion rot)
    {
        //프리팹 인스턴스
        GameObject blood = Instantiate<GameObject>(this.bloodEffectPrefab, hitPoint, rot, this.transform);
        Destroy(blood, 1.0f);
    }

    private void OnDrawGizmos()
    {
        if (this.state == eState.Trace)
        {
            Gizmos.color = Color.blue;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.attackRange);
        }
        else if(this.state == eState.Attack)
        {
            Gizmos.color = Color.red;
            GizmosExtensions.DrawWireArc(this.transform.position, this.transform.forward, 360, this.traceRange);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log(other.gameObject.name);
    }

    private void OnPlayerDie()
    {
        //몬스터의 상태를 체크하는 코루틴 함수를 모두 정지시킴
        StopAllCoroutines();
        //추적을 정지하고 애니메이션을 수행
        this.agent.isStopped = true;
        this.anim.SetFloat(this.hashSpeed, Random.Range(0.8f, 1.2f));
        this.anim.SetTrigger(hashPlayerDie);
    }

    private void HitDamage()
    {
        this.hp -= 1f;
        if(this.hp <= 0f)
        {
            this.state = eState.Die;
        }
    }
}

 

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

public class PlayerController : MonoBehaviour
{
    private enum eAnimState
    {
        Idle, RunB, RunF, RunL, RunR
    }

    [SerializeField]
    private float moveSpeed = 5f;
    [SerializeField]
    private float turnSpeed = 800f;

    private Animation anim;

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

    //-------------------------------------------
    private readonly float initHp = 100f; //초기 체력
    public float currHp; //현재 체력
    //-------------------------------------------

    // Start is called before the first frame update
    private IEnumerator Start()
    {
        this.currHp = this.initHp; // HP초기화
        this.anim = GetComponent<Animation>();

        this.turnSpeed = 0f;
        yield return new WaitForSeconds(0.3f);
        this.turnSpeed = 800f;
    }

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

        Vector3 moveDir = this.transform.forward * v + this.transform.right * h;

        this.transform.Translate(moveDir.normalized * this.moveSpeed * Time.deltaTime);
        this.PlayAnimation(moveDir);
        DrawArrow.ForDebug(this.transform.position, moveDir.normalized, 0.1f);

        //if(Input.GetMouseButton(0))
        //{
        //    this.transform.Rotate(Vector3.up * x * this.turnSpeed * Time.deltaTime);
        //}
        
    }

    private void PlayAnimation(Vector3 moveDir)
    {
        if(moveDir.x > 0)
        {
            //오른쪽
            this.anim.CrossFade(eAnimState.RunR.ToString(), 0.25f);
        }
        else if(moveDir.x < 0)
        {
            //왼쪽
            this.anim.CrossFade(eAnimState.RunL.ToString(), 0.25f);
        }
        else if(moveDir.z > 0)
        {
            //위
            this.anim.CrossFade(eAnimState.RunF.ToString(), 0.25f);
        }
        else if(moveDir.z < 0)
        {
            //아래
            this.anim.CrossFade(eAnimState.RunB.ToString(), 0.25f);
        }
        else
        {
            //정지
            this.anim.CrossFade(eAnimState.Idle.ToString(), 0.25f);
        }
    }
    private void OnTriggerEnter(Collider other)
    {
        if(this.currHp >= 0.1f && other.CompareTag("Punch"))
        {
            Debug.Log("펀치");
            this.currHp -= 10.0f;
            Debug.Log($"Player hp = {currHp / initHp}");

            if (currHp <= 0.0f)
            {
                this.PlayerDie();
            }
        }
    }

    private void PlayerDie()
    {
        Debug.Log("죽음");
        ////Monster 태그를 가진 모든 게임오브젝트를 가져옴 , 배열은 arr, gameObject는 Go를 붙이는게 좋다
        //GameObject[] arrMonsterGo = GameObject.FindGameObjectsWithTag("Monster");
        //foreach (GameObject monsterGo in arrMonsterGo)
        //{
        //    monsterGo.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        //}
        OnPlayerDie();
    }
}