PavilionDV7の雑多なやつ

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

【UE4】Influence Map 再び~プログラム編~

お知らせ

  • 2020-12-21
    • Influence Mapシステムをプラグイン化したプロジェクトを「プロジェクトのダウンロード」に追加

はじめに

Unreal Engine 4 (UE4) Advent Calendar 2020 16日目の記事です。

プロジェクト編」の続きです。

プロジェクトのダウンロード

いつもの通りOneDriveにてプロジェクトを配布しています。

エンジンバージョン:UE4.25.4

https://1drv.ms/u/s!Au-8FqgREBKZjQ-yQTgaqBBB5K5k?e=oxkjek

自分の勉強も兼ねて近い将来はプラグイン化して配布する予定です。その時はTwitterにてお知らせします。

PavilionDV7 (@Dv7Pavilion) | Twitter


プラグイン化したInfluence Mapシステムを含む、サンプルプロジェクトを追加しました。

https://1drv.ms/u/s!Au-8FqgREBKZjRJhN191fYnkoHv9?e=vumuzk

任意のプロジェクトへのインストール方法

  1. 上のURLからIMapPluginプラグインを含むサンプルプロジェクトをダウンロードする
  2. プロジェクトを作成する
  3. .uprojectがあるフォルダ内に「Plugins」フォルダを作成する
  4. 3番で作成したPluginsフォルダ内に「IMapPlugin」フォルダをコピーする
  5. プロジェクトを開き「Edit -> Plugins」を選択
  6. 開いたウィンドウの左の欄にある中から「Project/Other」を選択し、右の欄から「IMapPlugin」を有効化する

プログラムについて

大まかな地図

かなり大雑把ですかInfluence Mapシステムの地図を用意してみました。

f:id:PavilionDV7:20201215213339p:plain


登場する殆どのクラスはインターフェースを備えます。これは本プロジェクトに着手した当時に発表された「ハード参照、ソフト参照」についての話題が盛り上がっていたためです。

「インターフェースを適切に使用すればクラス間の依存関係を解消でき、参照アセットの連鎖を最小限に留めることが出来る」と解釈したので「じゃあ使ったろうやないか!」ということで多様してみた次第です。


マップについて

マップ構造

一般的なInfluence Mapは空間をグリッドで分割し、それぞれのセルに影響値を格納します。しかしグリッドは「高さ」を扱うのが苦手です。

そこでX-Y平面のグリッドからZ軸を加えたボクセルによる実装も考えましたが今回は見送ることとしました。理由は無駄なデータが多く生成されてしまうからです。

3D環境はその空間全てが何かしらのオブジェクトで埋まっているわけではなく、実際のところ殆どは空白が空間を占めています。(赤がオブジェクトで埋まっている空間。青がなにもない空間)

f:id:PavilionDV7:20201215213835p:plain

反復処理の回数は限界まで減らしておきたかったのでボクセルの実装は見送りました。


無駄な空間にデータを生成する必要がなく、しかしレベル全体をうまくカバー出来るような構造はなにかと考えたとき、その解決策がナビメッシュを利用する事でした。

ナビメッシュはレベル全体に等しく存在し、エージェントが歩行可能なサーフェスのみに生成されているため、ボクセルでの課題だった空白の空間に無駄にデータが生成されてしまう問題は一発で解決出来ると考えました。

更にナビメッシュを利用することで、うまいこと経路探索時にもInfluence Mapを活用出来ることにも気づきました。


以上のことからこのInfluence Mapシステムではナビメッシュに準じた空間の分割を行うこととしました。

詳しい手順については後に回して、まずはInfluence Mapシステムで駆使される3つのマップについて解説します。

Propagation Map

Propagation Mapは名前の通り何らかの影響力の伝播を表すマップでInfluence Mapの更新の度に新しく計算されます。

1つのPropagatorに1つのPropagation Map。複数のPropagation Mapが同居することはありません。


Propgation Mapには頻繁に利用される2つのタイプが有ります。1つはエージェントの物理的な位置を表す「近接マップ(Proximity)」。近接マップはエージェントの位置の他にも「短時間に移動できる距離」も表すので、エージェントの位置に最も高い影響値をセットし、距離が離れるほど低くなる傾向があります。

もう1つはエージェントの潜在的な脅威を表す「脅威マップ(Threat)」。潜在的な脅威とはアクションゲームで言えば攻撃範囲、ステルスゲームで言えば視野範囲が該当します。


遠距離攻撃を実行できるエージェントと近距離攻撃のみのエージェントでは影響マップのサイズは大きく異なり、さらに遠距離攻撃をするエージェントでも異なる伝播をするマップを用意する必要があります。

例えば銃を持つエージェントは遠くから自身のつま先までを狙うことが出来るため、エージェントの位置に最も高い影響値をセットし、距離が離れるほど低くなるマップを持ちます。

しかし戦車といった砲撃を行うエージェントは自身の足元に主砲を向けることが出来ないため、エージェントの位置から一定範囲は影響値が最も低くセットされ、離れるにつれて影響値は高まり、更に一定距離離れると影響値が低くなる。いわゆるドーナツ状の伝播を行います。

f:id:PavilionDV7:20201215215052p:plain

Working Map

収集したInfluence Mapを演算して任意のデータを作り出す際の作業場となるマップです。個数の制限はありませんが演算後は即座に破棄されます。Interest Mapと組み合わせて利用され、基本的にはInterest Mapと同サイズか一回り大きいサイズになります。


Working Mapはエージェントを中心に作成されるため真にエージェントが必要とする範囲の情報のみを扱う事ができます。あるエージェントが周囲の状況を知るためにInfluence Mapを作成するとした場合、例えば100km離れた味方の影響を気にする必要があるか?と言われるとそうではありません。あるエージェントが欲しい影響は恐らく自身の目が届く範囲に収まるはずです。Working Mapはそれと同等の働きをします。

Interest Map

Interest(興味)とあるようにInterest Mapは「どの位置にある影響に興味があるか」を定義するマップです。

何らかの脅威から逃げるため安全な場所へと移動したいエージェントを考えてみます。このとき近くにある安全な場所と遠くにある安全な場所はどちらが良いでしょうか? 特に理由がない限り近くにある安全な場所へと移動したいはずです。Interest Mapはこの問題を解決する手助けをします。

ナビメッシュを利用したノードグラフの作成

「マップ構造」の項目でもあったように、このInfluence Mapシステムはナビメッシュを利用して空間分割を行うことにしました。ナビメッシュへのアクセスはC++を使えば簡単です。

過去に投稿した

【UE4】RecastNavMeshを拡張してNavLinkProxyを自動で配置させる.

【UE4】経路探索のコスト計算をカスタマイズする.

でも同じようにナビメッシュを利用した色々なことをしています。興味のある方は是非読んでみてください。


ノードグラフの生成は「AInfluenceNodeGraphクラス」が担当します。

AInfluenceNodeGraphクラスは以下の仕事をします。

  • ノードグラフの生成

  • ノードグラフの保持

  • ノードグラフ情報の提供
  • 与えられた地点から最も近いノードの検索

ノードグラフの生成は2つのステップを踏むことで完了します。1つ目は「ノードの生成」。2つ目は「ノードの島との接続」です。


ノードの生成は「SpawnAndConnectingNodes関数」で実装されています。

この関数の前半はエラーチェックに割かれているため、実際に仕事をしているコードは次の部分となります。

// ~~~ 省略 ~~~

            TArray<FVector> PolyVerts;
            if (!NavMeshCache->GetPolyVerts(Poly.Ref, PolyVerts))
            {
                SkipCount++;
                continue;
            }

            TArray<AInfluenceNode*> SpawnNodes;
            SpawnNodes.Reserve(PolyVerts.Num());

            // ここで求められる頂点は同じポリゴンに属するため近隣ノードとしての登録が可能.
            for (int VertIdx = 0; VertIdx < PolyVerts.Num(); VertIdx++)
            {
                // エッジの中点を求めているのは経路探索時に通過する位置がポリゴンエッジの中点だから.
                // FIntVector型にすることで経路探索時の影響値の検索をしやすくする.
                FVector Middle = (PolyVerts[VertIdx] + PolyVerts[(VertIdx + 1) % PolyVerts.Num()]) * 0.5f;
                FIntVector IntMiddle = FIntVector(Middle);
                // 既に同じキーを持つノードが生成されているかチェック.
                AInfluenceNode* Node = (NodeGraph.Contains(IntMiddle)) ? NodeGraph[IntMiddle] : nullptr;
                if (Node == nullptr)
                {
                    Node = GetWorld()->SpawnActor<AInfluenceNode>(AInfluenceNode::StaticClass());
                    Node->SetID(NewID++);
                    Node->SetRegionTileID(TileSetIdx);
                    Node->SetGraphLocation(IntMiddle);
                    Node->SetSpawnSegment(PolyVerts[VertIdx], PolyVerts[(VertIdx + 1) % PolyVerts.Num()]);
                    Node->SetActorLocation(Middle/* + FVector(0.f, 0.f, 10.f)*/);
                    Node->SetFolderPath("Nodes");

                    NodeGraph.Add(IntMiddle, Node);
                }

                // 既にスポーンされた他のノードと近隣ノードとしてマークし合う.
                SpawnNodes.Add(Node);
                for (AInfluenceNode* Other : SpawnNodes)
                {
                    if (Node != Other)
                    {
                        Node->AddNeighbor(Other);
                        Other->AddNeighbor(Node);
                    }
                }
            }

// ~~~ 省略 ~~~

次のコードに注目します。

FVector Middle = (PolyVerts[VertIdx] + PolyVerts[(VertIdx + 1) % PolyVerts.Num()]) * 0.5f;

取得したナビメッシュポリゴンの頂点座標からエッジベクトルを求めて半分にしています。エッジベクトルを半分にした理由は「経路探索時にInfluence Mapデータを直感的に利用出来るようにするため」です。

次の画像は経路探索のコスト計算時に参照される始点と終点をDrawDebugLineで表示したものです。

f:id:PavilionDV7:20201219170353p:plain

黒線がナビメッシュポリゴンを表しており、水色の薄い線がどれも黒線の中点を通っている様子が見えます。

ノードの座標がエッジの中点であることにより、経路探索時にInfluence Mapを利用する際、冗長な検索処理をせずとも一発で目的のノードを手に入れることが可能になることがわかるかと思います。


中点Middleを求めた後は次のようなFIntVector型に変換します。

FIntVector IntMiddle = FIntVector(Middle);

実のところ深い意味はなく、等価チェックを行うときに何らかの要因によって0.0001の違いでFalseになるのが怖かったからです。ですが、詳細に突き詰めたわけではないので、もしかするとFVector型のままで保存しても良かったかもしれません。


2つ目のステップである「ConnectingNodeIslands関数」。これは「見た目上、同じエッジにあるのに隣接ノードとして接続されていない」問題を解決するためのものです。

ナビメッシュは時として次のようなポリゴンが生成される時があります。

f:id:PavilionDV7:20201219170407p:plain

画像中央のエッジは中途半端な位置で分割されていますが実はこのエッジには3本のエッジが同居しています。

f:id:PavilionDV7:20201219170422p:plain

1番と2番のエッジは同じタイル内に属しているので、それぞれに生成されたノードは隣接ノードとして記録されていますが、3番のエッジは別のタイルに属しているため隣接ノードとして記録されません。よってエージェントが1番から影響を伝播しても3番に伝わらないため明らかにおかしな結果が生じてしまいます。

このようなエッジが生成されてしまったときに、きちんとそれぞれに生成されたノード同士で隣接ノードとして記録するようにするのがConnectingNodeIslands関数の仕事です。


ConnectingNodeIslands関数の仕組みは単純です。

各ノードは生成されたエッジの始点と終点を保持しており、ノードから始点、ノードから終点へと2本のライントレースを実行し「Node」というタグを持つアクターにヒットすれば、そのヒットしたノードと隣接ノードとして記録し合うだけです。

f:id:PavilionDV7:20201219170811p:plain

これによって満遍なく環境をカバーしたノードグラフが得られました。

ノードグラフからInfluence Mapを作成

このInfluence MapシステムでのInfluence Mapを表すデータは次のものです。

TMap<FIntVector, float>

ノードの座標や隣接リストといったデータは全てノードグラフが保持しているため、Influence Mapそのものを表すのには「目的のノードを取り出すためのFIntVector型のキー」と「そのノードの影響値を表すFloat型の値」の2つだけで表現が出来ます。


Influence Mapを作成している場所は「UInfluencePropagatorクラス」にある「CreateNewMap関数」が該当します。CreateNewMap関数はInfluence MapだけではなくPropagation MapやInterest Mapの作成にも汎用的に使えるようにしてみました。

 // InfluenceMapのコピーを取得.
    const auto& InfluenceMap = InfluenceMapCollectionRef->GetNodeGraph()->GetNodeGraphData();
    const int INFLUENCEMAP_SIZE = InfluenceMap.Num();

    // ~~~ 省略 ~~~

    // 影響値を求める必要があるノードだけをピックアップする.
    while (WorkingBuffer.Num() != 0)
    {
        // 伝播対象のノードインデックスを取り出す.
        CurrentNode = WorkingBuffer.Pop(false);

        // 既に影響値を設定済み || フィルタリング関数でFALSE
        if (Visited.Find(CurrentNode.Key) || !ExcludeFunc(Cast<AActor>(InfluenceMap[CurrentNode.Key])))
            continue;

        // 訪問済み.
        Visited.Emplace(CurrentNode.Key, true);

        // ノードの隣接ノードを取得.
        for (const AInfluenceNode* Neighbor : InfluenceMap[CurrentNode.Key]->GetNeighbor())
        {
            // 訪問済みの近隣ノードであればスキップ
            if (Visited.Find(Neighbor->GetGraphLocation()))
                continue;

            // 距離がMaxRange以内
            float CenterNeighborDistSq = FVector::DistSquared(Neighbor->GetActorLocation(), CenterNode->GetActorLocation());
            if (CenterNeighborDistSq <= FMath::Square(MaxRange))
            {
                WorkingBuffer.Emplace(Neighbor->GetGraphLocation(), CurrentNode.Value + 1);
                NeedPropagationCulcuration.Emplace(Neighbor->GetGraphLocation(), CurrentNode.Value + 1);

                MaxStep = FMath::Max(MaxStep, CurrentNode.Value + 1);
            }
        }
    }

    for (const auto& Pair : NeedPropagationCulcuration)
    {
        // ノードの影響値を求める.
        Result.Emplace(Pair.Key, PropagationValueFunc(Pair.Value, MaxStep, InfluenceMap[Pair.Key]->GetActorLocation()));
    }


    return Result;

影響値の伝播

影響値の伝播はCreateNewMap関数のパラメータにある「PropagationValueCalculator」が担当します。これは関数オブジェクトであり、ヘッダファイルにて次のように宣言されています。

using PropagationValueCalculator = TFunctionRef<float(uint32, uint32, const FVector&)>;

つまりPropagationValueCalculatorとは「返り値がFloat型、第一引数がuint32、第二引数もuint32、第三引数がconstのFVectorの参照型である関数オブジェクト」であることがわかる


「CreateNewPropagationMap関数」を見ると、先の関数オブジェクトに該当する関数を宣言している箇所があります。

 // ここで定義した関数オブジェクトは最大でNodeGraphの要素数と同じ回数呼び出されるので高速な関数を渡す.
    auto PropagationCalculator = [&](uint32 StepDistance, uint32 MaxStepDistance, const FVector& NodeLocation)
    {
        // レイキャストしちゃってるけどさ!
        GetWorld()->LineTraceSingleByChannel(TraceResult, 
                                             GetOwner()->GetActorLocation() + OwnerHeadOffset, 
                                             NodeLocation, 
                                             ECollisionChannel::ECC_Visibility, // PawnやCharacterMeshにはヒットしないチャンネルを指定.
                                             FCollisionQueryParams{ "Propagate LOS", false, GetOwner() });

        float Ratio = 0.f;
        // 何も衝突していない(間に障害物がない) または 「Node」タグを持つアクターにヒットした
        if (!TraceResult.bBlockingHit || (TraceResult.GetActor() && TraceResult.GetActor()->ActorHasTag("Node")))
            Ratio = FVector::Dist(CenterNode->GetActorLocation(), NodeLocation) / PropagateRange;
        else
            Ratio = (float)StepDistance / (float)MaxStepDistance;

        if (PropagationCurve)
            return PropagationCurve->GetFloatValue(Ratio);
        else
            return FMath::Max(0.0f, 1.f - 1.f * Ratio);

    };

コメントにも書いてあるとおり、この関数オブジェクトは頻繁(最悪ノードグラフのサイズと同じ回数)に呼び出されるので出来る限り負荷の高い処理は実装しないことをおすすめします。

今回トレース処理を実行しているのは、「プロジェクト編」でも説明した「移動を伴った影響の伝播」を実装するためなので、しょうがないのです。


この関数の引数に渡される「StepDistance」「MaxStepDistance」はトレースが障害物によってブロックされてしまったときに利用されます。

トレースがそのままノードのコリジョンにヒットしたとき、そのノードには「伝播の中心ノードの座標と現在取り出しているノードとの距離 / 伝播距離」の数値を使って影響値を求めます。障害物にブロックされてしまったノードは「迂回した場合の距離」を使って影響値を求める必要があります。ただし経路探索を使ってきちんとした距離を求めることは避けたかったので、「中心ノードからいくつのノードを渡り歩いて到達するか」を表す「歩数」で近似することにしました。

このPropagationValueCalculatorが呼ばれた時点で「最も多くかかった歩数」が記録されているので、あとは「現在のノードまでの歩数 / 最も多くかかった歩数」を求めることで、大体距離での影響値と近しい値を求めることが出来ました。

Influence Mapの演算

Influence Map同士の演算は「UInfluenceWorkingMapOperatorクラス」に実装されています。ここで実装している機能はどれもブループリント上で使うことを想定しているため、UBlueprintFunctionLibraryクラスを継承して扱いやすいようにしてみました。


最も単純なInfluence Mapの演算は次の画像のようになります。

f:id:PavilionDV7:20201219170907p:plain

AddやMult等の演算ノードはWorking Mapに該当する「FMapOperationResult構造体」を必要とします。そのため、使う前には必ず「Initialize Working Mapノード」でFMapOperationResult構造体を任意のInfluence Mapから生成する必要がります。

この手順を必須としたのは演算時に不要にメモリ確保を行わないようにするためです。

AddやMultノードでマップサイズのメモリを確保するようなコードを書いたとき「Map Aのサイズで確保されるのか」「それともMap Bで確保されるのか」「Map AとMap Bの演算後のサイズで確保されるのか」が判断しづらくなると考えました。そこでFMapOperationResult構造体を必須とするインターフェースにすることで、不要な混乱を招かないようにしました。

Influence Mapを利用したナビゲーション

コスト関数をオーバーライドする

経路探索時に呼び出されるコスト関数に手を入れるには「dtQueryFilterクラス」にある「getVirtualCost関数」をオーバーライドすればOK。このプロジェクトでは「FInfluenceRecastQueryFilterクラス」にてgetVirtualCost関数をオーバーライドしています。

Influence Mapを使ったナビゲーションコストの操作は次の箇所で行っています。

 float AdditionalCost = 0.f;
    if (InfluenceMap && InfluenceMap->Num() != 0)
    {
        FIntVector NodeALoc = FIntVector(Recast2UnrealPoint(pa));
        FIntVector NodeBLoc = FIntVector(Recast2UnrealPoint(pb));

        // 影響値は「追加されるコストの重み」として機能する.
        if (InfluenceMap->Contains(NodeALoc)) { AdditionalCost = (*InfluenceMap)[NodeALoc] * CostMultiplier * FIXED_ADDITIONAL_COST; }
        if (InfluenceMap->Contains(NodeBLoc)) { AdditionalCost = (*InfluenceMap)[NodeBLoc] * CostMultiplier * FIXED_ADDITIONAL_COST; }
    }

pa、pbを「Recast2UnrealPoint関数」でRecast & Detourで扱われるベクトル型からUE4で扱われるFVector型に変換します。更にInfluence Mapのキーとして扱うためにFIntVectorへと変換します。後は手に入れたキーで予め保持していたInfluence Mapにアクセスし影響値を取得。残りの計算を終えればpa、pb上にある影響値を考慮したときに追加される新しいコストが手に入る仕組みです。

オーバーライドしたコスト関数を利用する

AInfluenceRecastNavmesh

デフォルトのナビメッシュクラスである「ARecastNavmeshクラス」は「FRecastQueryFilter」からコスト関数を呼び出すため、オーバーライドしたFInfluenceRecastQueryFilterクラスのコスト関数は呼ばれません。よって目的のコスト関数を呼び出すためにARecastNavmeshクラスを継承して新しいRecastNavmeshクラスを作る必要があります。

AInfluenceRecastNavmeshクラスはARecastNavmeshクラスを継承して、RecreateDefaultFilter関数にて目的のコスト関数を呼び出すように変更しています。

重要な箇所は次の部分

 DefaultQueryFilter->SetFilterType<FInfluenceRecastQueryFilter>();
    DefaultQueryFilter->SetMaxSearchNodes(DefaultMaxSearchNodes);

    // 以下のように new(変数名)型名(コンストラクタ引数) という形は「配置new」と呼ばれる.
    // 既に確保されたメモリにオブジェクトを構築することにより、メモリ確保時のコストを少なく出来る.
    DetourFilter = static_cast<FInfluenceRecastQueryFilter*>(DefaultQueryFilter->GetImplementation());
    DetourFilter = new(DetourFilter)FInfluenceRecastQueryFilter(true);
    DetourFilter->SetDataUsedForDebugging(GetWorld(), bDrawDebugFindPath);

このように設定するだけで経路探索時に目的のクラスがオーバーライドしたコスト関数を呼び出させることが出来ます。


コード上の変更は以上で完了しますがもう一つ、今度はNav Mesh Bounds Volumeを配置/更新したときにAInfluenceRecastNavmeshクラスを生成するようにエディタを設定する必要があります。

この手順については「プロジェクト編」の「セットアップの手順」にて紹介されていますのでご参照ください。

Influence Mapデータを渡すまで

経路探索時に利用するInfluence Mapデータを渡すまでには、いくつかのクラスを経由する必要があり、全体を把握するまでに時間を要する可能性があるため、ここで簡単な案内をしておきます。


まずは利用するInfluence Mapデータを受け取る最初の入り口である「UAITask_MoveToInfluenceMapクラス」を見てみます。

これはAIの移動処理でおなじみ「Move to Location or Actorノード」を実装するUAITask_MoveToクラスを継承したクラスで、Influence Mapデータを利用した経路探索をするための唯一のインターフェースである「AIMoveToUseInfluenceMapData関数」を備えます。AIMoveToUseInfluenceMapDataはブループリントで「Move To Location or Actor Use Influence Map Data」という名前で登場しています。

     UAITask_MoveToInfluenceMap* MyTask = Controller ? UAITask::NewAITask<UAITask_MoveToInfluenceMap>(*Controller, EAITaskPriority::High) : nullptr;
        if (MyTask)
        {
            FAIInfluenceMapMoveRequest MoveReq;
            
            // ~~~ 省略 ~~~
            
            MoveReq.SetTargetInfluenceMapData(TargetMapData);   // 追加.
            MoveReq.SetCostMultiplier(CostMultiplier);          // 追加.
            
            // ~~~ 省略 ~~~
    
            MyTask->SetUp(Controller, MoveReq);
            MyTask->SetContinuousGoalTracking(bUseContinuosGoalTracking);
            MyTask->SetInfluenceMapMoveRequest(MoveReq); // 追加.
            if (bLockAILogic)
            {
                MyTask->RequestAILogicLocking();
            }
        }
    
        return MyTask;

AIMoveToUseInfluenceMapData関数の中身は実は移動処理が無く、移動時の振る舞いの設定や移動に必要なデータをセットするだけのものです。新しく用意した「FAIInfluenceMapMoveRequest構造体」にパラメータで渡されたInfluence Mapデータを格納しています。


実際に移動処理が開始されると「PerformMove関数」が呼ばれ、自作のAIControllerにAIMoveToUseInfluenceMapData関数で作ったFAIInfluenceMapMoveRequest構造体を渡します。

重要な箇所は次の部分です。

 // Influence Mapデータを格納したMoveRequestを渡し結果を待つ.
    FNavPathSharedPtr FollowedPath;
    const FPathFollowingRequestResult ResultData = OwnerInfluenceMapAIController->MoveToUseInfluenceMap(InfluenceMapMoveRequest, &FollowedPath);

AInfluenceMapAIControllerクラスではInfluence Mapデータを利用するために2つの関数を用意しています。ただその中の1つのMoveToUseInfluenceMap関数は、UAITask_MoveToInfluenceMap::PerformMove関数から受け取ったデータを別の所にパスしているだけなので(重要な部分だけを抜き出して見た場合)特に解説はしません。


AInfluenceMapAIControllerクラスに用意された最後の1つが「FindPathForInfluenceMapMoveRequest関数」です。ここでは実際にNavmeshクラスにパス検索の命令を出している所になります。

重要な箇所は次の部分です。

     // ~~~ 省略 ~~~
        bool bSuccessGetNavmesh = false;
        
        if (AInfluenceRecastNavmesh* NavMesh = UInfluenceMapFunctionLibrary::GetInfluenceRecastNavMesh(GetWorld()))
        {
            NavMesh->SetInfluenceMapData(MoveRequest);

            // Influence Mapデータを利用したパス検索時には確実にAInfluenceRecastNavmeshが持つQueryFilterを利用させる. 
            Query.NavData = NavMesh;
            Query.QueryFilter = NavMesh->GetDefaultQueryFilter();

            bSuccessGetNavmesh = true;
        }

        // ~~~ 省略 ~~~

現在のWorld情報からレベルにあるAInfluenceRecastNavmeshクラスを取得し、SetInfluenceMapData関数にInfluence Mapデータを渡しています。


少々長かったが、これで晴れてInfluence Mapデータを経路探索時に利用する準備が完了しました。

実際のところInfluence Mapデータを受け取って、NavMesh->SetInfluenceMapData(MoveRequest);を呼び出すまではInfluence Mapデータを次の関数またはクラスへとパスし続けているだけです。

最後にInfluence Mapデータを渡すまでの流れをまとめてみます。

UAITask_MoveToInfluenceMap::AIMoveToUseInfluenceMapData
↓
UAITask_MoveToInfluenceMap::PerformMove
↓
AInfluenceMapAIController::MoveToUseInfluenceMap
↓
AInfluenceMapAIController::FindPathForInfluenceMapMoveRequest
↓
AInfluenceRecastNavmesh::SetInfluenceMapData
↓
FInfluenceRecastQueryFilter::SetAdditionalNavigationData

終わりに

今回はなし!疲れた!

参考資料

The Core Mechanics of Influence Mapping

The Core Mechanics of Influence Mapping - Video

Spatial Knowledge Representation through Modular Scalable Influence Maps

Modular Influence Map System (Imap)

Escaping the Grid: Infinite-Resolution Influence Mapping, Mike Lewis

Modular Tactical Influence Maps, Dave Mark

Paragon Bots: A Bag of Tricks, Mieszko Zieliński