AI 네비게이션(AI Navigation)은 인공지능이 게임이나 시뮬레이션 등 가상 환경에서 이동하는 방법을 결정하는 기술이다. 주로 3D 게임에서 캐릭터나 NPC가 지능적으로 이동하도록 만들어진다. 이를 위해 AI네이게이션 시스템은 지형, 장애물, 목표 지점 등을 고려하여 적절한 경로를 생성하고 이동하는데 사용된다.
AI 네비에기션을 구현하는 주요 기술과 개념은 다음과 같다.
Navigation Mesh (네비에기션 매쉬) :
3D 공간을 그리드로 나누어 이동 가능한 지역과 장애물이 있는 지역을 구분하는 매쉬이다.
캐릭터가 이동할 수 있는 영역과 이동할 수 없는 영역을 정의하고, 이를 기반으로 경로를 계산한다.
Pathfinding (경로 탐색) :
캐릭터의 현재 위치에서 목표 지점까지 가장 적절한 경로를 찾는 알고리즘이다.
주로 A*알고리즘 등이 사용되며, 지정된 목표 위치까지 최단 경로를 탐색한다.
Steeing Behavior (스티어링 동작) :
캐릭터나 NPC가 경로를 따라 이동할 때, 보다 자연스러운 동작을 구현하는데 사용된다.
동적으로 캐릭터의 이동방향과 속련을 조정하야 부드럽고 현식적인 이동을 시뮬레이션 한다.
Obstacle Avoidance (장애물 피하기) :
캐릭터가 이동 중에 장애물과 충돌하지 않도록 하는 기술이다.
각종 센서나 알고리즘을 사용하여 장애물을 감지하고 피하는 동작을 수행한다.
Local Avoidance (근접 회피) :
여러 캐릭터나 NPC가 서로 충돌하지 않도록 하는 기술이다.
캐릭터들 사이의 거리를 유지하거나 회피 동작을 수행하여 서로 부딪히지 않도록 한다.
AI - Navigation(Obsolete) 클릭
Navigation Static 설정
Bake
빈 오브젝트 생성 - NPC 이름 변경
Nav Mesh Agent, Box Collider 추가
NPC 스크립트 추가
하위 Bear 모델 추가
애니메이터 컨트롤러 생성
아래와 같이 파라메터 & 트렌지션 구성
public enum AIState
{
Idle,
Wandering,
Attacking,
Fleeing
}
public class NPC : MonoBehaviour, IDamagable
{
[Header("State")]
public int health;
public float walkSpeed;
public float runSeppd;
public ItemData[] dropOnDeath;
[Header("AI")]
private AIState aiState;
public float detectDistance;
public float safeDistance;
[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 NavMeshAgent agent;
private Animator animator;
private SkinnedMeshRenderer[] meshRenderers; // 피격을 위한
private void Awake()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponentInChildren<Animator>();
meshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
}
private void Start()
{
SetState(AIState.Wandering);
}
private void Update()
{
// 플레이어의 위치를 찾는다. ( 내위치(NPC transform.position), Player의 위치)
PlayerDistance = Vector3.Distance(transform.position, PlayerController.instance.transform.position);
animator.SetBool("Moving", aiState != AIState.Idle);
switch (aiState)
{
case AIState.Idle: PassiveUpdate(); break;
case AIState.Wandering: PassiveUpdate(); break;
case AIState.Attacking: AttackingUpdate(); break;
case AIState.Fleeing: FleeingUpdate(); break;
}
}
private void FleeingUpdate()
{
if (agent.remainingDistance < 0.1f)
{
agent.SetDestination(GetFleeLocation());
}
else
{
SetState(AIState.Wandering);
}
}
private void AttackingUpdate()
{
// 1. 공격중일 때, Player가 나보다 멀어진다?
if (PlayerDistance > attackDistance || !IsPlaterInFireIdOfView())
{
agent.isStopped = false;
NavMeshPath path = new NavMeshPath();
// 2. 경로 체크를 하고 이동이 가능하면 이동
if (agent.CalculatePath(PlayerController.instance.transform.position, path)) // 경로를 새로 검색, 찾는다()안의 경로를
{
// SetDestination : 설정해준 목적지로 이동해준다.
agent.SetDestination(PlayerController.instance.transform.position);
}
// 2. 불가능하면 도망
else
{
SetState(AIState.Fleeing);
}
}
else
{
agent.isStopped = true;
if (Time.time - lastAttackTime > attackRate)
{
lastAttackTime = Time.time;
PlayerController.instance.GetComponent<IDamagable>().TakePhysicalDamage(damage);
animator.speed = 1;
animator.SetTrigger("Attack");
}
}
}
private void PassiveUpdate()
{
if (aiState == AIState.Wandering && agent.remainingDistance < 0.1f) // remainingDistance : NavMeshAgent에 지정된 목적지까지 남은 거리를 반환.
{
SetState(AIState.Idle);
Invoke("WanderToNewLocation", Random.Range(minWanderWaitTime, maxWanderWaitTime));
}
if (PlayerDistance < detectDistance)
{
SetState(AIState.Attacking);
}
}
private bool IsPlaterInFireIdOfView() // 시야각, 뒤돌아 보고 있는데 공격할 수는 없으니, 시야에 보이면 공격
{
Vector3 directionToPlayer = PlayerController.instance.transform.position - transform.position; // 바라보고 싶은 방향을 빼야한다.(플레이어를 바라보는 방향)
float angle = Vector3.Angle(transform.forward, directionToPlayer); // Player를 바라보는 방향이 몇도냐?
return angle < fieldOfView * 0.5f; // 정 중앙을 0도로 하고있기 때문에, 반으로 나눠서 사용.
}
// 어떤 상태를 줬을 때, 그 상태를 적용하기 위한
private void SetState(AIState newState)
{
aiState = newState;
switch(aiState)
{
case AIState.Idle:
{
agent.speed = walkSpeed;
agent.isStopped = true; // .isStopped 멈춰라, 더이상 움직이지 마라
}
break;
case AIState.Wandering:
{
agent.speed = walkSpeed;
agent.isStopped = false;
}
break;
case AIState.Attacking:
{
agent.speed = runSeppd;
agent.isStopped = false;
}
break;
case AIState.Fleeing:
{
agent.speed = runSeppd;
agent.isStopped = false;
}
break;
}
animator.speed = agent.speed / walkSpeed; // agent.speed 에 비례해서 animator.speed(애니메이션 스피드)가 빨라진다.
}
private void WanderToNewLocation()
{
if (aiState != AIState.Idle)
{
return;
}
SetState(AIState.Wandering);
agent.SetDestination(GetWanderLocation());
}
private Vector3 GetWanderLocation()
{
NavMeshHit hit;
// SamplePosition : 경로상에 가장 가까운 곳을 가져온다.
// Random.onUnitSphere : 구체 안에서 랜덤으로 준다. 1이라는 거리를 가지고있는 모든 구체를 이룰수 있는 방향을 준다.
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * Random.Range(minWanderDistance, maxWanderDistance)), out hit, maxWanderDistance, NavMesh.AllAreas);
// 구체의 경로 안에서 경로를 더해보고서 구체의 상,하 로 갈 수도 있으니 원하는 거리의 내부에서 나올수도 있기에
// 그것을 조금씩 바꿔주려고 여러번 돌려본다. 그중에 마음에 드는것이 있으면 나오게
// 아니면 30번을 다 했다. 그래도 나오게.
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;
}
private Vector3 GetFleeLocation()
{
NavMeshHit hit;
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
int i = 0;
// GetDestinationAngle(hit.position) > 90 : 플레이어가 NPC를 바라보는 방향과 hit.positio의 각도가 90도 보다 크냐. (도망을 쳐야하니 플레이어 쪽으로 도망치지는 않을 것이다.)
while (GetDestinationAngle(hit.position) > 90 || PlayerDistance < safeDistance)
{
NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
i++;
if (i == 30)
break;
}
return hit.position;
}
private float GetDestinationAngle(Vector3 targetPos)
{
// Calculates the angle between vectors from and ( ~ 와 벡터 사이의 각도를 계산합니다.)
// 반환 값 : The anfle in degrees between the two vectors ( 두 벡터 사이의 각도(도) )
return Vector3.Angle(transform.position - PlayerController.instance.transform.position, transform.position + targetPos);
}
public void TakePhysicalDamage(int damageAmount)
{
health -= damageAmount;
if (health <= 0)
Die();
StartCoroutine(DamageFlash());
}
private void Die()
{
for(int x = 0; x < dropOnDeath.Length; x++)
{
Instantiate(dropOnDeath[x].dropPrefab, transform.position + Vector3.up * 2, Quaternion.identity);
}
Destroy(gameObject);
}
private IEnumerator DamageFlash()
{
for (int x = 0; x < meshRenderers.Length; x++)
meshRenderers[x].material.color = new Color(1.0f, 0.6f, 0.6f);
yield return new WaitForSeconds(0.1f);
for (int x = 0; meshRenderers.Length > 0; x++)
meshRenderers[x].material.color = Color.white;
}
}
포스트 프로세싱(Post-Processing)은 Unity나 다른 게임 엔진에서 화면에 렌더링된 이미지에 추가적인 효과를 적용하는 기술이다. 이 기술을 사용하여 게임 화면에 색상 보정, 블러 효과, 광학 효과 등을 적용하여 시각적으로 더 흥미로운 그래픽 효과를 구현할 수 있다.
포스트 프로세싱의 일부 기능과 사용법은 다음과 같다.
색상 보정 및 색감 필터링 : 화면에 특정 색조, 채도, 밝기 등의 보정을 적용하여 화면의 색상을 조작할 수 있다.
블러 효과 : 게임 화면에 블러 효과를 적용하여 광학적 흐림, 광학 흐림, 도형 흐림 등을 구현할 수 있다.
선 처리 및 톤 매핑 : 경계선 처리, 윤곽선 강조, 톤 매핑 등을 적용하여 게임 화면을 더 생동감 있게 만들 수 있다.
광학 효과 : 렌즈 반사, 빛 착란, 먼지 효과 등을 추가하여 게임 화면에 현실적인 광학 효과를 부여할 수 있다.
사용자 정의 효과 : Unity의 프로세싱 스택을 사용하여 사용자가 직접 쉐이더 코드를 작성하여 원하는 효과를 만들어 적용할 수도 있다.
https://docs.unity3d.com/kr/2021.3/Manual/PostProcessingOverview.html
Main Camera - Post Process Volume, Post Process Layer 추가
Volume Is Global 체크
예)
public class Footsteps : MonoBehaviour
{
public AudioClip[] footstepClips;
private AudioSource audioSource;
private Rigidbody _rigidbody;
public float footstepThreshold;
public float footstepRate;
private float lastFootstepTime;
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
audioSource = GetComponent<AudioSource>();
}
private void Update()
{
if (Mathf.Abs(_rigidbody.velocity.y) < 0.1f) // Player가 땅에 있을 때
{
if (_rigidbody.velocity.magnitude > footstepThreshold) // 움직인다면
{
if (Time.time - lastFootstepTime > footstepRate) // 사운드의 딜레이
{
lastFootstepTime = Time.time;
audioSource.PlayOneShot(footstepClips[Random.Range(0, footstepClips.Length)]);
}
}
}
}
}
빈 오브젝트 생성
Audio Source 추가
박스 콜라이더 추가
Is Trigger 체크
public class MusicZone : MonoBehaviour
{
public AudioSource audioSource;
public float fadeTime;
public float maxVolume;
private float targetVolume;
private void Start()
{
targetVolume = 0.0f;
audioSource = GetComponent<AudioSource>();
audioSource.volume = targetVolume;
audioSource.Play();
}
private void Update()
{
if (!Mathf.Approximately(audioSource.volume, targetVolume)) // 근사치가 되면 동일한 값으로 해라
{
// targetVolume으로 서서히 줄어들거나 올라간다.
audioSource.volume = Mathf.MoveTowards(audioSource.volume, targetVolume, (maxVolume / fadeTime) * Time.deltaTime);
}
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
targetVolume = maxVolume;
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
targetVolume = 0.0f;
}
}