본문 바로가기

진리는어디에

[Unity] Physics.Raycast 완벽 가이드

들어가며

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

이번 포스트에서는 Raycast 함수를 사용하기 위해 알아야할 필수적인 요소들을 살펴 보는 시간을 갖도록 하겠다.

Unity에서 Raycast를 사용하는 법

Unity 2020.3 버전 기준으로 Physics.Raycast는 아래와 같이 다양한 버전으로 오버로드 되어 제공되고 있다. 

bool Raycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask, QueryTriggerInteraction queryTriggerInteraction);

bool Raycast(Ray ray, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

파라메터가 많아 복잡해 보이지만 디폴트 파라메터들을 제외하고 보면 결국 Raycast 함수의 핵심은 아래 세가지 정도로 요약 된다.

  • Ray 변수
  • RaycastHit 변수
  • Raycast 함수

Collider 컴포넌트를 오브젝트에 적용

Raycast 를 이용해 대상 직선이 대상에 투영되는지 체크하기 위해서 대상 오브젝트에는 Collider 컴포넌트가 적용되어 있어야 한다. 아래 이미지에는 BoxCollider를 예시하고 있으나 CapsuleCollider, MeshCollider 등등 많은 Collider 컴포넌트들이 있다.

단, 주의할 점은 만일 Physics2D.Raycast를 이용하는 경우에는 Collider2D 시리즈의 Collder 컴포넌트를 사용해야 한다.

Ray 구조체 사용법

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

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

Unity에서 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를 만들어 낼수도 있다.
    ScreenPointToRay는 일반적으로 마우스로 화면(screen)을 클릭했을 때 해당 위치의 오브젝트를 검출할 때 사용 된다.
// Creates a Ray from the mouse position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

이런 헬퍼 함수들을 사용하여 월드의 특정 지점에서 부터 쉽게 Ray를 만들수 있다.

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

Ray ray;

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

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

RaycastHit 구조체 사용법

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

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 함수 사용법

Unity의 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의 충돌 탐지 거리 제한, 특정 레이어 또는 트리거 콜라이더 무시하기 등의 제약사항을 추가할 수 있다. 이러한 세팅들은 어떤 오버로드 된 레이케스트 함수를 사용하느냐에 따라 달라진다.

Raycast 함수의 다양한 기능들

Unity에는 다양한 버전의 Raycast함수가 있으며 각각은 서로 약간 다른 기능을 제공하고 있다. 일부 버전은 몇가지 인자만 사용하여 간단한 기능만 수행하지만 다른 오버로드된 버전은 더 복잡한 인자들을 받아들이고 더 복잡한 작업을 한다.

예를들어 가장 기본적인 버전의 Physics.Raycast는 인자로 Ray 변수 하나만 받는다.

if (Physics.Raycast(ray)) 
{ 
    // The Ray hit something
}

다른 오버로드 된 버전의 Physics.Raycast는 Ray, RaycastHit, MaxDistance, LayerMask(특정 레이어가 포함되거나 제외 되는 것을 지정) 및 TriggerCollider를 사용할 수 있는지 여부를 결정하는 QueryTriggerInteraction을 설정할 수 있다.

public LayerMask layerMask;

void Update()
{
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit hitData;
    
    if (Physics.Raycast(ray, out hitData, 10, layerMask, QueryTriggerInteraction.Ignore))
    {
        // The Ray hit something less than 10 Units away,
        // It was on the a certain Layer
        // But it wasn't a Trigger Collider
    }
}

이제 부터 Raycast 함수들이 제공하는 기능들에 대해 살펴 보도록 하자.

최대거리를 지정하여 Raycast 범위 제한

대부분의 오버로드 된 Physics.Raycast에서는 아래와 같이 레이캐스팅 최대 거리를 제한 할 수 있다.

Ray ray = new Ray(transform.position, transform.forward);

if (Physics.Raycast(ray, 10))
{ 
    // Hit Something closer than 10 units away
}

최대 거리를 제한하므로써 최대 사거리가 있는 발사 무기의 명중 판정이라던지, 단순히 씬 전체를 무한히 가로질러 발생할 수 있는 다양한 문제를 예방할 수 있다.

하지만 거리 제한만으로 충분 할까? 아니다. 제한된 거리 내에서도 다양한 충돌 객체가 감지될 수 있다. 어떤 객체가 충돌에 감지 되어야 하고 그렇지 않은지를 결정하는 것은 전적으로 여러분에게 달려 있다. 이제 부터 알아 볼 것은 충돌이감지 되었을 때 어떻게 구분하여 별도의 처리를 해줄 수 있는지를 살펴 보도록하겠다.

Raycast에 Layer Mask 사용하기

Raycast 함수의 유용한 기능 중의 하나는 레이어에 따라 충돌체를 필터링하는 기능이다. 이를 통해 레이캐스팅에서 무시해야하는 객체를 쉽게 구분할 수 있다. 만일 당신이 아주 커다란 씬에서 엄청나게 많은 객체들이 있고 각각의 객체들이 서로 다양한 타입을 가지고 있다고 상상해보자. 이 기능은 이럴때 당신에게 필요한 특정 몇몇 객체들에 대해서만 레이캐스팅을 진행할 수 있게 해주는 아주 유용한 도구다.

예를 들어 지금 'world'라는 레이어가 있고 해당 레이어의 객체들만 레이캐스팅을 이용해 감지하려고 한다고 가정해보자. 당신이 가장 먼저 해야할 일은 퍼블릭 LayerMask 변수를 생성하는 것이다.

public class CameraRay : MonoBehaviour
{
    public LayerMask worldLayer;
    // ...
}

이렇게 public으로 선언된 LayerMask 변수는 인스펙터에서 셋팅이 가능하다.

이제 스크립트에서 아래와 같이 Raycast 함수에 LayerMask 변수를 넘겨 주기만 하면 된다.

public class CameraRay : MonoBehaviour
{
    public LayerMask worldLayer;

    void FireLaser()
    {
        Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
        if (Physics.Raycast(ray, 10, worldLayer))
        {
            Debug.Log("You hit a wall, good job!");
        }
    }
}

이렇게 하면 오직 "world" 레이어에 속한 객체들에 대해서만 레이캐스팅 검사를 진행하게 된다.

LayerMask를 사용할 때 레이어 번호를 직접 입력하는 방법

public 변수를 이용해 LayerMask를 선언하고 인스펙터에서 레이어를 지정하는 방법은 분명히 간단하면서도 쉬운 방법이지만 우리는 때로 스크립트에서 레이어 마스크를 동적으로 지정해야할 필요가 있을 때도 있다.

public static bool Raycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

레이어 마스크를 인자로 받는 Raycast 함수를 살펴 보면 int 타입을 요구하기 때문에 혹시 여러분 중에서 인스펙터의 레이어 번호를 인자로 넘기면 될것이라고 생각하는 사람이 있을 수도 있다. 아래 그림을 예로 들어 설명하면 'Server' 레이어를 선택하기 위해 9를 넘기면 될것이라 생각할 수 있다. 결론 부터 말하자면 그렇게하면 안된다.

유니티에서는 총 32개의 레이어를 지원하며 각 레이어를 구분하기 위해 32bit 비트 마스크를 사용한다. 레이어는 0부터 시작하며 31이 마지막 레이어 번호다.

9번 레이어를 선택하고 싶다면, layerMask인자로 9를 넘겨주는 것이아니라 9번 레이어는 오른쪽에서 부터 0을 포함해 10번째 이므로 아래와 같은 비트 마스크를 만들어야 한다.

그럼 정수를 이용해 유니티가 사용하는 이진값을 만들기 위해서는 어떻게 해야 하는가? 이진수는 오른쪽에서 왼쪽으로 계산되며 한칸씩 왼쪽으로 이동할때 마다 2배씩 증가한다. 예를 들어 숫자 8은 이진수로 1000이다. 

반면 9는 아래와 같이 마스킹 된다.

만일 여러분이 9번 레이어를 선택하기 위해 9를 넘기게 되면 결과적으로 위 그림과 같이 마스킹 되어 0번, 3번 레이어가 선택 되게 된다. 여러분이 9번 레이어를 선택하기 위해서는 9가 아닌 512를 넘겨 주어야 한다.

if (Physics.Raycast(ray, 10, 512))
{
    // Layer 9 was hit!
}

만일 9번과 4번 레이어를 동시에 선택하고 싶다면 아래와 같이 528을 넘겨야 한다.

if (Physics.Raycast(ray, 10, 528))
{
    // Layer 9 or 4 was hit!
}

정수를 LayerMask 값으로 변환하는 방법

앞에서 우리는 Unity는 레이어를 지정하기 위해 32bit 비트 마스크를 사용하고 있고, 레이어를 지정하기 위해서는 각 레이어 순서의 플래그가 켜져 있는 이진 값을 넘겨 줘야함을 배웠다. 하지만 앞의 예제는 우리가 이해하기에 직관적이지 못했다. 지금 부터는 쉬프트 연산자(<<)를 이용해 보다 쉽게 레이어 마스크 값을 구하는 방법에 대해 살펴 보도록 하겠다.

쉬프트 연산을 이용하는 것은 매우 간단하다. 위의 예제에서 처럼 9번 레이어를 선택하기 위해서는 0번째 레이어 마스크를 켜고 쉬프트 연산자(<<)를 이용해 왼쪽으로 9번 이동시켜 주면 된다.

if (Physics.Raycast(ray, 10, 1<<9))
{
    // Layer 9 was hit!
}

위와 같은 방식으로 단일 레이어에 대한 충돌을 감지할 수 있다. 그럼 특정 한 레이어만을 제외한 다른 모든 레이어에서 충돌을 감지하고 싶다면 어떻게 해야 할까?

하나를 제외한 모든 레이어에서 Raycast 감지

LayerMask를 사용하여 특정 레이어의 충돌을 감지할 때와 마찬가지로 LayerMask 값을 반전(invert)하여 지정된 레이어를 제외한 모든 레이어에 대해 충돌을 감지할 수 있다.

이건은 비트 연산자 중 NOT(~)연산자를 이용하면 된다. 비트 NOT 연산은 물결표(~)를 사용하고 모든 비트를 뒤집어 반전 시킨다. 예를 들어 9번 레이어를 제외한 모든 레이어에대해 감지하고 싶다면 다음과 같이 하면 된다.

Ray ray = new Ray(transform.position, transform.forward);

if (Physics.Raycast(ray, 10, ~(1<<9)))
{
    Debug.Log("something else was hit");
}

비트 연산자를 사용하여 코드에 레이어 마스크를 직접 추가하는 경우 값을 반전하기 전에 비트 연산이 먼저 수행 되도록 괄호 안에 배치하는 것이 좋다.

만일 LayerMask 변수를 따로 가지고 있다면 아래와 같이 간단하게 처리할 수도 있다.

public LayerMask worldLayer; // 레이어 마스크 변수

void FireLaser()
{
    Ray ray = new Ray(transform.position, transform.forward);

    if (Physics.Raycast(ray, 10, ~worldLayer))
    {
        // Something other than the world was hit!
    }
}

레이어 이름으로 레이어 번호를 알아 오는법

앞의 예제에서는 레이어를 지정하기 위해 레이어 번호를 직접 입력했다. 하지만 여러 가지 개발적 이슈로 인해 레이어의 번호가 변경 될 수도 있다. 이 때 마다 코드를 검색하여 레이어 번호를 사용하는 부분을 일일이 수정한다는 것은 비효율적인 일이다. Unity에서는 레이어의 문자열 이름으로 부터 레이어 번호를 얻을 수 있는 LayerMask.NameToLayer 헬퍼 함수를 제공하고 있다.

예를 들어 5번 레이어의 이름이 "UI"라고 가정한다면 아래와 같은 코드는 정수 5를 리턴한다.

int layerNum = LayerMask.NameToLayer("UI");
Debug.Log(layerNum); // 5

주의 할 점은, NameToLayer 함수에서 리턴 되는 값을 바로 Raycast에 사용하면 안된다는 것이다. 앞에서 이미 다루었듯이 레이어 마스크는 이진 데이터를 파라메터로 받는다. 쉬프트 연산을 통해 해당 위치의 비트를 켜주어야 한다.

Ray ray = new Ray(transform.position, transform.forward);
int layerNum = LayerMask.NameToLayer("UI");

if (Physics.Raycast(ray, 10, 1<<layerNum))
{
    Debug.Log("something else was hit");
}

레이어 번호로 레이어 이름을 알아 오는법

앞에서 레이어 이름으로 레이어 번호을 알아 왔듯이 레이어 번호를 이용해 레이어의 이름을 알아 낼 수도 있다.

int layerNum = LayerMask.NameToLayer("UI");

string layerName = LayerMask.LayerToName(layerNum);
Debug.Log(layerName); // UI

Raycast를 사용할 때 trigger collider를 무시하는 법

만일 Raycast 함수가 trigger collider에 대해서 동작하는 것을 원치 않는 경우 해당 객체를 별도의 레이어에 배치하는 방법도 있겠지만 그리 좋은 방법은 아니다. 이번 섹션에서는 레이어에서 트리거 콜라이더를 무시하는 방법에 대해 살펴 보도록하겠다.

기본적으로 raycasting은 트리거 콜라이더를 감지한다. 레이캐스트가 트리거 콜라이더에 충돌하면 다른 콜라이더와 동일한 방식으로 동작한다. 하지만 프로젝트 설정을 통해 전역적으로 또는 Raycast별로 동작을 변경할 수 있다.

모든 Raycast Trigger 충돌을 비활성화 하는 법

모든 Raycast 트리거 충돌을 비활성화 하는 가장 간단한 방법은 프로젝트 셋팅에서 해당 옵션을 끄는 것이다. Project Setting를 열고 Physics 메뉴를 선택후 Queries Hit Trigger의 체크를 해제한다.

이제 기본적으로 Raycast는 모든 트리거 충돌을 무시하게 된다. 그리고 이 옵션을 끈 상태에서도 Raycast 함수의 QueryTriggerInteraction 파라메터를 이용해 전역 설정을 덮어 쓸 수 있다.

void FireLaser()
{
    Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
    if (Physics.Raycast(ray, 10, worldLayer, QueryTriggerInteraction.Ignore))
    {
        // Whatever you hit, it wasn't a trigger
    }
}

QueryTriggerInteraction 파라메터는 아래 세 가지 중 하나의 값을 가질 수 있다.

  • Ignore - 트리거 콜라이더의 충돌을 무시한다.
  • Collider - 트리거 콜라이더의 충돌을 허용한다.
  • UseGlobal - Physics 옵션에 정의된 기본 값을 따른다
※ Physics2D.Raycast 의 경우에는 위와 같은 enum 값이 아닌 Physics2D.queriesHitTrigger를 이용하여 true, false를 사용한다고 한다.
https://stackoverflow.com/questions/44402021/how-to-make-raycast-ignore-trigger-colliders

Raycast를 이용하여 여러 물체를 맞추는 법

Raycast 함수는 단일 객체에 충돌이 발생하면 true를 리턴하고 멈춘다. 하지만 때때로 레이저가 여러 물체를 관통하는 것과 같이 동일 Ray를 사용하여 여러 객체에 대한 충돌을 검사해야 하는 때가 있다. 이런 경우 단일 객체에 대한 레이캐스팅을 진행하는 Raycast 함수 대신 RaycastAll을 사용할 수 있다.

하나의 Ray로 여러 객체에 대한 충돌을 검사하고 싶을 때는 RaycastAll을 사용한다.

RaycastAll 함수 사용법

RaycastAll 함수는 기본적으로 Raycast함수와 매우 비슷하게 동작한다. 단 Raycast 함수에서 단 하나의 객체에 대한 충돌 정보만 반환하는 대신 RaycastHit 구조체 배열을 이용해 여러 개체에 대한 충돌 정보들을 반환한다. 

public RaycastHit[] hits;

void Update()
{
    Ray ray = new Ray(transform.position, transform.forward);
    hits = Physics.RaycastAll(ray);
}

RaycastAll은 단일 Ray를 사용하여 총돌한 여러 객체에 대한 정보를 얻는데 사용 된다. 예를 들어 아래와 같이 Ray가 충돌한 객체들의 개수를 알아 낼 수 있다.

int numObjectsHit = hits.Length;

아니면 Ray의 경로에 있던 모든 객체들을 파괴하는데 사용 될 수도 있다.

public class CameraRay : MonoBehaviour
{
    public RaycastHit[] hits;

    void Update()
    {
       if(Input.GetMouseButtonDown(0))
        {
            FireLaser();
        }
    }

    void FireLaser()
    {
        Ray ray = new Ray(transform.position, transform.forward);
        hits = Physics.RaycastAll(ray);

        foreach(RaycastHit obj in hits)
        {
            Destroy(obj.transform.gameObject);
        }
    }
}

RaycastAll의 문제

앞에서와 같이 RaycastAll은 Ray에 충돌하는 모든 객체에 대한 정보를 얻어 오는데 유용하게 사용 될 수 있다. 단, RaycastAll은 여러 충돌체를 감지 할 수는 잇지만 정의되지 않은 순서로 검색한다. 우리가 직관적으로 생각하기에 RaycastAll에 의해 리턴 되는 정보는 물체를 통과한 레이저로써 시작점과 가까이 있는 순서대로 배열에 들어갈것 같지만 실제 결과값은 예측 할 수 없는 순서로 저장된다. 

결과값에 대해 동시에 처리를 한다면 이렇게 순서가 뒤섞이는것이 문제가 되진 않지만 순서가 중요하다면 아래와 같이 거리에 따라 배열을 정렬하는 방법도 있다.

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

...

void FireLaser()
{
    RaycastHit[] hits;
    Ray ray = new Ray(transform.position, transform.forward);
    hits = Physics.RaycastAll(ray);

    // Sorts the Raycast results by distance
    Array.Sort(hits, (RaycastHit x, RaycastHit y) => x.distance.CompareTo(y.distance));
}

RaycastAll vs RaycastNonAlloc

RaycastNonAlloc은 앞에서 살펴본 RaycastAll과 매우 유사하게 동작한다. 단, 한가지 차이점이 있다면 RaycastNonAlloc은 RaycastAll 처럼 호출 될 때 마다 내부적으로 RaycastHit 배열을 생성 후 리턴하는 방식이 아니라, 외부에서 이미 생성된 배열을 out 파라메터로 재사용 할 수 있어 가비지(garbage)의 발생을 줄인다.

RaycastNonAlloc는 충돌한 객체의 개수를 리턴하지만 그 수는 인자로 넘겨진 배열의 길이 보다는 크지 않다. 실제 반환된 충돌된 객체의 개수를 알면 리턴된 배열이 가득 차지 않았을 때 빈 요소들을 참조하는 것을 방지할 수 있다.

RaycastHit[] results = new RaycastHit[10];

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        FireLaser();
    }
}

void FireLaser()
{
    Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
    int hits = Physics.RaycastNonAlloc(ray, results);

    for(int i=0; i < hits; i++)
    {
        Destroy(results[i].transform.gameObject);
    }
}

일반적으로 RaycastNonAlloc은 RaycastAll 보다 효율적인 버전이다. 하지만 RaycastAll과 마찬가지로 RaycastNonAlloc 역시 정의되지 않은 순서로 충돌 정보 배열을 리턴한다. 이게 왜 문제가 되냐면 RaycastNonAlloc으로 부터 리턴 되는 충돌 객체에 대한 정보들은 지정된 배열의 길리 제한으로 인해 모두 리턴 되지 않을 수 있다. 이 배열은 정의 되지 않은 순서로 저장되기 때문에 리턴된 충돌 정보들이 시작점으로 부터 가까운 객체들이라는 보장이 없다.

예를 들어 최대 3명의 적을 관통하는 무기를 만들려는 경우, 결과를 받아올 RaycastHit 배열의 크기가 3인 경우, RaycastNonAlloc을 사용하면 3개의 결과가 반환 되긴하지만 이 결과를 정렬하더라도 가장 가까운 3개가 될것이라는 보장을 하지 못한다.

RaycastNonAlloc이 얼필 보면 성능을 향상 시킬 수 있는 좋은 방법 처럼 보이지만 위와 같은 문제가 있다. 따라서 이를 효과적으로 사용하려면 LayerMask와 같은 기능들을 사용하여 raycasting 결과로 리턴되는 개수가 한정적일 때 최대 길이의 배열을 사용하여 반복적인 배열을 재할당 없이 사용하는 것이 가장 적합한 방법이다.

마치며

이상으로 Unity Raycast에 대한 전반적인 내용에 대해 살펴보았다. 할 이야기는 더 있지만 지면이 너무 길어지면 지겨워서 아무도 안 읽을것 같으므로 살짝 쪼개서 다음 포스트에 이어서 써보도록 하겠다.

본 포스트는 gamedevbeginner.com의 John French에 의해 작성된 'Raycast in Unity'를 번역한 것이다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!