【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」という今回紹介したよりも高度な機能があります。
- FPlaneを追加するだけで凸領域を作れ、判定は IntersectPoint() などで行える
- 点だけでなく球体やAABBとの交差判定も可能
- Unreal Engine標準の許容誤差判定が使われるため判定の精度が安定しやすい
- カメラの視錐台などで使われていることもあり、枯れている機能で信頼性が高い
公式ドキュメント→「FConvexVolume」
FConvexVolumeは使用したことがないので、実際に使う機会があれば紹介記事を書くかもしれません。
以上、簡易的な内外判定の実装でした。

