障害物を自動で避ける仕組みを実装してみた
前回のあらすじ
MovementComponentを作り始めてみた - yamonメモ.cpp
MovementComponentの基礎部分は実装できたので、
続いて、歩いているアクターが進行方向にある障害物を見つけると
自動で左右に避ける仕組みを作ってみることにした。
アクターAから見たアクターBが右側にいるかどうかを判定する
まず、必要になる情報が、
・LineTraceで前方に伸ばしたレイでヒットした障害物の原点
・現在進行中のアクター自身の回転値
アクターAからアクターBへの回転値(Yaw)を計算し、
自分の回転値(Yaw)と比較して大きければ右方向、小さければ左方向。
ソースだとこんな感じ。
auto LineTraceComp = Actor->GetLineTraceComponent(); if (!LineTraceComp) { return; } FVector CurrentVel = Actor->GetVelocity(); if (CurrentVel.IsZero()) { // 動いていないアクターは対象外 return; } // LineTraceでHitしたアクター位置を取得 FVector HitActorLocation = FVector::ZeroVector; if (!LineTraceComp->GetNearestForwardHitActorLocation(&HitActorLocation)) { return; } const FRotator CurrentRot = Actor->GetCurrentRotation(); const FVector Dir = HitActorLocation - Actor->GetCurrentLocation(); const FRotator TargetRot = Dir.Rotation(); if (CurrentRot.Yaw > TargetRot.Yaw) { // 対象はアクターから見て右にいる } else { // 対象はアクターから見て左にいる }
現在の移動速度に計算した避け具合を反映させる
対象が右にいるなら左に、左にいるなら右に移動値を反映する。
単純に現在の速度に加算してみた
避ける瞬間に加速する見た目になってしまった。
(避けられた方イラっとするだろうな…)
単純な速度の加算だとそうなってしまうので、
現在の速度から再設定を行うようにしてみる。
アクターの回転値をとってきて、
少し回転させた状態で同じように速度を与えてみる。
if (CurrentRot.Yaw > TargetRot.Yaw) { // 対象はアクターから見て右にいる CurrentRot.Yaw += AvoidAddYaw; } else { // 対象はアクターから見て左にいる CurrentRot.Yaw -= AvoidAddYaw; } const FVector NewVel = CurrentRot.Quaternion().GetAxisY(); Actor->SetVelocity(NewVel * Speed);
これでうまくいく避けてくれる感じを出すことができた。
向かい合わせてもそれっぽく避けてくれる。
気になる点
同じ速度のアクターを縦に連ねて配置し、後ろを追従させた場合、
追い抜けないのに横にずれてしまう。
当たりそうな対象の速度も見て追い抜けそうなら横にずれ、
追い抜けなさそうならそのままを維持するようにしてみたい。
前が詰まっている時(どこに避けてもぶつかってしまう場合)は
「止まる」を選択肢に入れる実装もしてみたい。
また今度気になったらやってみよう。
雑談
先日
「UE4製TPSアクション EDF: IR事例解説セミナー in 大阪」
に行ってきました。
感想
ABPの設計って本当に難しい!
変に共通化しようとしても身動きとり辛いし…
コントローラー入力からモーションのブレンドに繋げる発想
凄い柔軟だしTPSにマッチしてて感動した。
Entryから空State(Entry)に繋くとTポーズの補間がかかってどうしようって問題。
自分も経験したことあるけど、
結局ゴリ押ししたような気がする(EntryにIdleのモーションを入れておくだけ)w
99秒の補間は盲点だった。
SubAnimInstanceって相当便利そうだな。
あまり個人だと使う機会少ないかもしれないけど。
感想終わり!楽しかった!
次は移動関連とはまた違うことがやりたいな。
・Statの表示を試してみる。
・フラスタムカリングを実装してみる。
・マーケットプレイスからアセットを取り込んでみる。
・ポーズ機能を実装してみる
どれかをやります!
MovementComponentを作り始めてみた
前回のあらすじ
接地判定を実装してみた - yamonメモ.cpp
地形判定を取れるようになったので、
本格的にキャラクターを速度ベースで動かしてみることにする。
追加したコンポーネントはActorComponent継承のMovementComponent。
速度や回転値等を計算して更新し続ける挙動を想定している。
直面した問題
何も考えずにアクターの正面方向に速度を与えてみると、
倒れてしまう…。(それよりGIF貼れたのか!!)
回転のロックで凌いでいたが、小回りが利かなくなりそうなので、
今のうちに法線から計算して回転値を入れる設計にしておく。
全てMovementComponent内で完結させるようにしてみる。
法線と進行方向とUpVectorをCrossProductに掛けてごにょごにょ…
できた!
これでMovementComponentで自由に動かしても、
とりあえずそれっぽく動作するようにはなったかな?
短いけど今日はここまで。
次はキャラ同士がぶつかりそうになったら自動で避ける仕組みを入れてみようかな。
接地判定を実装してみた
前回までのあらすじ
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++での設計の話になると、貼り付ける画像がないな)
今日はここまで。
次は向き補正を実装してみようかな(短くなるかも?)
AnimationBlueprintでEnumを使って遷移させてみた
前回までのあらすじ
LineTraceを実装してみた - yamonメモ.cpp
ActorComponentを作成し、
まずはLineTraceを管理するコンポーネントを追加してみた。
そろそろアクターをCharactorMovementComponentじゃなく、
独自のMovementComponentで動かしたいと思うようになった。
というわけで、今日はキャラのAnimationBlueprintを作成し、
新しく作成したMovementComponentで動作するようにしてみる。
CharactorAnimInstanceの作成
まずはアニメーションさせて少しはそれっぽく見せるために
AnimationInstanceを追加して、AnimationBlueprintを設計してみる。
以前拝見させていただいた講演会で紹介されていた、
Enumでのアニメーション制御をやってみようと思う。
参考にさせていただいたスライド:
UE4を用いたTPS制作事例 EDF:IR アニメーション作成事例紹介
とりあえず、列挙型の追加。
// MyAnimInstance.h UENUM(BlueprintType) enum class ECharactorAnimationIndex : uint8 { Idle, Walk, Run, }; UCLASS() class MY_API UMyCharactorAnimInstance : public UMyAnimInstance { GENERATED_BODY() public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly) EMyCharactorAnimationIndex AnimIndex = EMyCharactorAnimationIndex::Idle; UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "歩き速度") float WalkSpeed = 10.0f; UPROPERTY(EditAnywhere, BlueprintReadOnly, DisplayName = "走り速度") float RunSpeed = 50.0f; };
次にAnimationBlueprint側の設定
アクター側の遷移処理(移動部分のみ)
const auto Vel = GetCurrentVelocity(); const auto Speed = Vel.Size2D(); if (Speed < AnimInstance->WalkSpeed) { AnimInstance->AnimIndex = EKJCharactorAnimationIndex::Idle; } else if(Speed < AnimInstance->RunSpeed) { AnimInstance->AnimIndex = EKJCharactorAnimationIndex::Walk; } else { AnimInstance->AnimIndex = EKJCharactorAnimationIndex::Run; }
アクター側から、遷移させたいタイミングでAnimIndexを更新しておくと、
勝手に遷移してくれるようにするのが目標です。
Stateを細かく分割して、複雑な制御を用いて遷移させるのは、
トランジションの繋ぎ忘れや、AI側と状態のズレが生じやすくなるのに対して、
列挙型での遷移はわかりやすいだけでなく、
共通化もしやすくAI側とは別軸で考えることができるので、
特に大規模開発では良い効果があるように感じた。
まだまだ機能拡張は必要だけど、
とりあえずは速度を入れるだけで、
その状態にあったモーションを再生してくれるようになった。
次の課題
今回やっていて不便だったのは、
速度を与えて移動しているので、カプセルコリジョンのキャラクターが移動しようとすると、
その方向に倒れてしまうので、いったんの対処として「回転をロック」したのだが、
もう少し柔軟な挙動を取らせたいので、ロックせずに、
その時に接地している地形にあった回転値を入れるようにしようかと思う。
どうするのが負荷的にも優しいのかな?
CharactorMovementComponentを少し見てみようかな。
次は接地判定をどうやってとるか?を調べてみたいと思います。
接地判定を実装してみた - yamonメモ.cpp
LineTraceを実装してみた
前回までのあらすじ
デバッグ描画用のアクターと、それを管理するためのGameFrameworkを作ってみた。
これからどんなものを作っていこうかな。
わくわくしている私です。
まずは機能追加してみて感覚を掴もうと、
LineTrace専用のコンポーネントを作成してみることにした。
目標
RayCastしたいアクターはこのコンポーネントをアタッチしておくだけで、
好きな方向へレイを飛ばして判定に使用することができる。
最初だしこんな感じで良いかな…?
UActorComponentを継承した親クラスを作成し、
Ownerの取得方法の整備やデバッグなどのメンバを追加しておく。
初期設計はどうしていくのがセオリーなのだろうか…。
やりたいことが見えてきたら都度拡張していくことにする。
↑で作成したクラスを継承したLineTraceComponentを作成。
アクター側の整備は何もできていないので、
とりあえずグレイマンにそのまま追加してみた。
どういうことがしたい等の要件も特に無いため、
パラメータをひたすら並べただけになってしまった。
(必要分だけ追加できるようにTArrayで作ったほうが良かったかな…)
LineTraceSingleByObjectTypeだけ対応してみました。
非常に簡素な作りだが、とりあえずは実装できた。
LineTraceを1つのコンポーネント内で完結させたい初期実装テスト pic.twitter.com/HsVRRJlyQ9
— yamon (@kyamyamj) October 30, 2019
デバッグ描画も追加してみた。
先日追加したデバッグ描画関連も問題なさそうで良かった!
課題
・コリジョンプリセットも設定していかないと…
・TickGroupはどこにしておくのが良い?
・有効でないレイチェックへの無駄なオーバーヘッドを無くしたい
次は、アニメーション関連少しいじってみようかな。
4.23始めました
UE4勉強しています。
まだまだ分からないことだらけですが、
色々試行錯誤しながら「何か」を作ってみたいと思ってます。
せっかくなので、ブログに書いて残してみようかなと開設してみました。
バージョンは4.23
まずは、単純なキャラクターAIを作ってみようかと思い、
基本アクターの設計から考え始める。
コンポーネントはどうしよう、コリジョン設定はどうすれば良いかな…
AnimationBlueprintは保守しやすい設計にしたいな..等
この考えている時間が好きで胸が躍る。
まずは基礎実装でチュートリアルとして、
デバッグ表示用のアクターとGameFrameworkなるものを作ってみようと決めた。
GameModeBaseを継承したクラスを作成してプロジェクトに設定。
StartPlayでGameFrameworkとデバッグ表示用アクターをスポーンさせてみる。
なんとなくアウトライナ上には表示しないでおくことにしたので、
bHideFromSceneOutlinerを無効化して生成しておいた。
適当に配置したアクターから、GameFrameworkにアクセスし、
取得した文字列をデバッグ用のアクターに繋いで画面に表示してみる。
Playすると表示される文字列
「!!!!」
何故か個人的にテストで良く使う文字列を使用してみた。ちゃんと出ている…
UE4での開発が始まったような気がして嬉しい。
次はLineTrace専用のコンポーネントを作成してみようかな。