본문 바로가기

라인 트레이스 (Trace) 시스템

시작점·끝점·채널을 지정한 Line Trace로 대상을 검출하고 FHitResult에서 충돌 정보를 읽습니다.

총을 쏘는 순간 조준선 끝에 무엇이 있는지 알아야 할 때, 실제로 작은 총알 액터를 만들어 날려 보내고 무언가와 부딪히기를 기다리는 방법도 있습니다. 하지만 그 순간의 경로에 무엇이 있는지만 알면 되는 경우가 더 많습니다. 플레이어가 지금 바라보는 문이 무엇인지, AI가 플레이어를 볼 수 있는지, 캐릭터 발밑의 바닥이 어떤 재질인지도 마찬가지입니다. 이럴 때 충돌을 기다리지 않고 직접 월드에 질문을 던지는 방법이 트레이스(trace)입니다.

물리 시뮬레이션과 Force 적용에서는 물체를 움직였다면, 이 글에서는 보이지 않는 선을 쏘아 맞은 대상을 찾는 라인 트레이스를 다룹니다. 트레이스의 종류와 필터 방식을 비교하고, C++로 총 쏘기 트레이스를 구현한 뒤, Single과 Multi 함수가 무엇을 돌려주는지 구체적인 배치로 따라가 봅니다.


라인 트레이스는 무엇을 하는가

라인 트레이스(line trace)는 3D 공간의 시작점에서 끝점까지 가상의 선을 그어, 그 선이 월드의 어떤 물체와 만나는지 알아내는 쿼리(query)입니다. 레이캐스트(raycast)라고도 부릅니다. 충돌 컴포넌트끼리 부딪히거나 겹치기를 기다리는 대신, 필요한 순간에 월드를 직접 훑어 정보를 얻는다는 점이 다릅니다.

라인 트레이스는 여러 곳에 쓰입니다. 발사하는 순간의 경로를 검사하는 히트스캔(hitscan) 판정에 쓰는데, 이때는 실제 투사체의 비행 시간이나 부피를 재현하지 않으므로 빠르게 날아가는 투사체는 스윕이나 연속 충돌 검사 같은 다른 설계가 필요합니다. 플레이어가 바라보는 정확한 지점이나 물체를 찾는 조준, AI가 앞의 장애물이나 플레이어를 볼 수 있는지 판단하는 시야 검사, 플레이어가 버튼이나 문을 바라볼 때만 상호작용하게 하는 감지에도 씁니다. 착지할 때 발밑 바닥의 재질과 높이를 알아내거나, 벽면의 법선(normal) 벡터를 얻어 캐릭터가 벽을 따라 움직이게 할 때도 쓰입니다.


트레이스의 모양과 필터

트레이스는 쏘는 모양에 따라 네 가지로 나뉩니다. 라인 트레이스는 두 점 사이의 직선을 따라 검사하는 가장 기본적인 형태입니다. 스피어 트레이스(Sphere Trace)는 두 점 사이를 따라 구를 굴리듯 검사하므로, 얇은 선보다 넓은 범위를 감지해야 할 때 씁니다. 박스 트레이스(Box Trace)는 직육면체를 움직이듯 검사해 넓은 면의 장애물을 찾는 데 좋고, 캡슐 트레이스(Capsule Trace)는 캡슐 모양을 움직이듯 검사해 캐릭터가 이동할 때의 장애물이나 지면을 찾는 데 알맞습니다.

어떤 물체를 대상으로 할지 고르는 필터는 대표적으로 두 가지입니다. 채널별 트레이스(By Channel)는 대상이 지정한 채널, 예를 들어 ECC_Visibility나 커스텀 Bullet 채널에 어떻게 응답하는지로 판정합니다. 오브젝트 타입별 트레이스(By Object Type)는 WorldStatic, Pawn, PhysicsBody 같은 특정 오브젝트 타입의 물체만 찾습니다. 충돌 프로파일을 기준으로 하는 함수도 있으므로, 쓸 함수가 어떤 규칙으로 판정하는지 확인합니다. 또 맞은 대상을 하나만 돌려주는 Single 버전과 여러 개를 돌려주는 Multi 버전이 있습니다.

트레이스의 결과 배열과 반환 bool을 따로 읽습니다

0..1000 cm 가정에서 100 cm의 A Overlap을 지나 300 cm의 벽을 첫 Block으로 반환하고, 700 cm의 B는 수집하지 않는 선택된 조회 경로이다. 원 FireWeapon의 10000 cm와 구분하고 내부 탐색 구현 순서를 단정하지 않는다.

UNREAL C++ · CH5-4

트레이스의 결과 배열과 반환 bool을 따로 읽습니다

Single·Multi 및 Channel·Object Type마다 필터와 반환 계약이 다릅니다. 본문의 실제 FireWeapon 코드는 Single By Channel만 호출합니다.

Single By Channel은 첫 Block을 결과로 반환한다시작X0cm,길이1000cm,+X방향의가정에서A는100cmOverlap,벽은300cmBlock,B는700cmBlock이다. A에서계속검사한뒤첫Block벽을Single결과로반환한다. 벽뒤B는이결과에들어오지않으며B로향하는간선도없다. Multi반환배열과bool은아래표및본문의별도계약으로비교한다.쿼리 시작0 → 1000 cmA100 cm · Overlap벽300 cm · BlockSingle 결과벽 · bool=true벽 뒤 · 미수집B700 cm · Block
Single By Channel은 첫 Block을 결과로 반환한다시작X0cm,길이1000cm,+X방향의가정에서A는100cmOverlap,벽은300cmBlock,B는700cmBlock이다. A에서계속검사한뒤첫Block벽을Single결과로반환한다. 벽뒤B는이결과에들어오지않으며B로향하는간선도없다. Multi반환배열과bool은아래표및본문의별도계약으로비교한다.쿼리 시작0 → 1000 cmA100 cm · Overlap벽300 cm · BlockSingle 결과벽 · bool=true벽 뒤 · 미수집B700 cm · Block

선은 이 가정의 Single 검사와 반환 흐름입니다. B를 별도 영역에 두어 첫 Block 뒤의 대상까지 수집한 결과로 읽지 않게 했습니다. 실제 원 FireWeapon의 기본 거리는 10000 cm이며 이 그림의 1000 cm 가정과 다릅니다.

트레이스 필터와 반환 범위
선택결과 범위읽을 때의 경계
Single By Channel지정 채널의 첫 Blocking Hittrue는 blocking hit 발견. Overlap만 있는 대상은 이 결과로 얻지 않음
Multi By Channel첫 Block까지의 Overlap과 그 BlockBlock 뒤를 관통해 모두 수집하지 않음. bool은 Block 유무이므로 false여도 Overlap 배열이 있을 수 있음
Multi By Object Type요청한 타입에 속한 쿼리 대상들타입 필터를 명시하며, 채널 Multi의 첫 Block 중단 규칙과 구별
Single By Channel
결과 범위: 지정 채널의 첫 Blocking Hit
읽을 때의 경계: true는 blocking hit 발견. Overlap만 있는 대상은 이 결과로 얻지 않음
Multi By Channel
결과 범위: 첫 Block까지의 Overlap과 그 Block
읽을 때의 경계: Block 뒤를 관통해 모두 수집하지 않음. bool은 Block 유무이므로 false여도 Overlap 배열이 있을 수 있음
Multi By Object Type
결과 범위: 요청한 타입에 속한 쿼리 대상들
읽을 때의 경계: 타입 필터를 명시하며, 채널 Multi의 첫 Block 중단 규칙과 구별

계약 예시 · Channel Multi, Overlap만 발견

Overlap 결과 배열이 있어도 Blocking Hit가 없으면 bool=false입니다.

계약 예시 · Channel Multi, Block도 발견

그 앞의 Overlap들과 첫 Block을 반환하며 bool=true입니다. 그 Block 뒤의 대상을 모두 수집하는 호출은 아닙니다.

다른 행은 API 계약 비교이며 실제 다중 히트 실행 결과가 아닙니다. 이 예제의 시작점은 Pawn의 EyesViewPoint이고 FollowCamera와 같다고 가정하지 않습니다. 자기 자신 제외·충돌 형상·쿼리 참여를 함께 확인합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명시작 X 0 cm, 길이 1000 cm, +X 방향의 가정에서 A는 100 cm Overlap, 벽은 300 cm Block, B는 700 cm Block이다. A에서 계속 검사한 뒤 첫 Block 벽을 Single 결과로 반환한다. 벽 뒤 B는 이 결과에 들어오지 않으며 B로 향하는 간선도 없다. Multi 반환 배열과 bool은 아래 표 및 본문의 별도 계약으로 비교한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
조회 가정

설명용 0..1000 cm에 대상 응답을 둔다.

  • A: 100 cm Overlap
  • 벽: 300 cm Block
  • 원 FireWeapon의 10000 cm 코드와 별도 설명 사례임을 명시한다.
반환 경계

이 query는 첫 Block 벽까지의 결과를 정한다.

  • 첫 Block: 벽 300 cm
  • OverlapA: Block 아님
  • 간선은 논리 반환 범위이며, 엔진 내부 탐색 순서를 보장하지 않는다.
뒤 대상 제외

벽 뒤 B의 700 cm는 이 반환 범위에 수집되지 않는다.

  • B: 700 cm 미수집
  • 종료 범위: 벽까지
  • 벽을 통과하는 피해 권한을 이 조회 결과에 추가하지 않는다.

트레이스의 결과 배열과 반환 bool을 따로 읽습니다

0..1000 cm 가정에서 100 cm의 A Overlap을 지나 300 cm의 벽을 첫 Block으로 반환하고, 700 cm의 B는 수집하지 않는 선택된 조회 경로이다. 원 FireWeapon의 10000 cm와 구분하고 내부 탐색 구현 순서를 단정하지 않는다.

UNREAL C++ · CH5-4

트레이스의 결과 배열과 반환 bool을 따로 읽습니다

Single·Multi 및 Channel·Object Type마다 필터와 반환 계약이 다릅니다. 본문의 실제 FireWeapon 코드는 Single By Channel만 호출합니다.

Single By Channel은 첫 Block을 결과로 반환한다시작X0cm,길이1000cm,+X방향의가정에서A는100cmOverlap,벽은300cmBlock,B는700cmBlock이다. A에서계속검사한뒤첫Block벽을Single결과로반환한다. 벽뒤B는이결과에들어오지않으며B로향하는간선도없다. Multi반환배열과bool은아래표및본문의별도계약으로비교한다.쿼리 시작0 → 1000 cmA100 cm · Overlap벽300 cm · BlockSingle 결과벽 · bool=true벽 뒤 · 미수집B700 cm · Block
Single By Channel은 첫 Block을 결과로 반환한다시작X0cm,길이1000cm,+X방향의가정에서A는100cmOverlap,벽은300cmBlock,B는700cmBlock이다. A에서계속검사한뒤첫Block벽을Single결과로반환한다. 벽뒤B는이결과에들어오지않으며B로향하는간선도없다. Multi반환배열과bool은아래표및본문의별도계약으로비교한다.쿼리 시작0 → 1000 cmA100 cm · Overlap벽300 cm · BlockSingle 결과벽 · bool=true벽 뒤 · 미수집B700 cm · Block

선은 이 가정의 Single 검사와 반환 흐름입니다. B를 별도 영역에 두어 첫 Block 뒤의 대상까지 수집한 결과로 읽지 않게 했습니다. 실제 원 FireWeapon의 기본 거리는 10000 cm이며 이 그림의 1000 cm 가정과 다릅니다.

트레이스 필터와 반환 범위
선택결과 범위읽을 때의 경계
Single By Channel지정 채널의 첫 Blocking Hittrue는 blocking hit 발견. Overlap만 있는 대상은 이 결과로 얻지 않음
Multi By Channel첫 Block까지의 Overlap과 그 BlockBlock 뒤를 관통해 모두 수집하지 않음. bool은 Block 유무이므로 false여도 Overlap 배열이 있을 수 있음
Multi By Object Type요청한 타입에 속한 쿼리 대상들타입 필터를 명시하며, 채널 Multi의 첫 Block 중단 규칙과 구별
Single By Channel
결과 범위: 지정 채널의 첫 Blocking Hit
읽을 때의 경계: true는 blocking hit 발견. Overlap만 있는 대상은 이 결과로 얻지 않음
Multi By Channel
결과 범위: 첫 Block까지의 Overlap과 그 Block
읽을 때의 경계: Block 뒤를 관통해 모두 수집하지 않음. bool은 Block 유무이므로 false여도 Overlap 배열이 있을 수 있음
Multi By Object Type
결과 범위: 요청한 타입에 속한 쿼리 대상들
읽을 때의 경계: 타입 필터를 명시하며, 채널 Multi의 첫 Block 중단 규칙과 구별

계약 예시 · Channel Multi, Overlap만 발견

Overlap 결과 배열이 있어도 Blocking Hit가 없으면 bool=false입니다.

계약 예시 · Channel Multi, Block도 발견

그 앞의 Overlap들과 첫 Block을 반환하며 bool=true입니다. 그 Block 뒤의 대상을 모두 수집하는 호출은 아닙니다.

다른 행은 API 계약 비교이며 실제 다중 히트 실행 결과가 아닙니다. 이 예제의 시작점은 Pawn의 EyesViewPoint이고 FollowCamera와 같다고 가정하지 않습니다. 자기 자신 제외·충돌 형상·쿼리 참여를 함께 확인합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명시작 X 0 cm, 길이 1000 cm, +X 방향의 가정에서 A는 100 cm Overlap, 벽은 300 cm Block, B는 700 cm Block이다. A에서 계속 검사한 뒤 첫 Block 벽을 Single 결과로 반환한다. 벽 뒤 B는 이 결과에 들어오지 않으며 B로 향하는 간선도 없다. Multi 반환 배열과 bool은 아래 표 및 본문의 별도 계약으로 비교한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
조회 가정

설명용 0..1000 cm에 대상 응답을 둔다.

  • A: 100 cm Overlap
  • 벽: 300 cm Block
  • 원 FireWeapon의 10000 cm 코드와 별도 설명 사례임을 명시한다.
반환 경계

이 query는 첫 Block 벽까지의 결과를 정한다.

  • 첫 Block: 벽 300 cm
  • OverlapA: Block 아님
  • 간선은 논리 반환 범위이며, 엔진 내부 탐색 순서를 보장하지 않는다.
뒤 대상 제외

벽 뒤 B의 700 cm는 이 반환 범위에 수집되지 않는다.

  • B: 700 cm 미수집
  • 종료 범위: 벽까지
  • 벽을 통과하는 피해 권한을 이 조회 결과에 추가하지 않는다.

위 그림의 위쪽은 설명을 위해 정한 배치에서 Single By Channel이 무엇을 돌려주는지 보여 줍니다. 시작점 X=0cm에서 +X 방향으로 1000cm 길이의 선을 쏘고, 선 위에는 100cm에 Overlap으로 응답하는 A, 300cm에 Block으로 응답하는 벽, 700cm에 Block으로 응답하는 B가 있습니다. 쿼리는 A를 지나 계속 검사하고, 첫 Block인 벽을 결과로 돌려주며 이때 반환값은 참입니다. 벽 뒤의 B는 그림에서도 따로 떨어진 영역에 놓여 있고 B로 가는 선이 없는데, 첫 Block 뒤의 대상은 이 결과에 들어오지 않기 때문입니다. 그림 아래의 설명처럼 이 1000cm는 설명용 가정이며, 뒤에 나올 실제 FireWeapon 코드의 기본 거리는 10000cm입니다.

그림의 표는 필터와 반환 범위를 비교합니다. Single By Channel은 지정한 채널의 첫 Blocking Hit를 돌려주고, 반환값 true는 Blocking Hit를 찾았다는 뜻이므로 Overlap만 하는 대상은 이 결과로 얻을 수 없습니다. Multi By Channel은 첫 Block까지의 Overlap들과 그 Block을 돌려주는데, Block 뒤까지 뚫고 들어가 모두 모으지는 않습니다. 이 함수의 반환값도 Block이 있었는지를 뜻하므로, false여도 Overlap 결과 배열이 채워져 있을 수 있습니다. Multi By Object Type은 요청한 타입에 속한 쿼리 대상들을 돌려주며, 채널 Multi의 첫 Block에서 멈추는 규칙과는 다릅니다.

표 아래의 두 예는 Channel Multi의 반환 규칙을 정리합니다. Overlap만 찾았다면 결과 배열이 있어도 Blocking Hit가 없으므로 반환값은 false이고, Block도 찾았다면 그 앞의 Overlap들과 첫 Block을 돌려주며 반환값은 true입니다. 이 행들은 API 규칙의 비교이지 실제로 여러 히트를 실행한 결과가 아닙니다. 단계 그림도 같은 배치를 가정하고, 첫 Block인 벽까지가 이 쿼리의 반환 범위이며 벽 뒤의 B는 모이지 않는다는 점을 차례로 짚습니다. 그림의 화살표는 논리적인 반환 범위를 나타낼 뿐 엔진 내부의 탐색 순서를 뜻하지 않으며, 벽 뒤까지 피해를 줄 권한을 이 결과가 주지도 않습니다.


C++로 총 쏘기 트레이스 만들기

라인 트레이스 함수는 주로 UWorld와 블루프린트용 함수 라이브러리인 UKismetSystemLibrary에 정의되어 있으며, C++에서는 보통 UWorld 포인터로 부릅니다. 다음은 캐릭터가 바라보는 방향으로 트레이스를 쏘는 FireWeapon 함수입니다.

MyCharacter.h (예시: 캐릭터가 총을 쏘는 함수)
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;

public:
    // 마우스 왼쪽 클릭 시 발사되는 함수 (블루프린트에서 호출 가능하도록)
    UFUNCTION(BlueprintCallable, Category = "Combat")
    void FireWeapon();

    // 트레이스 길이를 에디터에서 조절할 수 있도록 노출
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
    float TraceDistance = 10000.0f; // 100미터
};

헤더에는 블루프린트에서도 부를 수 있는 FireWeapon과, 에디터에서 사거리를 조절할 TraceDistance를 선언했습니다. 기본값 10000은 cm 단위이므로 100m입니다.

MyCharacter.cpp
#include "MyCharacter.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/SpringArmComponent.h" // 카메라 암 컴포넌트
#include "Camera/CameraComponent.h"         // 카메라 컴포넌트
#include "Kismet/KismetMathLibrary.h"       // Rotator To Vector 등을 위해 (선택 사항)
#include "DrawDebugHelpers.h"               // 디버그 드로잉을 위해
#include "Engine/World.h"
#include "CollisionQueryParams.h"

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    // ... 기존 컴포넌트 설정 (CapsuleComponent, Mesh, SpringArm, Camera 등) ...
}

void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
}

void AMyCharacter::FireWeapon()
{
    // 월드 포인터 확인
    UWorld* World = GetWorld();
    if (!World)
    {
        UE_LOG(LogTemp, Error, TEXT("World is null. Cannot perform trace."));
        return;
    }

    // 1. Pawn의 눈 위치와 시야 방향을 시작 기준으로 사용
    FVector StartLocation;
    FRotator CameraRotation;
    GetActorEyesViewPoint(StartLocation, CameraRotation);
    // 또는 ACharacter의 EyeHeight 등을 사용할 수 있습니다.
    // FVector StartLocation = GetMesh()->GetSocketLocation(TEXT("MuzzleSocket")); // 총구 소켓이 있다면

    // 2. 시야 방향과 사거리로 종료 지점 결정
    FVector EndLocation = StartLocation + (CameraRotation.Vector() * TraceDistance);

    // 3. 트레이스 파라미터 설정
    FCollisionQueryParams TraceParams;
    TraceParams.AddIgnoredActor(this); // 트레이스 시 자기 자신(캐릭터)은 무시
    TraceParams.bTraceComplex = true;  // 복잡 충돌 쿼리 요청; 실제 형상은 에셋의 Collision Complexity에도 따름
    TraceParams.bReturnPhysicalMaterial = false; // 물리 재질 정보 반환 여부

    // 4. 충돌 결과 정보 구조체
    FHitResult HitResult;

    // 5. 라인 트레이스 실행!
    // LineTraceSingleByChannel(히트 결과, 시작점, 끝점, 충돌 채널, 쿼리 파라미터)
    // ECC_Visibility는 Project Settings의 Collision -> Trace Channels에서 기본으로 정의된 채널
    bool bHit = World->LineTraceSingleByChannel(HitResult, StartLocation, EndLocation, ECollisionChannel::ECC_Visibility, TraceParams);

    // 6. 트레이스 결과 처리
    if (bHit)
    {
        // 뭔가에 맞았다!
        UE_LOG(LogTemp, Warning, TEXT("Trace Hit: %s at %s"), *GetNameSafe(HitResult.GetActor()), *HitResult.Location.ToString());

        // 히트한 액터, 컴포넌트, 위치, 노멀 벡터 등의 정보에 접근
        AActor* HitActor = HitResult.GetActor();
        UPrimitiveComponent* HitComponent = HitResult.GetComponent();
        FVector HitLocation = HitResult.Location;
        FVector HitNormal = HitResult.Normal;

        // 여기에 게임플레이 로직 구현 (예: 데미지 적용, 파티클 생성 등)
        if (HitActor)
        {
            // 예시: 맞은 액터에게 데미지 적용 (추후 데미지 시스템 학습)
            // UGameplayStatics::ApplyDamage(HitActor, 10.0f, GetController(), this, UDamageType::StaticClass());
        }
    }
    else
    {
        // 아무것도 맞지 않았다
        UE_LOG(LogTemp, Warning, TEXT("Trace Missed."));
    }

    // 7. 디버그 드로잉 (개발 중에 트레이스 시각화)
    DrawDebugLine(World, StartLocation, EndLocation, bHit ? FColor::Red : FColor::Green, false, 5.0f, 0, 3.0f);
    if (bHit)
    {
        DrawDebugSphere(World, HitResult.Location, 25.0f, 12, FColor::Red, false, 5.0f);
    }
}

함수는 먼저 트레이스의 시작점과 끝점을 정합니다. GetActorEyesViewPoint는 폰의 눈 위치와 시야 방향을 돌려주므로, 시작점은 눈 위치이고 끝점은 거기서 시야 방향으로 TraceDistance만큼 간 지점입니다. 이 위치는 3인칭 FollowCamera의 위치와 같다고 가정할 수 없으므로, 카메라를 기준으로 쏘고 싶다면 실제 카메라 컴포넌트의 위치와 방향을 써야 합니다.

FCollisionQueryParams는 트레이스의 추가 설정입니다. AddIgnoredActor(this)로 자기 자신을 무시해 캐릭터 자신에게 맞는 일을 막습니다. bTraceComplex는 단순 충돌 대신 복잡 충돌을 쓰도록 요청하는 값인데, 실제로 어느 형상을 쓰는지는 에셋의 Collision Complexity 설정에도 영향을 받으므로 비용과 정확도는 대상 형상에 따라 확인합니다.

결과는 FHitResult에 담깁니다. 무언가에 맞으면 맞은 액터, 컴포넌트, 충돌 위치, 충돌 지점의 법선 같은 정보가 채워집니다. World->LineTraceSingleByChannel(...)이 실제로 트레이스를 실행하며, 여기서는 ECC_Visibility, 즉 프로젝트 설정의 Collision에서 트레이스 채널로 기본 정의된 Visibility 채널을 씁니다. 이 Single 호출은 이 채널에 대한 첫 Block 결과를 돌려주고, Overlap 결과의 배열이 필요하면 Multi 버전을 씁니다.

마지막의 DrawDebugLine과 DrawDebugSphere는 개발하는 동안 뷰포트에 트레이스 선과 맞은 지점을 그려 줍니다. 맞았으면 빨간색, 빗나갔으면 초록색 선이 5초 동안 보입니다. 게임을 출시할 때는 이 코드를 지우거나 디버그 빌드에서만 동작하게 해야 합니다.

직접 시험해 보기

헤더의 FireWeapon 선언과 .cpp 정의를 실제 캐릭터 클래스에 넣고, 플레이어 입력 이벤트에서 한 번 부르도록 연결합니다. 이 예제는 입력 매핑이나 카메라 컴포넌트까지 만들어 주는 완성 프로젝트가 아닙니다. 쏠 방향에 쿼리에 참여하는 물체를 두고 Visibility 응답을 Block으로 맞춘 뒤, 로그와 디버그 선을 비교합니다. 그다음 응답 하나만 바꿔 결과가 어떻게 달라지는지 확인합니다. 피해를 주는 ApplyDamage 호출은 주석으로 남겨 두었으므로, Trace Hit 로그만 보고 실제로 피해가 들어갔다고 판단하면 안 됩니다.

앞 그림의 배치를 다시 예로 들면, Single By Channel로 X=0cm에서 +X 방향으로 1000cm, 즉 10m의 선을 쏠 때 다른 필터나 처음부터 겹쳐 있는 문제가 없다면 결과는 첫 Block인 벽입니다. A가 벽보다 앞에 있거나 B도 선 위에 있다는 이유로 그들을 고르지 않습니다.

더 깊이: 단순 충돌과 복잡 충돌

메시의 충돌 형상은 두 종류입니다. 단순 충돌(simple collision)은 상자, 구, 캡슐, 볼록 껍질(convex hull) 같은 기본 도형으로 만든 형상이고, 복잡 충돌(complex collision)은 메시의 삼각형을 그대로 쓰는 트라이메시(trimesh)입니다. 기본 설정에서는 두 형상이 모두 만들어지고, 물리 엔진은 쿼리가 단순 쿼리인지 복잡 쿼리인지에 따라 검사할 형상을 고릅니다. 앞의 코드에서 bTraceComplex를 true로 둔 것이 복잡 쿼리를 요청한 경우입니다.

에셋의 Collision Complexity는 이 선택을 바꿉니다. Use Simple Collision As Complex로 두면 복잡 쿼리도 단순 형상으로 검사하므로 트라이메시를 굽지 않아 메모리를 아끼고, 형상이 더 단순해 성능이 좋아질 수도 있습니다. 반대로 Use Complex Collision As Simple로 두면 단순 쿼리도 트라이메시로 검사하는데, 이렇게 설정한 물체는 스스로 물리 시뮬레이션을 할 수 없고, 시뮬레이션되는 다른 물체가 와서 부딪히는 대상으로만 쓰입니다. 그래서 bTraceComplex를 켰는데도 맞은 지점이 단순한 상자 모양을 따른다면, 트레이스 코드보다 대상 에셋의 Collision Complexity를 먼저 확인합니다. 각 설정은 Simple versus Complex Collision 문서에 정리되어 있습니다.


트레이스 채널과 오브젝트 채널

라인 트레이스는 충돌 채널 시스템과 밀접하게 연결되어 있습니다. 프로젝트 설정의 Collision에는 오브젝트 채널(Object Channels)과 트레이스 채널(Trace Channels)이 있습니다. 오브젝트 채널은 Pawn, WorldStatic, PhysicsBody처럼 액터나 컴포넌트 자체가 어떤 종류의 물체인지 정하고, 트레이스 채널은 Visibility, Camera처럼 트레이스가 어떤 종류의 질문인지 정합니다.

트레이스를 실행하면 함수에 지정한 트레이스 채널에 대해, 월드의 각 물체가 자신의 충돌 프로파일에서 그 채널에 어떻게 응답하는지에 따라 충돌 여부가 정해집니다. 그래서 결과를 읽을 때는 채널 응답과 Single, Multi 함수의 반환 규칙을 함께 봐야 합니다.

트레이스를 부르기 전에는 어떤 대상을 찾을지부터 정합니다. LineTrace…ByChannel 계열은 대상이 지정한 채널에 어떻게 응답하는지 검사하므로, Visibility, Camera, 커스텀 트레이스 채널 가운데 목적에 맞는 것을 고릅니다. WorldStatic, WorldDynamic, Pawn 같은 오브젝트 타입만 찾으려면 FCollisionObjectQueryParams에 타입을 넣고 LineTrace…ByObjectType 계열을 씁니다. ECC_Pawn 같은 값을 ByChannel 함수의 채널 인자에 넣는다고 오브젝트 타입 필터가 되지는 않습니다.


정리

  • 라인 트레이스는 두 점 사이에 가상의 선을 그어 만나는 물체를 찾는 쿼리로, 충돌을 기다리지 않고 필요한 순간에 월드를 검사합니다.
  • 트레이스는 라인, 스피어, 박스, 캡슐 모양이 있고, 채널별, 오브젝트 타입별, 프로파일별 필터와 Single, Multi 버전이 있습니다.
  • Single By Channel은 첫 Blocking Hit를 돌려주며, 반환값 true는 Blocking Hit를 찾았다는 뜻입니다.
  • Multi By Channel은 첫 Block까지의 Overlap들과 그 Block을 돌려주고, Block 뒤까지 모으지 않습니다. 반환값이 false여도 Overlap 배열은 있을 수 있습니다.
  • FCollisionQueryParams로 자기 자신을 무시하고 복잡 충돌을 요청하며, 결과는 FHitResult의 액터, 컴포넌트, 위치, 법선으로 읽습니다.
  • 오브젝트 타입으로 찾으려면 FCollisionObjectQueryParams와 ByObjectType 함수를 쓰며, 채널 인자에 오브젝트 타입 값을 넣는 것과는 다릅니다.

연습 문제

1. Multi로 바꾸면

앞 그림의 배치(A는 100cm에 Overlap, 벽은 300cm에 Block, B는 700cm에 Block)를 그대로 두고 Channel Multi 함수를 쓴다고 가정합니다. 결과 배열에는 무엇이 들어가고 반환값은 무엇일까요?

해설 보기

벽 앞의 A와 첫 Block인 벽이 배열에 들어가고, 반환값은 true입니다. Channel Multi는 첫 Block까지의 Overlap들과 그 Block을 돌려주므로, 벽 뒤의 B까지 뚫고 모두 모으지는 않습니다.

이 문제는 Channel Multi 규칙을 연습하기 위한 가정이며, 이 글의 FireWeapon 코드는 Single을 씁니다.

2. Block을 모두 없애면

같은 배치에서 벽과 B의 응답만 Ignore로 바꾸고 A의 Overlap은 그대로 두었습니다. Channel Multi의 결과 배열과 반환값은 어떻게 달라질까요? 반환값이 false라는 이유로 결과 배열이 비어 있다고 처리해도 될까요?

해설 보기

A의 Overlap 결과는 배열에 남을 수 있지만, Blocking Hit가 없으므로 반환값은 false입니다. 반환값은 Block이 있었는지를 뜻할 뿐이므로, false라고 배열이 비었다고 처리하면 A를 놓치게 됩니다. 배열의 내용은 배열 자체를 확인해야 합니다.

또 채널 응답을 바꾸는 것과 오브젝트 타입 필터를 쓰는 것은 서로 다른 선택이라는 점도 함께 기억합니다.

3. 3인칭 카메라와 어긋나는 조준

3인칭 게임에서 화면 가운데의 조준점과 실제로 맞는 지점이 조금씩 어긋납니다. 이 글의 FireWeapon 코드에서 원인으로 의심할 부분은 어디일까요?

해설 보기

시작점과 방향을 GetActorEyesViewPoint로 얻는 부분입니다. 이 함수는 폰의 눈 위치를 돌려주는데, 3인칭에서는 화면을 비추는 FollowCamera가 캐릭터 뒤쪽에 떨어져 있으므로 두 위치와 방향이 다릅니다. 그래서 화면 가운데에서 보이는 지점과 눈에서 쏜 선이 만나는 지점이 어긋납니다.

조준점을 맞추려면 실제 카메라 컴포넌트의 위치와 방향으로 먼저 트레이스해 조준 지점을 찾고, 필요하면 총구나 눈에서 그 지점으로 다시 트레이스하는 식으로 설계합니다.

트레이스의 종류와 Single, Multi의 반환 규칙은 Epic의 Traces Overview 문서에서 확인할 수 있습니다.

이것으로 5장을 마칩니다. 5장에서는 충돌 채널과 프로파일로 물체 사이의 응답을 정하는 방법, 트리거 박스와 오버랩 이벤트로 영역의 진입과 이탈을 감지하는 방법, 물리 시뮬레이션에 힘과 충격량을 주는 방법, 그리고 라인 트레이스로 월드에 직접 질문하는 방법을 살펴봤습니다. 다음 장의 UMG 시스템 소개와 위젯 구조부터는 언리얼의 UI 시스템인 UMG로 위젯을 만들고, C++에서 위젯을 제어하고, HUD와 데이터 바인딩, UI 입력과 애니메이션을 다룹니다.