【UE5】囲まれた壁の内外判定の作り方

プログラマーの尾関です。

今回は囲まれた壁の内外判定を実装する方法について書きます。

注意点

今回の判定方法は Half-Space (平面とその法線方向) を使った簡易的なもので、凸領域(Convex)限定 の手法です。

凹領域 (Concave) を "判定不可" と書いていますが、正確には開いている方向は無限距離で内部判定となります。

プロジェクトの作成

今回は内外判定がわかりやすいように「トップダウン」のプロジェクトを C++ で作成しました。

壁の配置

壁に使うスタティックメッシュは メッシュの中心が Pivot となっているものが良いので「SM_ChamferCube」を使いました。

足元に Pivot があるとより良かったのですが、今回はこれで代用。

マテリアルは透過しているものが良いと思ったので、Blend Modeを Translucent にして 0.8 で透過させました。

ということで以下のようなスタティックメッシュアクターを作成してレベルに配置。

それとレベルブループリントからの検索用として Actorタグに "Wall" を指定。

地面に埋まっているのが無駄ではあるのですが、実験用ということでひとまず。

内外判定クラスの作成

ツールからC++クラスを作成。

アクターを継承した「CPP_HalfSpace」というクラスを作成しました。

ヘッダファイルには以下のように記述しました。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/StaticMeshActor.h"
#include "CPP_HalfSpace.generated.h"

UCLASS()
class HALFSPACETEST_API ACPP_HalfSpace : public AActor
{
	GENERATED_BODY()
	
public:	
	ACPP_HalfSpace();

public:	
	virtual void Tick(float DeltaTime) override;

	// StaticMeshActorを追加する関数
	UFUNCTION(BlueprintCallable, Category = "HalfSpace")
	void AddMeshActor(AStaticMeshActor* MeshActor);

private:
	// PawnがHalf-Space内にいるかどうかを判定する関数
	bool _IsPawnInHalfSpace(APawn* Pawn) const;

	// デバッグ用の壁を描画する関数
	void _DrawDebugWall(bool isInHalfSpace);

private:
	UPROPERTY()
	TArray<AStaticMeshActor*> m_MeshActors;
};

レベルブループリント側から StaticMeshActorを受け取り、それを使って内外判定を行うクラスです。

cpp側の実装は以下の通り。

#include "CPP_HalfSpace.h"
#include "Engine/Engine.h"

ACPP_HalfSpace::ACPP_HalfSpace()
{
	PrimaryActorTick.bCanEverTick = true;
}

// StaticMeshActorを追加.
void ACPP_HalfSpace::AddMeshActor(AStaticMeshActor* MeshActor)
{
	if(MeshActor) {
		m_MeshActors.Add(MeshActor);
	}
}

void ACPP_HalfSpace::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// Pawnを取得.
	APawn* pawn = GetWorld()->GetFirstPlayerController()->GetPawn();
	// PawnがHalf-Space内にいるかどうかを判定.
	bool isInHalfSpace = _IsPawnInHalfSpace(pawn);
	if(isInHalfSpace) {
		// ヒット.
		GEngine->AddOnScreenDebugMessage(0, 0.1f, FColor::Yellow, TEXT("Pawn is in the room."));
	}
	else {
		// 外に出た.
		GEngine->AddOnScreenDebugMessage(0, 0.1f, FColor::Cyan, TEXT("Pawn is out the room."));
	}

	// XY平面での各メッシュの境界を線分でデバッグ描画.
	_DrawDebugWall(isInHalfSpace);
}

// PawnがHalf-Space内にいるかどうかを判定.
bool ACPP_HalfSpace::_IsPawnInHalfSpace(APawn* Pawn) const
{
	if(Pawn == nullptr) {
		return false;
	}

	// Pawnの位置を取得.
	FVector PawnLocation = Pawn->GetActorLocation();

	for(const auto& Actor : m_MeshActors) {
		if(Actor == nullptr) {
			continue;
		}
		FVector Origin = Actor->GetActorLocation(); // 中心座標.
		FVector Forward = Actor->GetActorForwardVector(); // 正面.
		// 内積で内側かどうかを判定.
		if(FVector::DotProduct(PawnLocation - Origin, Forward) < 0.0f) {
			return false; // PawnはHalf-Space内にいない
		}
	}

	// すべてのHalf-Space内部にいる.
	return true;
}


// デバッグ用の壁を描画する関数
void ACPP_HalfSpace::_DrawDebugWall(bool isInHalfSpace)
{
	// 壁の線の色.
	FColor wallColor = isInHalfSpace ? FColor::Red : FColor::Green;

	for(auto* actor : m_MeshActors) {
		if(actor == nullptr) {
			continue;
		}
		// 足元の中心を取得.
		FVector origin = actor->GetActorLocation();
		// 中心を球体で描画.
		DrawDebugSphere(GetWorld(), origin, 20.f, 12, wallColor, false, -1.f, 0, 1.f);
		// 正面ベクトルを取得.
		FVector forward = actor->GetActorForwardVector();
		// 正面ベクトルをデバッグ描画.
		DrawDebugLine(GetWorld(), origin, origin + forward * 1000.f, FColor::Yellow, false, -1.f, 0, 1.f);
		// 正面から垂直なベクトルを取得.
		FVector right = forward.RotateAngleAxis(90.f, FVector::UpVector);
		FVector left = forward.RotateAngleAxis(-90.f, FVector::UpVector);
		// 正面から垂直なベクトルをデバッグ描画.
		DrawDebugLine(GetWorld(), origin, origin + right * 10000.f, wallColor, false, -1.f, 0, 5.f);
		DrawDebugLine(GetWorld(), origin, origin + left * 10000.f, wallColor, false, -1.f, 0, 5.f);
	}
}

重要となるのが_IsPawnInHalfSpace()の以下のコードですね。

		FVector Origin = Actor->GetActorLocation(); // 中心座標.
		FVector Forward = Actor->GetActorForwardVector(); // 正面.
		// 内積で内側かどうかを判定.
		if(FVector::DotProduct(PawnLocation - Origin, Forward) < 0.0f) {
			return false; // PawnはHalf-Space内にいない
		}

壁の中心座標 (Origin) から Pawn (PawnLocation) への方向ベクトルと、壁の法線ベクトル (Forward) の内製を求めて、この値が "0.f" 以上であれば内側にいるかどうかを判定できます。

これをすべての壁と判定し、すべて内側であれば壁の中にいると判定できます。

この CPP_HalfSpace を BPクラスで継承し、レベルに配置します。

レベルブループリントの作成

レベルブループリントを作成しレベルに配置した壁メッシュを BP_HalfSpaceに登録します。

実行して動作確認

実行すると、囲まれた壁の中にいるときは、それぞれの壁の補助線が赤くなります。

おしまい

なお Unreal Engineには「FConvexVolume」という今回紹介したよりも高度な機能があります。

  1. FPlaneを追加するだけで凸領域を作れ、判定は IntersectPoint() などで行える
  2. 点だけでなく球体やAABBとの交差判定も可能
  3. Unreal Engine標準の許容誤差判定が使われるため判定の精度が安定しやすい
  4. カメラの視錐台などで使われていることもあり、枯れている機能で信頼性が高い

公式ドキュメント→「FConvexVolume

FConvexVolumeは使用したことがないので、実際に使う機会があれば紹介記事を書くかもしれません。

以上、簡易的な内外判定の実装でした。

\ 最新情報をチェック /