【UE5】LineTrace(レイキャスト)の使い方【C++】

本稿では、UE5 C++ の LineTraceMultiByProfile 関数を使った際に、私が分かりにくいと感じた点について調査し、分かったことを共有したいと思います。
Unreal Engine の C++ の使い方を習得している方が対象となります。

動作環境
Visual Studio 2022
Unreal Engine 5.4.1
Windows 11 Home

LineTraceMultiByProfile 関数の使い方

読み方
ライントレースマルチバイプロフィール
または、ライントレースマルチバイプロファイル

複数あるライントレース関数の中で、一番ややこしいのが LineTraceMultiByProfile 関数だと思いますので、こちらに限定して説明します。

必要なインクルード

#include “Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h”

必要なモジュール

Engine

※Engine モジュールは最初から組み込まれているので、特に指定する必要はありません。

戻り値

ブロックによる接触を検出した場合 true を返し、それ以外は false を返します。

コリジョンプリセットにオーバーラップだけ設定している場合は常に false を返します。
関数名はプロファイルとなっていますが、実際はコリジョンプリセットのことを指しています。
引数

const UObject* WorldContextObject
const FVector Start
const FVector End
FName ProfileName
bool bTraceComplex
const TArray<AActor*>& ActorsToIgnore
EDrawDebugTrace::Type DrawDebugType
TArray<FHitResult>& OutHits
bool bIgnoreSelf
FLinearColor TraceColor
FLinearColor TraceHitColor
float DrawTime

WorldContextObject

GetWorld() などで取得できる UWorld* を指定します。

Start

線の始点を FVector で指定します。
アクターの場合は GetActorLocation() を使うことが多いです。
ベクトルの向きは考慮する必要はありません。

End

線の終点を FVector で指定します。
Start と End を結んだ線分とオブジェクトが重なっている場合、そのオブジェクトと接触したことを検出します。
検出したいオブジェクトと検出方法はコリジョンプリセット(Collision Preset)に設定します(後述)。

ProfileName

オブジェクトと当たったことを検出するためのコリジョンプリセットの名前を指定します。

プロジェクト設定 > コリジョン > プリセット
※画像をクリックまたはタップすると大きいサイズで表示します。

これも引数名が ProfileName となっているのですが、コリジョンプリセットの名前のことです。

bTraceComplex

以下の2種類のコリジョンのどちらに対して検出を行うかを指定します。

1.シンプルなコリジョン
2.複雑なコリジョン

この引数は、「複雑なコリジョン」を使って検出を行う場合に true を指定します。

一般論ですが、複雑なコリジョンを使った処理は計算が複雑になるので重いです。

ActorsToIgnore

特定のアクターを無視したい場合に、この配列にそのアクターを追加します。
ライントレースの始点がアクターの内部にある場合、そのアクター自身を検出してしまうため、この配列に自分自身のポインタ this を指定するか、後述の引数 bIgnoreSelf を使うことで、それを回避できます。

ActorsToIgnore.Add(this);

DrawDebugType

Start と End を結ぶ線分を表示する方法を指定します。
シッピング向けのパッケージでは表示されません。

以下の値を指定できます。

  • None
  • ForOneFrame
  • ForDuration
  • Persistent

None
線を表示しません。

ForOneFrame
ライントレース実行時の1フレームだけ線を表示(Tick などで毎フレームライントレースを実行する場合に便利です)。

ForDuration
引数 DrawTime に指定した時間だけ線を表示します。

Persistent
常に線を表示し、表示した線は消えません。

OutHits

ライントレースで検出したオブジェクトを格納する配列を指定します。
この配列の要素にアクセスすることで、ライントレースと接触したオブジェクトが分かります。
検出できなかった場合、OutHits.IsEmpty() == true になります。

オーバーラップを検出した場合、オーバーラップした全てのオブジェクトが追加されます。
ブロックを検出した場合、そのオブジェクトはこの配列の末尾に追加されます。
ブロックは最初に当たったオブジェクトだけ検出できます。

bIgnoreSelf

この引数に true を指定した場合、以下のように処理されます。

const AActor* IgnoreActor = Cast<AActor>(WorldContextObject);
if (IgnoreActor)
{
	Params.AddIgnoredActor(IgnoreActor);
}
else
{
	// find owner
	const UObject* CurrentObject = WorldContextObject;
	while (CurrentObject)
	{
		CurrentObject = CurrentObject->GetOuter();
		IgnoreActor = Cast<AActor>(CurrentObject);
		if (IgnoreActor)
		{
			Params.AddIgnoredActor(IgnoreActor);
			break;
		}
	}
}

WorldContextObject に GetWorld() を指定した場合、GetWorld() を持つアクターが対象になります。
WorldContextObject が AActor のサブクラスでない場合、GetOuter() をたどり、それが AActor のサブクラスなら、そのアクターが対象になります。

TraceColor

DrawDebugType が None 以外のときに有効です。
ライントレースがオブジェクトを検出して「いない」ときに、この引数に指定した色で線が表示されます。

TraceHitColor

DrawDebugType が None 以外のときに有効です。
ライントレースがオブジェクトを検出したときに、この引数に指定した色で線が表示されます。

DrawTime

DrawDebugType が ForDuration のときだけ有効です。
それ以外を指定した場合は無視されます。
ライントレースの線を表示する時間を秒で指定します。

使用例

複数のライントレースを角度を変えて実行するサンプルです。
このサンプルは、以下の記事に記載されている方法を C++ のコードにして拡張しやすくしたものです。

面倒なので C++ プロジェクト作成時に自動生成されたソースコードに処理を追加しています。
//{ から //} までが追加したコードです。

LineTraceTestCharacter.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

/* 略 */

UCLASS(config=Game)
class ALineTraceTestCharacter : public ACharacter
{
	/* 略 */		

protected:
	// APawn interface
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	// To add mapping context
	virtual void BeginPlay();

	//{
	virtual void OnTimer();
	//}

public:
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	/** Returns FollowCamera subobject **/
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

LineTraceTestCharacter.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "LineTraceTestCharacter.h"
/* 略 */

//{
#include "Runtime/Engine/Classes/Kismet/KismetMathLibrary.h"
#include "Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h"
#include "Runtime/Engine/Classes/Components/PrimitiveComponent.h"

namespace { namespace LineTraceTest {
	// デバッグ用の線の表示秒数
	constexpr float Draw_Time{ 0.5f };

	// ライントレースで検出するコリジョンプリセットの名前
	const FName Preset_Name{ TEXT("TestLineTrace") };

	// ライントレースの本数
	constexpr int32 Number_Of_Lines{ 5 };

	// ライントレースの角度(Z軸)
	const TArray<float> Angles = {
		0.0f, -30.0f, -15.0f, 15.0f, 30.0f
	};

	// ライントレースの長さ(単位は不明)
	const TArray<float> Lengths = {
		1000, 2000, 1500, 1500, 2000
	};

	// ライントレースの始点と終点をキャラの立ち位置からどれだけズラした位置に設定するか
	const FVector Offset{ 0.0f, 0.0f, 0.0f };

	// PrintString の文字色
	const TArray<FLinearColor> PrintColors = {
		FLinearColor::Blue, FLinearColor::Green, FLinearColor::Red, FLinearColor::Yellow, FLinearColor::White
	};

	void PrintBlockingHit(const UObject* world_context_object, const bool is_hit, const int32 index)
	{
		FString is_hit_string{ TEXT("Blocking Hit=") };
		is_hit_string.Append(FString::FormatAsNumber(static_cast<int32>(is_hit)));
		UKismetSystemLibrary::PrintString(world_context_object, is_hit_string, true, true, PrintColors[index], Draw_Time);
	}
	void PrintHits(const UObject* world_context_object, const TArray<FHitResult>& hit_results, const int32 index)
	{
		for (const FHitResult& result : hit_results)
		{
			UPrimitiveComponent* component{ result.Component.Get() };
			if (!IsValid(component)) { continue; }

			FString hit_object_name{};
			component->GetName(hit_object_name);
			UKismetSystemLibrary::PrintString(world_context_object, hit_object_name, true, true, PrintColors[index], Draw_Time);
		}
	}
}}
//}

DEFINE_LOG_CATEGORY(LogTemplateCharacter);

//////////////////////////////////////////////////////////////////////////
// ALineTraceTestCharacter

/* 略 */

void ALineTraceTestCharacter::BeginPlay()
{
	// Call the base class  
	Super::BeginPlay();

//{
	FTimerHandle   timer_handle  {};
	FTimerDelegate timer_delegate{};

	timer_delegate.BindUObject(this, &ALineTraceTestCharacter::OnTimer);
	constexpr bool is_timer_repeated{ true };

	GetWorldTimerManager().SetTimer(timer_handle, timer_delegate, LineTraceTest::Draw_Time, is_timer_repeated);
//}
}

//{
void ALineTraceTestCharacter::OnTimer()
{
	const FVector location      { GetActorLocation() };
	const FVector forward_vector{ GetActorForwardVector() };

	const     UObject*        world_context_object{ GetWorld() };
	const     FVector         start               { location + LineTraceTest::Offset };
	const     FVector         end                 { forward_vector };
	constexpr bool            is_trace_complex    { false };
	const     TArray<AActor*> actors_to_ignore    {};
	constexpr bool            is_ignore_self      { true  };

	constexpr EDrawDebugTrace::Type draw_debug_type{ EDrawDebugTrace::Type::ForDuration };
	const     FLinearColor          trace_color    { FLinearColor::Red };
	const     FLinearColor          trace_hit_color{ FLinearColor::Green };

	bool is_hit{ false };
	TArray<FHitResult> hit_results{};

	for (int32 i = 0; i < LineTraceTest::Number_Of_Lines; i++)
	{
		if (i >= LineTraceTest::Lengths.Num() || i >= LineTraceTest::Angles.Num()) { continue; }

		is_hit = UKismetSystemLibrary::LineTraceMultiByProfile(
				world_context_object,
				start, UKismetMathLibrary::RotateAngleAxis(start + end * LineTraceTest::Lengths[i], LineTraceTest::Angles[i], FVector(0.0f, 0.0f, 1.0f)),
				LineTraceTest::Preset_Name, is_trace_complex, actors_to_ignore,
				draw_debug_type, hit_results, is_ignore_self, trace_color, trace_hit_color, LineTraceTest::Draw_Time
			);

		LineTraceTest::PrintBlockingHit(world_context_object, is_hit, i);
		LineTraceTest::PrintHits(world_context_object, hit_results, i);
	}
}
//}

//////////////////////////////////////////////////////////////////////////
// Input

/* 略 */

サンプルコードの使用方法
  1. サードパーソンテンプレート、C++ を選択し、プロジェクト名を LineTraceTest にしてプロジェクトを作成します。
  2. UE エディタが起動したら、プロジェクト設定 > コリジョン > プリセットに TestLineTrace を追加します。
  3. Visual Studio でサンプルのソースファイルとヘッダファイルを開き、//{ ~ //} で挟んだ部分のコードをコピペします。
  4. ライブコーディングでビルドします。
  5. PIE で動作確認します。

TestLineTrace の設定をいろいろ変更してみて、どの設定のときに何とヒットするか?を確認すると、コリジョンプリセットの設定方法について、理解が深まると思います。

こんなことして遊べます。

以降はライントレースについての補足情報です。


ライントレースとは?

ゲームプログラミングでよく「レイを飛ばす」と言われる機能は UE では「ライントレース」と呼ばれます。
本来は「レイキャスト」と呼ばれています。

UE の LineTrace 関数の処理を掘り下げて行くと、最終的に Raycast 関数にたどり着くので、ライントレースはレイキャストと同義です。

レイキャストは、キャラの任意の点から、まっすぐな線を伸ばして行き、その線とぶつかったオブジェクトを調べることです。
レイはレーザーのようにまっすぐ伸びる光線のことで、キャストには色々な意味がありますが、この場合は「投げる」という意味になります。

UE の場合は線だけでなく、球などの立体を使って判定する機能もあるため、レイキャストとは違います。
そのため、任意の立体と接触しているオブジェクトを調べるという意味でトレースという言葉を使ったんだろうと想像できます。

公式のリファレンス

UKismetSystemLibrary::LineTraceMultiByProfile

公式の説明文の補足

公式の説明はとても分かりにくいので補足します。

レイの検出方法は物理(エンジン)のコリジョンと同じく2種類あります。

1.オーバーラップ
2.ブロック

まず、オーバーラップですが、レイがオブジェクトと重なったかどうかを検出する方法です。
これはレイをブロックする(当たった位置で止める)効果がないため、オブジェクトを貫通します。
レイにオブジェクトが複数接触していれば、全てのオブジェクトとオーバーラップします。
ただし、そのオブジェクトがオーバーラップで検出できる設定になっている場合に限ります。

ブロッキングヒットは、オブジェクトがレイをブロックしたかどうかを検出することです。
レイのブロック対象になっていないオブジェクトはブロックしません。
最初にレイと当たったオブジェクトはレイをブロックしてしまうので、レイはそこで途切れます。
そのため、LineTraceMulti 関数は複数のブロッキングヒットを検出できません。
一番最初にブロッキングヒットしたオブジェクトが、引数 OutHits の末尾の要素に1つだけ追加されるのは、そのためです。
従って、複数のオブジェクトとレイが接触したかどうかを検出したい場合、ブロックではなく、オーバーラップで検出するように設定する必要があります。

LineTraceMultiByProfile 関数で複数のオブジェクトを検出するには、コリジョンプリセット(Collision Preset)でオーバーラップを指定する必要があります。
オーバーラップの検出だけ行う場合、ブロッキングヒットが起きないため、LineTraceMultiByProfile 関数は常に false を返すようになります。


以上

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です