게임 개발 공부 기록
29일차 - AI Navigation
00lwt
2025. 3. 5. 20:48
▶▷ AI Navigation
인공지능이 게임이나 시뮬레이션 등 가상 환경에서 캐릭터나 NPC가 지형, 장애물, 목표 지점 등을 고려하여 지능적으로 적절한 경로를 생성하고 이동하는데 사용
- Navigation Mesh
- 3D 공간을 그리드로 나누어 이동 가능한 지역과 장애물이 있는 지역을 구분하는 매쉬
- 캐릭터가 이동할 수 있는 영역과 이동할 수 없는 영역을 정의하고 이를 기반으로 경로를 계산
2. Pathfinding
- 캐릭터의 현재 위치에서 목표 지점까지 가장 적절한 경로를 찾는 알고리즘
3. Steering Behavior
- 캐릭터나 NPC가 경로를 따라 이동할 때 자연스러운 동작을 구현하기 위해 사용
- 동적으로 캐릭터의 이동 방향과 속력을 조정하여 부드럽고 현실적인 이동을 시뮬레이션함
4. Obstacle Avoidance
- 캐릭터가 이동 중에 장애물과 충돌하지 않도록 하는 기술
- 각종 센서나 알고리즘을 사용하여 장애물을 감지하고 피하는 동작 수행
5. Local Avoidance (근접 회피)
- 여러 캐릭터나 NPC가 서로 충돌하지 않도록 하는 기술
- 캐릭터들 사이의 거리를 유지하거나 회피 동작을 수행하여 서로 부딪히지 않도록 함.
▶▷ 적용
적 오브젝트에 NavMeshAgent 컴포넌트 추가
적 스크립트 작성 (세부 내용은 주석으로 작성)
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
// NPC의 AI 상태 정의: 대기, 배회, 공격
public enum AIState
{
Idle, // 대기 상태
Wandering, // 배회 상태
Attacking // 공격 상태
}
public class NPC : MonoBehaviour, IDamagable
{
[Header("Stat")]
public int health; // 체력
public float walkSpeed; // 걷기 속도
public float runSpeed; // 달리기 속도
public ItemData[] dropOnDeath; // 사망 시 떨어지는 아이템
[Header("AI")]
private NavMeshAgent agent; // 네비게이션 에이전트
public float detectDistance; // 플레이어 탐지 거리
private AIState aiState; // 현재 AI 상태
[Header("Wandering")]
public float minWanderDistance; // 최소 배회 거리
public float maxWanderDistance; // 최대 배회 거리
public float minWanderWaitTime; // 최소 대기 시간
public float maxWanderWaitTime; // 최대 대기 시간
[Header("Combat")]
public int damage; // 공격력
public float attackRate; // 공격 간격
private float lastAttackTime; // 마지막 공격 시간 기록
public float attackDistance; // 공격 가능 거리
private float playerDistance; // 플레이어와의 거리
public float fieldOfView = 120f; // 시야각
private Animator animator; // 애니메이터 컴포넌트
private SkinnedMeshRenderer[] meshRenderers; // 모델의 메쉬 렌더러 배열
private void Awake()
{
// 컴포넌트 초기화
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
meshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
}
void Start()
{
// 시작시 배회 상태로 전환
SetState(AIState.Wandering);
}
void Update()
{
// 플레이어와의 거리 계산
playerDistance = Vector3.Distance(transform.position, CharacterManager.Instance.Player.transform.position);
// 이동 여부에 따른 애니메이터 파라미터 설정
animator.SetBool("Moving", aiState != AIState.Idle);
// 현재 상태에 따른 업데이트 처리
switch (aiState)
{
case AIState.Idle:
case AIState.Wandering:
PassiveUpdate(); // 대기 및 배회 상태 처리
break;
case AIState.Attacking:
AttackingUpdate(); // 공격 상태 처리
break;
}
}
// 상태 변경 메서드
public void SetState(AIState state)
{
aiState = state;
// 상태에 따라 속도와 이동 제어 설정
switch (aiState)
{
case AIState.Idle:
agent.speed = walkSpeed;
agent.isStopped = true; // 정지
break;
case AIState.Wandering:
agent.speed = walkSpeed;
agent.isStopped = false; // 이동 시작
break;
case AIState.Attacking:
agent.speed = runSpeed;
agent.isStopped = false; // 추격 중
break;
}
// 애니메이터 재생 속도를 이동 속도에 맞춰 조정
animator.speed = agent.speed / walkSpeed;
}
// 대기 및 배회 상태 업데이트
void PassiveUpdate()
{
// 배회 상태에서 목적지에 도착했을 때 대기 상태로 전환 후 새로운 배회 위치로 이동 예약
if (aiState == AIState.Wandering && agent.remainingDistance < 0.1f)
{
SetState(AIState.Idle);
Invoke("WanderToNewLocation", Random.Range(minWanderWaitTime, maxWanderWaitTime));
}
// 플레이어가 감지 거리 내에 들어오면 공격 상태로 전환
if (playerDistance < detectDistance)
{
SetState(AIState.Attacking);
}
}
// 새로운 배회 위치로 이동하는 메서드
void WanderToNewLocation()
{
// 현재 상태가 Idle이 아니면 실행하지 않음
if (aiState != AIState.Idle) return;
SetState(AIState.Wandering);
agent.SetDestination(GetWanderLocation());
}
// 배회할 새로운 위치를 반환
Vector3 GetWanderLocation()
{
NavMeshHit hit;
// 랜덤한 방향과 거리를 이용하여 위치 샘플링
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * Random.Range(minWanderDistance, maxWanderDistance)), out hit, maxWanderDistance, NavMesh.AllAreas);
int i = 0;
// 플레이어와 너무 가까운 위치는 피함
while(Vector3.Distance(transform.position, hit.position) < detectDistance)
{
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * Random.Range(minWanderDistance, maxWanderDistance)), out hit, maxWanderDistance, NavMesh.AllAreas);
i++;
if (i == 30) break; // 무한루프 방지
}
return hit.position;
}
// 공격 상태 업데이트
void AttackingUpdate()
{
// 플레이어가 공격 범위 내에 있고 시야에 들어오면 공격 실행
if (playerDistance < attackDistance && IsPlayerInFieldOfView())
{
agent.isStopped = true; // 공격 시 이동 정지
if (Time.time - lastAttackTime > attackRate)
{
lastAttackTime = Time.time;
// 플레이어에게 피해 주기
CharacterManager.Instance.Player.controller.GetComponent<IDamagable>().TakePhysicalDamage(damage);
animator.speed = 1;
animator.SetTrigger("Attack");
}
}
else
{
// 플레이어가 여전히 감지 범위 내에 있을 때 플레이어 추격
if (playerDistance < detectDistance)
{
agent.isStopped = false;
NavMeshPath path = new NavMeshPath();
if(agent.CalculatePath(CharacterManager.Instance.Player.transform.position, path))
{
agent.SetDestination(CharacterManager.Instance.Player.transform.position);
}
else
{
// 경로 계산 실패 시 배회 상태로 전환
agent.SetDestination(transform.position);
agent.isStopped = true;
SetState(AIState.Wandering);
}
}
else
{
// 플레이어가 감지 범위를 벗어나면 배회 상태로 전환
agent.SetDestination(transform.position);
agent.isStopped = true;
SetState(AIState.Wandering);
}
}
}
// 플레이어가 시야에 들어왔는지 확인
bool IsPlayerInFieldOfView()
{
Vector3 directionToPlayer = CharacterManager.Instance.Player.transform.position - transform.position;
float angle = Vector3.Angle(transform.forward, directionToPlayer);
return angle < fieldOfView * 0.5f;
}
// 물리 피해를 입는 메서드
public void TakePhysicalDamage(int damage)
{
health -= damage;
if(health <= 0)
{
Die(); // 체력이 0 이하이면 사망 처리
}
StartCoroutine(DamageFlash()); // 피격 시 깜빡임 효과
}
// 사망 처리: 아이템 드랍 후 오브젝트 파괴
void Die()
{
for (int i = 0; i < dropOnDeath.Length; i++)
{
Instantiate(dropOnDeath[i].dropPrefab, transform.position + Vector3.up * 2, Quaternion.identity);
}
Destroy(gameObject);
}
// 피해 입을 때 깜빡임 효과를 주는 코루틴
IEnumerator DamageFlash()
{
// 색상 변경 (피해 효과)
for(int i = 0; i < meshRenderers.Length; i++)
{
meshRenderers[i].material.color = new Color(1.0f, 0.6f, 0.6f);
}
yield return new WaitForSeconds(0.1f);
// 원래 색상으로 복구
for(int i = 0; i < meshRenderers.Length; i++)
{
meshRenderers[i].material.color = Color.white;
}
}
}
적 스크립트 컴포넌트에 수치 작성
결과
▶▷ 오늘의 회고
내비게이션 기능은 예전에 독학으로 한번 사용해보려고 도전했다가 실패했었던 기능이었는데 그때부터 남아있던 아쉬움이 이번 강의를 통해 사라지게 되었고 앞으로 3D 프로젝트를 할 때 정말 많이 사용할 기능일 것 같아 확실히 이해하는 시간을 가져야 할 것 같다.