yamonメモ.cpp

ue4で勉強中 日々の進捗を簡単に残します。

接地判定を実装してみた

前回までのあらすじ
AnimationBlueprintでEnumを使って遷移させてみた - yamonメモ.cpp

AnimationBlueprintを作成し、Enum指定でモーション遷移できるようになった。
実際に動かしてみて必要に感じた地形に合わせたRotationの補正処理をしてみる。

UCharacterMovementComponentの調査

今後地面に立っている、壁にぶつかっているなどの判定も必要なため、
まずは、エンジンソースを見てみることにした。

どうやら状態を表すMovementModeがあり、
それをみて速度や物理設定を更新している様子。

気になる「状態の更新処理」は、
ざっくりいうと↓のような感じ。

・落下中判定はZ方向に速度が入っているか
・水中判定はボリューム
・ナビメッシュに沿って動く状態も別で管理
・MOVE_Flyingは明示的に使用処理を入れないと使えない?

なんとなくわかったので、必要そうな機能を追加する。

アクター側の拡張

まず必要なのが、現在接地しているかどうかの判定。
アクターがコリジョン接触した際に関数を呼ぶようにしたい。

Unreal C++ | Register Component Hit
を参考にしながら実装してみる。

// MyActor.h
	virtual void PostInitializeComponents() override;

	//----------------------
	// Collision
	//----------------------
protected:
	UFUNCTION(BlueprintCallable)
	void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

	//----------------------
	// ActorComponent
	//----------------------
protected:
	TArray<UMyActorComponent*> MyActorComponents;
// MyActorComponent.h
	//----------------------
	// Collision
	//----------------------
public:
	virtual void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) {}

ヘッダーに必要な定義を用意して、

// MyActor.cpp
void AMyActor::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	auto RootPrimitiveComponent = Cast<UPrimitiveComponent>(RootComponent);

	if (RootPrimitiveComponent)
	{
		RootPrimitiveComponent->OnComponentHit.AddDynamic(this, &AMyActor::OnHit);
	}

	GetComponents<UMyActorComponent>(MyActorComponents);
}

void AMyActor::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	for (auto Component : MyActorComponents)
	{
		Component->OnHit(HitComp, OtherActor, OtherComp, NormalImpulse, Hit);
	}
}

OnHitが呼ばれるようにし、
各Componentにも通知するようにしてみた。

FHitResultのキャッシュ

OnHitで、壁と地面に振り分けてキャッシュしておくようにする。
本来であれば、アクターに「私は壁です」のような情報を持たせて、振り分けるのが良いのかな?

とりあえずは汎用的に使えそうなImpactNormalを見て判定してみることにした。

// MyActor.h
// HitResult管理
struct FHitResultElement
{
	TOptional<FHitResult> HitResult;
	float Timer = 0.0f;

	bool Set(const FHitResult& Hit)
	{
		const float ValidTimer = 0.1f;
		Timer = ValidTimer;

		HitResult.Emplace(Hit);

		return true;
	}
	void Tick(float DeltaTime)
	{
		if (Timer <= 0.0f)
		{
			HitResult.Reset();
		}
		else
		{
			Timer -= DeltaTime;
		}
	}

	FORCEINLINE bool IsSet() const { return HitResult.IsSet(); }
};
USTRUCT(BlueprintType)
struct FHitResultManager
{
	GENERATED_BODY()

	FHitResultElement HitResultMyActor;
	FHitResultElement HitResultWall;
	FHitResultElement HitResultGround;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	bool	bDebug = false;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, DisplayName = "壁判定:法線Zがこれ以下")
	float	WallNormalZ = 0.2f;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, DisplayName = "地面判定:法線Zがこれ以上")
	float	GroundNormalZ = 0.8f;

	bool Set(AActor* OtherActor, const FHitResult& Hit)
	{
		if (Hit.bBlockingHit)
		{
			if (bDebug)
			{
				DrawDebugLine(Hit.ImpactPoint, Hit.ImpactPoint + (Hit.ImpactNormal * 100.0f));
			}

			if (Cast<AMyActor>(OtherActor))
			{
				return HitResultMyActor.Set(Hit);
			}
			else if (Hit.ImpactNormal.Z <= WallNormalZ)
			{
				return HitResultWall.Set(Hit);
			}
			else if (Hit.ImpactNormal.Z >= GroundNormalZ)
			{
				return HitResultGround.Set(Hit);
			}
		}
		return false;
	}
	void Tick(float DeltaTime)
	{
		HitResultMyActor.Tick(DeltaTime);
		HitResultWall.Tick(DeltaTime);
		HitResultGround.Tick(DeltaTime);
	}

	FORCEINLINE bool IsHitActor()		const { return HitResultMyActor.IsSet(); }
	FORCEINLINE bool IsHitWall()		const { return HitResultWall.IsSet(); }
	FORCEINLINE bool IsHitGround()	const { return HitResultGround.IsSet(); }
};

これをアクターが持っておいて、
Tick, OnHitに処理を追加すれば、IsHit関数が使用可能になる。

まとめ

・OnComponentHit.AddDynamic(this, &AMyActor::OnHit)で衝突した瞬間を取る。
・FHitResultをキャッシュしておいて、簡易的な判定として利用

これで、
コンポーネント側で衝突した瞬間を取りたければ、OnHitをoverrideしておき、
壁に接触しているかだけを見たければ、Actor->IsHitWallで取れるようになった。

法線はZ方向が0.2以下なら壁、0.8以上なら床としているがとりあえずこれで良いかな?
キャッシュは0.1秒間にしているけど、どのくらいが妥当なのかな?
あ、今は当たらなかった瞬間を取れてないのか(最短でも0.1秒後にわかる)
・・また考えておこう。

(それにしてもc++での設計の話になると、貼り付ける画像がないな)

今日はここまで。
次は向き補正を実装してみようかな(短くなるかも?)