接地判定を実装してみた
前回までのあらすじ
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++での設計の話になると、貼り付ける画像がないな)
今日はここまで。
次は向き補正を実装してみようかな(短くなるかも?)