PavilionDV7の雑多なやつ

Qiitaから移行しました。UE4に関する記事から興味のあることまで色々書きます。

【UE4】経路探索時に通過するエッジの中点を取得する

はじめに

UE4での経路探索はRecast & Detourライブラリに実装されているString Pullと呼ばれる経路整形処理によって小回りの効いた経路を出力します。ほとんどの状況ではString Pull後の経路で問題ありませんが、時には道の中心を歩いたり、ゆとりを持って角を曲がってほしいこともあります。そんな問題を解決する手助けになるかもしれない経路探索時に通過するエッジの中点を取得する方法を紹介します。

注意

紹介する方法はBlueprintでは実現不可能だと思われるのでC++が必須です。

Navigation Systemモジュールの機能を利用するので参考にされる方はプロジェクト名.Build.csPublicDependencyModuleNamesNavigationSystemを追加してください。

     PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "NavigationSystem" });

FNavMeshPathへのアクセス

経路探索時に通過するエッジの情報はFNavMeshPathが保持しています。

FNavMeshPathは通過するエッジ情報以外にも経路探索時に必要な設定や処理&データ(String Pullの有効・無効。String Pullの実行。経路の取得。etc...)を持っており、経路探索時においては非常に重要なポジションにいます。

String Pullについては 超シンプルナビメッシュを作る - PavilionDV7の雑多なやつ を参照

肝心なFNavMeshPathへのアクセスですが次のような手順になります。

     UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
        
        const ANavigationData* NavData = NavSys->GetNavDataForProps(FNavAgentProperties::DefaultProperties, StartActor->GetActorLocation());
        if(NavData)
        {
            FPathFindingQuery Query(this, *NavData, StartActor->GetActorLocation(), GoalActor->GetActorLocation(), UNavigationQueryFilter::GetQueryFilter(*NavData, this, UseFilterClass)/*, MakeShareable(Path)*/);


            FNavPathSharedPtr PathPtr = NavData->CreatePathInstance<FNavMeshPath>(Query);
            FNavMeshPath* NavMeshPath = PathPtr ? PathPtr->CastPath<FNavMeshPath>() : nullptr;
        }

通過するエッジの中点を取得する

FNavMeshPathへのポインタが無事に取得出来たらいよいよエッジの中点を取得することが出来ます。

次に載せるコードはエッジの中点の取得以外にもString Pullの有効・無効の設定方法も載せてあります。

     UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
        
        const ANavigationData* NavData = NavSys->GetNavDataForProps(FNavAgentProperties::DefaultProperties, StartActor->GetActorLocation());
        if(NavData)
        {
            FPathFindingQuery Query(this, *NavData, StartActor->GetActorLocation(), GoalActor->GetActorLocation(), UNavigationQueryFilter::GetQueryFilter(*NavData, this, UseFilterClass)/*, MakeShareable(Path)*/);


            FNavPathSharedPtr PathPtr = NavData->CreatePathInstance<FNavMeshPath>(Query);
            FNavMeshPath* NavMeshPath = PathPtr ? PathPtr->CastPath<FNavMeshPath>() : nullptr;

            // 丁寧なアクセス方法
            //const ARecastNavMesh* RecastNavMesh = (const ARecastNavMesh*)NavData;
            //FNavPathSharedPtr PathSharedPtr = RecastNavMesh->CreatePathInstance<FNavMeshPath>(Query);
            //FNavigationPath* NavPath = PathSharedPtr.Get();
            //FNavMeshPath* NavMeshPath = NavPath ? NavPath->CastPath<FNavMeshPath>() : nullptr;

            // String Pullの有効・無効設定
            //if (NavMeshPath)
            //{
            // NavMeshPath->SetWantsStringPulling(false);
            //}

            // FPathFindingQueryからもString Pullの有効・無効を設定出来る
            //Query.NavDataFlags = 1;

            // FPathFindingQueryに使用するパスクラスを設定する
            Query.SetPathInstanceToUpdate(PathPtr);

            FPathFindingResult Result = NavSys->FindPathSync(Query, EPathFindingMode::Regular);

            if (Result.Result == ENavigationQueryResult::Success)
            {
                FVector Start, End;
                // 通常の経路取得方法
                TArray<FNavPathPoint> ResultPathPoints = Result.Path->GetPathPoints();
                // この方法でも取得できる
                //ResultPathPoints = NavMeshPath->GetPathPoints();
                for (int i = 0; i < ResultPathPoints.Num() - 1; ++i)
                {
                    Start = ResultPathPoints[i];
                    End = ResultPathPoints[i + 1];
                    DrawDebugSphere(GetWorld(), Start, 25.f, 8, FColor::Red, true);
                    DrawDebugSphere(GetWorld(), End, 25.f, 8, FColor::Red, true);
                    DrawDebugDirectionalArrow(GetWorld(), ResultPathPoints[i], ResultPathPoints[i + 1], 50.f, FColor::Red, true, -1.f, 0, 4.f);
                }

                // エッジの中点を取得するには「NavMeshPath->GetPathCorridorEdges()」を呼ぶ
                // ただしスタート位置とゴール位置が含まれないので手動で含めてあげる必要がある
                TArray<FVector> EdgeMiddlePoints;
                // スタート位置を格納
                EdgeMiddlePoints.Add(StartActor->GetActorLocation());
                // エッジの中点を格納
                for (const FNavigationPortalEdge& PortalEdge : NavMeshPath->GetPathCorridorEdges())
                {
                    EdgeMiddlePoints.Add(PortalEdge.GetMiddlePoint());
                }
                // ゴール位置を格納
                EdgeMiddlePoints.Add(GoalActor->GetActorLocation());

                for (int i = 0; i < EdgeMiddlePoints.Num() - 1; ++i)
                {
                    Start = EdgeMiddlePoints[i];
                    End = EdgeMiddlePoints[i + 1];
                    DrawDebugSphere(GetWorld(), Start, 25.f, 8, FColor::Blue, true);
                    DrawDebugSphere(GetWorld(), End, 25.f, 8, FColor::Blue, true);
                    DrawDebugDirectionalArrow(GetWorld(), Start, End, 50.f, FColor::Blue, true, -1.f, 0, 4.f);
                }

            }
        }

上記のコードを実行すると次のような結果が表示されます。

赤線と赤球は通常の経路探索で得られる経路です。青線と青球はエッジの中点を通る経路です。黒線はナビメッシュを構成するポリゴンを表しています(緑線の位置にも黒線があります)

f:id:PavilionDV7:20200924203421p:plain

終わりに

今回紹介したのはあくまでも位置を取得するに留まっているものです。得られた経路を移動する機能とは連動していないので、こちらも手動で実装する必要があります。

いくつか自分の思いつく範囲で実装のヒントを紹介します。

  1. 得られた経路のポイントを1つずつ取り出してMoveToをする
  2. Splineを利用する(得られた経路をスプラインポイントとしてSplineに追加しキャラクターをSplineに追従させる)

2番については「[UE4] スプライン上を動く足場の作り方|株式会社ヒストリア」や「[UE4] スプライン移動するキャラクターを作る|株式会社ヒストリア」が参考になります。