카테고리 없음

16장. 좀비 서바이벌(내비게이션, 내비메시, 인공지능 추격 좀비, 몬스터)

MJ_119 2024. 4. 17. 23:08

@ 내비게이션 시스템

 - 내비메시 : 에이전트가 걸어 다닐 수 있는 표면

 - 내비메시 에이전트 : 내비메시 위에서 경로를 계산하고 이동하는 캐릭터 또는 컴포넌트

 - 내비메시 장애물 : 에이전트의 경로를 막는 장애물

 - 오프메시 링크 : 끊어진 내비메시 영역 사이를 잇는 연결 지점(뛰어 넘을수 있는 울타리나 올라갈 수 있는 담벼락 구현하는데 사용)

 

- 내비메시는 정적 오브젝트를 대상으로 생성됨. // 게임 플레이 도중에 실시간으로 생성할 수 없음.

 

- Navigation (Obsolete) 선택 후 베이크 하기

 

- 좀비 모델을 넣고 애니메이터 컨트롤러를 할당하고 루트 모션 사용을 해제함.

 

 

 - Box 콜라이더 : 좀비 공격 범위

 - 캡슐 콜라이더 : 좀비 물리적인 표면

 

 

@ Zombie 스크립트

 - LivingEntity에서 제공하는 기본 생명체 기능

 - 외부에서 Zombie 초기 능력치 셋업 기능

 - 주기적으로 목표 위치를 찾아 경로 갱신

 - 공격 받았을때 피탄 효과 재생

 - 트리거 콜라이더를 이용해 감지된 상대방을 공격

 - 사망 시 추적 중단

 - 사망 시 사망 효과 재생

 

 - LivingEntity를 상속하기 때문에 zombie는 체력을 가지고 죽거나 살 수 있으며 데미지를 받을 수 있음.

 

- 좀비는 lastAttackTime(가장 최근에 공격을 실행한 시점)에서 timeBetAttack(공격 사이의 시간 간격) 이상의 시간이 지나야 다음 공격을 할 수 있음.

 

using System.Collections;
using UnityEngine;
using UnityEngine.AI; // AI, 내비게이션 시스템 관련 코드 가져오기

// 좀비 AI 구현
public class Zombie : LivingEntity
{
    public LayerMask whatIsTarget; // 추적 대상 레이어

    private LivingEntity targetEntity; // 추적 대상
    private NavMeshAgent navMeshAgent; // 경로 계산 AI 에이전트

    public ParticleSystem hitEffect; // 피격 시 재생할 파티클 효과
    public AudioClip deathSound; // 사망 시 재생할 소리
    public AudioClip hitSound; // 피격 시 재생할 소리

    private Animator zombieAnimator; // 애니메이터 컴포넌트
    private AudioSource zombieAudioPlayer; // 오디오 소스 컴포넌트
    private Renderer zombieRenderer; // 렌더러 컴포넌트

    public float damage = 20f; // 공격력
    public float timeBetAttack = 0.5f; // 공격 간격
    private float lastAttackTime; // 마지막 공격 시점

    // 추적할 대상이 존재하는지 알려주는 프로퍼티
    private bool hasTarget {
        get
        {
            // 추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
            if (targetEntity != null && !targetEntity.dead)
            {
                return true;
            }

            // 그렇지 않다면 false
            return false;
        }
    }

    private void Awake() {
        // 초기화
        // 게임 오브젝트로부터 사용할 컴포넌트 가져오기
        navMeshAgent = GetComponent<NavMeshAgent>();
        zombieAnimator = GetComponent<Animator>();
        zombieAudioPlayer = GetComponent<AudioSource>();
        
        // 렌더러 컴포넌트는 자식오브젝트에 있으므로 GetComponentInChildren() 사용
        zombieRenderer = GetComponentInChildren<Renderer>();
    }

    // 좀비 AI의 초기 스펙을 결정하는 셋업 메서드
    public void Setup(ZombieData zombieData) {
        // 체력 설정
        startingHealth = zombieData.health;
        health= zombieData.health;

        // 공격력 설정
        damage = zombieData.damage;

        // 내비메시 에이전트의 이동 속도 설정
        navMeshAgent.speed = zombieData.speed;

        // 렌더러가 사용 중인 머티리얼의 컬러를 변경, 외형색 변함
        zombieRenderer.material.color = zombieData.skinColor;
    }

    private void Start() {
        // 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작
        StartCoroutine(UpdatePath());
    }

    private void Update() {
        // 추적 대상의 존재 여부에 따라 다른 애니메이션 재생
        zombieAnimator.SetBool("HasTarget", hasTarget);
    }

    // 주기적으로 추적할 대상의 위치를 찾아 경로 갱신
    private IEnumerator UpdatePath() {
        // 살아 있는 동안 무한 루프
        while (!dead)
        {
            if (hasTarget)
            {
                // 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
                navMeshAgent.isStopped = false;
                navMeshAgent.SetDestination(targetEntity.transform.position);
            }
            else
            {
                // 추적 대상 없음 : AI 이동 중지
                navMeshAgent.isStopped = true;

                // 20유닛의 반지름을 가진 가상의 구를 그렸을 때 구와 겹치는 모든 콜라이더를 가져옴
                // 단, whatIsTarget 레이어를 가진 콜라이더만 가져오도록 필터링
                Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

                // 모든 콜라이더를 순회하면서 살아있는 LivingEntity 찾기
                for ( int i =0; i < colliders.Length; i++ )
                {
                    // 콜라이더로부터 LivingEntity 컴포넌트 가져오기
                    LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                    // LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아있다면
                    if ( livingEntity != null && !livingEntity.dead)
                    {
                        // 추적 대상을 해당 LivingEntity로 설정
                        targetEntity = livingEntity;

                        // for 문 정지
                        break;
                    }
                }
            }

            // 0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }

    // 데미지를 입었을 때 실행할 처리
    public override void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        
        // 아직 사망하지 않은 경우에만 피격 효과 재생
        if (!dead)
        {
            // 공격받은 지점과 방향으로 파티클 효과 재생
            hitEffect.transform.position = hitPoint;
            hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal);
            hitEffect.Play();

            // 피격 효과음 재생
            zombieAudioPlayer.PlayOneShot(hitSound);
        }
        
        // LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage, hitPoint, hitNormal);
    }

    // 사망 처리
    public override void Die() {
        // LivingEntity의 Die()를 실행하여 기본 사망 처리 실행
        base.Die();

        // 다른 AI를 방해하지 않도록 자신의 모든 콜라이더를 비활성화
        Collider[] zombieColliders = GetComponents<Collider>();
        for ( int i = 0; i < zombieColliders.Length; ++i )
        {
            zombieColliders[i].enabled = false;
        }

        // AI 추격을 중지하고 내비메시 컴포넌트 비활성화
        navMeshAgent.isStopped = true;
        navMeshAgent.enabled = false;

        // 사망 애니메이션 재생
        zombieAnimator.SetTrigger("Die");

        // 사망 효과음 재생
        zombieAudioPlayer.PlayOneShot(deathSound);
    }

    private void OnTriggerStay(Collider other) {
        // 트리거 충돌한 상대방 게임 오브젝트가 추적 대상이라면 공격 실행

        // 자신이 사망하지 않고, 최근 공격시점에서 timeBetAttack 이상 시간이 지났다면 공격 가능
        if (!dead && Time.time >= lastAttackTime + timeBetAttack)
        {
            // 상대방의 LivingEntity 타입 가져오기 시도
            LivingEntity attackTarget = other.GetComponent<LivingEntity>();

            // 상대방의 LivingEntity가 자신의 추적 대상이라면 공격 실행
            if ( attackTarget != null && attackTarget == targetEntity)
            {
                // 최근 공격 시간 갱신
                lastAttackTime = Time.time;

                // 상대방의 피격 위치와 피격 방향을 근삿값으로 계산
                Vector3 hitPoint = other.ClosestPoint(transform.position); // 피격 위치
                Vector3 hitNormal = transform.position - other.transform.position; // 피격 방향

                // 공격 실행
                attackTarget.OnDamage(damage, hitPoint, hitNormal);
            }
        }
    }
}

 

 

 

- hasTarget 프로퍼티

 : set 접근자가 없으므로 임의로 값을 할당할 수 없음, 읽는 것만 가능

 

- GetComponentInChildren() : 자식 오브젝트에서 컴포넌트를 찾는 메서드.

 

 

@ UpdatePath()

 : 추적할 대상의 갱신된 위치를 일정 주기로 파악하고, 인공지능의 목적지를 재설정하는 코루틴 메서드

 무한루프를 사용함. // 코루틴을 사용하면 무한루프 회차 사이에 휴식시간을 삽입하여 에러없이 구현 가능.

 

- 내비메시 에이전트 컴포넌트는 이동 중단 여부를 나타내는 isStopped 필드와 목표 위치를 입력받아 이동 경로를 갱신하는 SetDestination() 메서드를 가지고 있음.

 

- Physics.OverlapSphere() 메서드 : 중심 위치와 반지름을 입력받아 가상의 구를 그리고, 구에 겹치는 모든 콜라이더를 반환함. // 세번째 값으로 레이어 마스크를 입력하여 특정 레이어만 감지하게 할 수 있음.

 

@ OnDamage()

 : 좀비가 피격당한 파티클 효과를 재생하기 전에 파티클 효과의 위치와 회전을 변경해야 함.

 위치 : 좀비가 몸에 공격받은 지점

 회전 : 공격이 날아온 방향(피격 맞은 방향)

 

 파티클 위치와 회전을 피격당한 부위(위치)와 방향에 맞게 수정해준 다음 파티클 효과를 재생시킴.

 

 - Quaternion.LookRotation() : 방향벡터를 입력받아 해당 방향을 바라보는 쿼터니언 회전값을 반환함.

 

 @ Die() 메서드

 : GetComponents() 메서드 => 게임오브젝트에서 주어진 타입의 모든 컴포넌트를 찾아 배열로 반환함. // 좀비 오브젝트가 2개의 콜라이더를 사용했기 때문에 사용함.

 

- navMeshAgent.isStopped = true;

- navMeshAgent.enabled = false; 

 이동정지와 컴포넌트 비활성화 둘다 하는 이유는 비활성화를 안하면 다른 좀비 AI가 사망한 좀비AI의 시체를 넘어가지 못하고 피해 다니기 때문임.

 

 

@ OnTriggerStay()

 : 충돌한 상대방 오브젝트가 자신이 공격하려는 대상이 맞는지 체크하고, 맞다면 상대방을 공격함.

 

- 상대방 오브젝트가 LivingEntity 타입의 컴포넌트를 가지고 있다면 해당 컴포넌트가 attackTarget에 성공적으로 할당.

 

- 단순히 트리거 콜라이더와 다른 콜라이더가 겹쳤는지로 공격대상을 감지했기 때문에 레이캐스트를 사용한 총과 달리 구체적인 타격 위치와 방향이 존재하지 않음.

 

- 콜라이더 컴포넌트의 ClosestPoint() 메서드는 콜라이더 표면 위의 점 중 특정 위치와 가장 가까운 점을 반환.

 

- hitNormal의 값은 공격 대상 위치에서 자신의 위치로 향하는 방향을 할당함. => 자신의 위치에서 공격 대상의 위치를 빼면 됨.