[Unity] 3D Navigation Mesh

Southbig·2023년 3월 24일
0

Navigation Mesh

Navigation Mesh는 바닥, 장애물과 같은 게임월드의 정보를 미리 저장해두고 에이전트가 이동할 때 저장된 정보를 바탕으로 장애물을 피해 목표 위치까지 이동하는 경로 탐색을 제공한다

Window -> AI -> Navigation

맵에 새로운 바닥, 장애물을 배치하였다면, Nacigation Static 설정과 Bake를 다시 진행해야 한다

Scene View에 하늘색으로 표시되는 부분이 이동 가능한 부분이다
(Navigation View가 활성화 되어 있어야 Navigation Mesh가 출력된다)

경사각(Max Slope)

계단 높이(Step Height)

NavMeshAgent와 같이 유니티에서 만든 인공지능을 이용하기 위해서 UnityEngine.AI가 필요하다

NavMovement3D

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

public class NavMovement3D : MonoBehaviour
{
    [SerializeField]
    private float moveSpeed = 5.0f;
    private UnityEngine.AI.NavMeshAgent navMeshAgent;

    private void Awake()
    {
        navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
    }

    public void MoveTo(Vector3 goalPosition)
    {
        // 기존에 이동 행동을 하고 있었다면 코루틴 중지
        StopCoroutine("OnMove");
        // 이동 속도 결정
        navMeshAgent.speed = moveSpeed;
        // 목표지점 설정 (목표까지의 경로 계산 후 알아서 움직인다)
        navMeshAgent.SetDestination(goalPosition);
        // 이동 행동에 대한 코루틴 시작
        StartCoroutine("OnMove");
    }
    IEnumerator OnMove()
    {
        while(true)
        {
            // 목표 위치(navMeshAgent.destination)와 내 위치(transform.position)의 거리가 0.1미만일 때
            // 즉, 목표 위치에 거의 도착했을 때
            if (Vector3.Distance(navMeshAgent.destination, transform.position) < 0.1f)
            {
                // 내 위치를 목표 위치로 설정
                transform.position = navMeshAgent.destination;
                // SetDestination()에 의해 설정된 경로를 초기화, 이동을 멈춘다
                navMeshAgent.ResetPath();

                break;
            }

            yield return null;
        }
    }
}
  • NavMeshAgent.SetDestination(Vector3 position)
    position을 목표지점으로 설정
    (목표지점만 설정되면 경로를 탐색해서 자동 이동한다)

  • float distance = Vector3.Distance(Vector3 a, Vector3 b);
    a와 b 두 백터 사이의 거리 값(distance)을 구한다

  • NavMeshAgent.destination
    SetDestination()으로 설정한 목표지점 Vector3 정보가 저장되어 있다

  • NavMeshAgent.ResetPath();
    현재 설정되어있는 이동 경로를 초기화하여 이동을 멈추게 한다

NavPlayerController

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

public class NavPlayerController : MonoBehaviour
{
    private NavMovement3D navMovement3D;

    private void Awake()
    {
        navMovement3D = GetComponent<NavMovement3D>();
    }

    private void Update()
    {
        // 마우스 왼쪽 버튼을 눌렀을 때
        if (Input.GetMouseButtonDown(0))
        {
            RaycastHit hit;
            //Camera.main : 태그가 "Camera"인 오브젝트 = "Main Camera"
            // 카메라로부터 마우스 좌표(Input.mousePosition) 위치를 관통하는 광선 생성
            // ray.origin : 광선의 진행 방향
            // ray.direction : 광선의 진행 방향
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            // physics.Raycast() : 광선을 발사해서 부딪히는 오브젝트를 검출
            // (광선에 부딪히는 오브젝트가 있으면 true 반환)
            // ray.origin 위치로부터 ray.direction 방향으로 무한한 길이(Mathf.Infinity)의 광선 발사
            // 광선에 부딪히는 오브젝트의 정보를 hit에 저장
            if(Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                // hit.transform.position : 부딪힌 오브젝트의 위치
                // hit.point : 광선과 오브젝트가 부닺힌 세부 위치

                // hit.point를 목표위치로 이동
                navMovement3D.MoveTo(hit.point);
            }
        }
    }
}

코루틴 (Coroutine)

코루틴은 시간의 경과에 따른 절차적 단계를 수행하는 로직을 구현하는 데 사용되는 함수다
시간의 경과에 따른 절차적 단계에 대한 로직을 구현하는 것은 Update() 함수들에서도 가능하다

매 프레임마다 호출되는 로직은 Update()에서 구현하면 된다
하지만 초당 호출이나, 매 프레임마다 호출이 필요하지 않은 부분을 매 프레임마다 호출하는 것은 바람직한 로직이 아니다

코루틴은 함수를 호출한다
함수는 한 프레임에 호출되어 완료가 된다
이에 IEnumerator 형식을 반환 값으로 가지는 함수를 사용한다
IEnumerator는 함수 내부에 실행을 중지하고, 다음 프레임에서 실행을 재개할 수 있는 yield return 구문을 사용한다

yield return

yield return 구문에는 YieldInstruction 클래스를 사용한다

  • yield return null : 다음 프레임에 실행을 재개한다

  • yield return new WaitForSeconds : 지정된 시간 후에 재개한다

  • yield return new WaitForSecondsRealtime : Time.timescale 값에 영향을 받지 않고 지정된 시간 후에 재개한다

  • yield return new WaitForFixedUpdate : 모든 스크립트에서 모든 FixedUpdate가 호출된 후에 재개한다

  • yield return new WaitForEndOfFrame : 모든 카메라와 GUI가 렌더링을 완료하고, 스크린에 프레임을 표시하기 전에 호출된다

  • yield return StartCoroutine() : 코루틴을 연결하고 코루틴이 완료된 후에 재개한다

StartCoroutine()와 StopCoroutine()

Coroutine은 3개의 전달 인자를 사용한다

StartCoroutine()

string MethodName을 사용하는 경우, 두 번째 매개변수로, 전달 인자를 사용한다

  • Coroutine StartCoroutine(string MethodName, object = null)
  • Coroutine StartCoroutine(IEnumerator routine)
  • Coroutine StartCoroutine(Coroutine routine)

StopCoroutine()

  • StopCoroutine(string MethodName)
  • StopCoroutine(IEnumerator routine)
  • StopCoroutine(Coroutine routine)

유니티는 코루틴을 사용할 때, 전달 인자를 혼합하여 사용하지 않는 것을 권장한다

만약 StartCoroutine(string MethodName)을 사용했다면, StopCoroutine(string MethodName)을 사용하여 중지한다
실제 사용해 보면, 대상 코루틴을 찾지 못한다

Physics.Raycast

Physics.Raycast는 직선을 씬에 투영하여 대상에 적중되면 true를 리턴하는 물리 함수다
Raycast 함수는 캐스팅 성공 실패에 따른 결과만 리턴하는 간단한 형태에서 부터,
대상과 Ray의 충돌에 관련된 자세한 정보를(직선과 객체의 교차 정보, 거리, 위치, 캐스팅에 검출 된 객체의 Transform에 대한 참조 등) 리턴하는 다양한 버전이 제공 되고 있다

Ray 구조체 사용법

Ray는 직선의 시작점(origin)과 방향(direction)을 가지고 있는 단순한 구조체다

시작점(origin)은 Vector3 타입의 월드 포지션이며 방향(direction)은 직선의 방향을 나타낼 Vector3 타입의 법선 벡터다

Ray 생성방법

new를 이용하는 방법

// Creates a Ray from this object, moving forward
Ray ray = new Ray(transform.position, transform.forward);

메라 뷰포트 중앙에서 시작하는 Ray와 같은 경우 헬퍼 함수를 이용해 Ray를 자동으로 생성할 수 있다

// Creates a Ray from the center of the viewport
// 아래에서 0.5f 값은 뷰포트의 중간값을 나타낸다.
Ray ray = Camera.main.ViewportPointToRay(new Vector3 (0.5f, 0.5f, 0));

스크린의 마우스 위치로 부터 Ray를 만들어 낼수도 있다

// Creates a Ray from the mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

여기서 주의해야 할 부분은 Ray는 사용할 때 마다 업데이트 되어야만 한다는 것이다
예를 들어 Ray의 시작점과 방향이 매 프레임마다 달라지는 경우 Ray도 매 프레임 마다 갱신되어야 한다

Ray ray;

void Update()
{
    ray = transform.position, transform.forward;
}

Ray가 시작되는 위치와 방향을 결정했으면 Ray로 부터 얻은 데이터를 RaycastHit 변수에 저장한다

RaycastHit 구조체 사용법

RaycastHit은 객체와 Ray의 충돌에 대한 결과 정보를 저장하는 구조체다
Raycast 함수의 out 파라메터로 사용되며 월드에서 레이캐스팅 히트가 발생한 위치, Ray가 충돌한 물체, Ray의 원점에서 얼마나 떨어져있는지 등의 정보를 저장하여 돌려준다

  1. RaycastHit를 다음과 같이 선언한다
    // Container for hit data
    RaycastHit hitData;

그리고 Raycast 함수를 통해 씬에 Ray를 발사하면 캐스팅 결과에 따라 충돌에 대한 정보를 RaycastHit 변수에 저장한다
RaycastHit에 저장된 정보들을 아래와 같이 접근할 수 있다

먼저 RaycastHit.point를 이용하여 월드에서 레이캐스팅이 감지된 위치를 얻을 수 있다

Vector3 hitPosition = hitData.point;

또는 RaycastHit.distance를 사용하여 Ray의 원점에서 충돌 지점까지의 거리를 구할수 있다

float hitDistance = hitData.distance;

Tag와 같은 히트 된 대상 객체의 Collider 세부 정보를 얻을수도 있다

// Reads the Collider tag
string tag = hitData.collider.tag;

RaycastHit.transform을 사용하여 충돌 객체의 Transform에 대한 참조를 얻을 수도 있다

// Gets a Game Object reference from its Transform
GameObject hitObject = hitData.transform.gameObject;

Ray와 RaycastHit 변수는 Ray가 어디로 발사되고, 그에 따른 충돌 정보가 어떻게 저장 될지를 정의하지만 이 두 가지로는 아무것도 할 수 없다
그래서 실제 씬에서 Ray를 발사하고 충돌이 있는지 확인하기 위해서는 Raycast 함수를 사용해야 한다

Raycast 함수 사용법

Raycast 함수를 사용하면 Ray가 씬의 다른 객체와 충돌하는지 여부를 알 수 있으며 충돌할 경우 충돌 정보를 RaycastHit 변수에 저장할 수 있다

여러 버전의 Raycast함수가 있지만,
Raycast를 사용하는 가장 일반적인 방법 중 하나는 Ray의 객체에 대한 히트여부에 따라 true 또는 false를 리턴하고, out 파라메터로 RaycastHit를 리턴하는 버전을 사용하는 것이다

// public static bool Raycast(Ray ray, out RaycastHit hitInfo);

void FireRay()
{
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit hitData;

    Physics.Raycast(ray, out hitData);
}

위와 같이 하면 생성된 Ray가 씬으로 발사되고 Ray에 충돌한 어떤 것이든 그것에 관한 충돌 정보가 RaycastHit 변수에 저장된다

앞에서 Physics.Raycast 함수의 리턴 타입은 bool이라고 했다

Ray에 어떠한 오브젝트라도 걸리면 true를 리턴한다
이는 if 문을 이용하여 raycasting이 성공했을때 그에 대한 처리를 추가 할 수 있다는 뜻이다

void Update()
{
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit hitData;

    if (Physics.Raycast(ray, out hitData))
    {
        // The Ray hit something!
    }
}

위와 같은 방법으로 Ray가 실제로 무엇인가에 충돌 했을 때만 if 문 내의 코드가 실행되도록 할 수 있다
이는 RaycastHit 변수에 실제 충돌 정보가 저장 되었을 때만 RaycastHit을 사용하도록 제한 할 수 있다는 뜻이다

Raycast함수는 추가 디폴트 인자를 가지고 있다
이 인자들을 이용해 Ray의 충돌 탐지 거리 제한, 특정 레이어 또는 트리거 콜라이더 무시하기 등의 제약사항을 추가할 수 있다
이러한 세팅들은 어떤 오버로드 된 레이케스트 함수를 사용하느냐에 따라 달라진다

profile
즐겁게 살자

0개의 댓글