16장. 좀비 서바이벌(내비게이션, 내비메시, 인공지능 추격 좀비, 몬스터)
@ 내비게이션 시스템
- 내비메시 : 에이전트가 걸어 다닐 수 있는 표면
- 내비메시 에이전트 : 내비메시 위에서 경로를 계산하고 이동하는 캐릭터 또는 컴포넌트
- 내비메시 장애물 : 에이전트의 경로를 막는 장애물
- 오프메시 링크 : 끊어진 내비메시 영역 사이를 잇는 연결 지점(뛰어 넘을수 있는 울타리나 올라갈 수 있는 담벼락 구현하는데 사용)
- 내비메시는 정적 오브젝트를 대상으로 생성됨. // 게임 플레이 도중에 실시간으로 생성할 수 없음.
- 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의 값은 공격 대상 위치에서 자신의 위치로 향하는 방향을 할당함. => 자신의 위치에서 공격 대상의 위치를 빼면 됨.