LoginSignup
11
1

More than 5 years have passed since last update.

【UE4】AIを作るのに必要不可欠なEQSのテストをC++で作成できるようになろう!

Posted at

最初に

今回記事にするために使用したUE4のバージョンは4.19です。

ちなみにこの記事はアドカレ用として用意したものですが盛大に遅刻しました。ごめんなさい。

予め用意するもの

  • 任意のEQSを実行する状態になっているBT
  • BTを実行するControllerを持つCharacterのBP

予め注意しておきたいこと

  • VSのバージョンを使用しているUE4のバージョンに合わせる(これを怠るとホットリロードも使えなくなりますしビルドも通らなくなります)。

EQSの準備

まずは以下のようなEQSを作成してBTもしくはControllerからRun EQSを行う状態にしておきましょう。

image.png

今回はGenerator(画像であれば"ActorsOfClass")に追加するテストをC++で自作する方法を解説したものとなります。

既存にあるテストの中には例えばAI Perceptionのような視界判定等が含まれていません。既存プロジェクトにおいてAI改良のためカスタムテストを追加したい場合にこの記事が少しでも役立てばと思います。

第一段階

「EnvQueryTest」を継承するC++クラスを作成

コンテンツブラウザのAdd New又は右クリックから以下の画像のように新規C++クラスの作成を選択します。
image.png

全クラスを表示させてからEnvQueryと検索をかけ、「EnvQueryTest」を選択し次へ進みます。
image.png

テストクラス名は「EnvQueryTest_○○」としてください。○○部分がテスト名としてEQSで追加する際に表示されることとなります。任意のパスを指定してCreate Classをクリックしましょう。
image.png

作成したC++クラスの準備を行う

作成したクラスはEnvQueryTestを継承しているはずです。ここで、最低限必要なoverrideするメソッドが三つ存在します。

#pragma once

#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryTest.h"
#include "EnvQueryTest_Sight.generated.h"

UCLASS()
class NPC_CREATE_API UEnvQueryTest_Sight : public UEnvQueryTest
{
    GENERATED_UCLASS_BODY()

    virtual void RunTest(FEnvQueryInstance& QueryInstance) const override;

    virtual FText GetDescriptionTitle() const override;
    virtual FText GetDescriptionDetails() const override;
};

それぞれとりあえずoverrideしてメソッドをcppファイル内に追加しておいてください。

// Fill out your copyright notice in the Description page of Project Settings.

#include "EnvQueryTest_Sight.h"

UEnvQueryTest_Sight::UEnvQueryTest_Sight(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}

void UEnvQueryTest_Sight::RunTest(FEnvQueryInstance & QueryInstance) const
{
}

FText UEnvQueryTest_Sight::GetDescriptionTitle() const
{
    return FText();
}

FText UEnvQueryTest_Sight::GetDescriptionDetails() const
{
    return FText();
}

ここまで書いたらそれぞれ何を行っているのかを確認していきましょう。

第二段階

それぞれのメソッドの役割

RunTest

このメソッドがテストの本体となり、この中でスコアリング処理を行います。

引数としてFObjectInitializer型のインスタンスを受け取っていますが、この中にはEQSにおける様々な情報が格納されています。このインスタンスから任意の情報をGetメソッドで値を得ることで、スコアリング処理に使用が可能となります。

また、スコアを計算した後は、EQSでスコアリング対象アイテムに大してSetScoreでスコア付けを行う必要があります。

(コードは後述)

GetDescriptionTitle

このメソッドから返す値がテスト名を表示する箇所に使用されます。Distanceのテストで言うなら「Distance: to Querier」と表示されている部分が該当します。

image.png

テスト自体の名前そのまま(今回であればそのまま「Sight」)表示したい場合は、親で定義されているGetDescriptionTitle()をreturnすればOKです。

FText UEnvQueryTest_Sight::GetDescriptionTitle() const
{
    return Super::GetDescriptionTitle();
}

少し凝ったやり方だと、テストに関わる二つのインスタンス名を表示させるようにも可能です。こうしておくと既存のテストのようにパッと見てどれが対象になっているかわかりやすくなるため、複雑なテストを作成するのであればカスタマイズすることをお勧めします。今回、Sightテストにおいては下記のようにカスタマイズを行いました。

image.png

FText UEnvQueryTest_Sight::GetDescriptionTitle() const
{
    return FText::FromString(FString::Printf(TEXT("%s: From %s"),
        *Super::GetDescriptionTitle().ToString(), 
        *UEnvQueryTypes::DescribeContext(SightFrom).ToString()));
}

GetDescriptionDetails

このメソッドから返す値がテストの説明文を表示する箇所に使用されます。Distanceのテストで言うなら「between 0.0 and....」と表示されている部分が該当します。

image.png

EnvQueryTest_DistanceクラスなどはDescribeFloatTestParams()をreturnとしてパラメータ群の説明を表示しています。

FText UEnvQueryTest_Sight::GetDescriptionDetails() const
{
    return DescribeFloatTestParams();
}

コンストラクタで必要な初期化

RunTestの具体的な実装を行う前にコンストラクタで最低限必要な初期化群を確認しておきましょう。

UEnvQueryTest_Sight::UEnvQueryTest_Sight(const FObjectInitializer& ObjectInitializer) :Super(ObjectInitializer)
{
    Cost = EEnvTestCost::Low;
    ValidItemType = UEnvQueryItemType_ActorBase::StaticClass();

    // 必要であればContextの初期化も
    SightFrom = UEnvQueryContext_Querier::StaticClass();
}

Cost

テスト自体の負荷の高さを設定します。

type 説明 設定されているテスト
Low データの読み込みや演算処理を行う(アクター間距離等)。デフォルトではこれ。 Distance, Dot, Gameplay, Random...
Medium 複数のソースやデータにまたいで処理を行う。 Project
High 経路探索や視認処理などの負荷が高い演算処理を行う。 Pathfinding, Overlap, Trace...

このCostで設定されたtypeに基づいてテストの処理順番が決定されます。EQS側で設定しているテストの中で最もコストの低いテストを実行してテスト対象となっているアイテム量を減らしてから、順々にコストの高いテストを実行して最終結果を出力します。

ValidItemType

テスト対象となるアイテムのタイプを設定します。

Distanceテストなら"UEnvQueryItemType_VectorBase"、Randomテストであればアイテムタイプは特に指定がないため全てのタイプが有効になるように"UEnvQueryItemType"を設定しています。

第三段階

RunTestの中身を考えてみよう

とりあえず質素に書くと以下のようなコードが基本となります。

void UEnvQueryTest_Sight::RunTest(FEnvQueryInstance & QueryInstance) const
{
    // Ownerは存在するか
    UObject* QueryOwner = QueryInstance.Owner.Get();
    if (QueryOwner == nullptr)
    {
        return;
    }


    // Editor側で設定したMAX値の取得
    FloatValueMin.BindData(QueryOwner, QueryInstance.QueryID);
    float MinThresholdValue = FloatValueMin.GetValue();

    // Editor側で設定したMIN値の取得
    FloatValueMax.BindData(QueryOwner, QueryInstance.QueryID);
    float MaxThresholdValue = FloatValueMax.GetValue();

    float score = 0.0f;
    // テスト対象のアイテムをここでスコアリングする
    for (FEnvQueryInstance::ItemIterator It(this, QueryInstance); It; ++It)
    {
        // scoreを確定する処理をここに…

        // フィルタリングかスコア設定だけか又はその両方か、
        // 上限値、下限値だけを見るか又はその範囲内か
        // 上限値と下限値を引数として渡す
        It.SetScore(TestPurpose, FilterType, score, MinThresholdValue, MaxThresholdValue);
    }
}

ここに必要であればコンテキスト一覧の取得コードも追加することができます。

    TArray<AActor*> ContextActors;
    if (!QueryInstance.PrepareContext(SightFrom, ContextActors))
    {
        return;
    }

全体としてのコードは以下の通りとなります。

// Fill out your copyright notice in the Description page of Project Settings.

#include "EnvQueryTest_Sight.h"
#include "EnvironmentQuery/Items/EnvQueryItemType_ActorBase.h"
#include "EnvironmentQuery/Contexts/EnvQueryContext_Querier.h"

UEnvQueryTest_Sight::UEnvQueryTest_Sight(const FObjectInitializer& ObjectInitializer) :Super(ObjectInitializer)
{
    Cost = EEnvTestCost::Low;
    ValidItemType = UEnvQueryItemType_ActorBase::StaticClass();
    SightFrom = UEnvQueryContext_Querier::StaticClass();
}

void UEnvQueryTest_Sight::RunTest(FEnvQueryInstance & QueryInstance) const
{
    UObject* QueryOwner = QueryInstance.Owner.Get();
    if (QueryOwner == nullptr)
    {
        return;
    }

    FloatValueMin.BindData(QueryOwner, QueryInstance.QueryID);
    float MinThresholdValue = FloatValueMin.GetValue();

    FloatValueMax.BindData(QueryOwner, QueryInstance.QueryID);
    float MaxThresholdValue = FloatValueMax.GetValue();

    TArray<AActor*> ContextActors;
    if (!QueryInstance.PrepareContext(SightFrom, ContextActors))
    {
        return;
    }

    float score = 0.0f;
    for (FEnvQueryInstance::ItemIterator It(this, QueryInstance); It; ++It)
    {
        // scoreを確定する処理をここに…

        It.SetScore(TestPurpose, FilterType, score, MinThresholdValue, MaxThresholdValue);
    }
}

FText UEnvQueryTest_Sight::GetDescriptionTitle() const
{
    return FText::FromString(FString::Printf(TEXT("%s: From %s"),
        *Super::GetDescriptionTitle().ToString(), 
        *UEnvQueryTypes::DescribeContext(SightFrom).ToString()));
}

FText UEnvQueryTest_Sight::GetDescriptionDetails() const
{
    return DescribeFloatTestParams();
}

Editorで確認してみる

ここまで記述してビルドをかけると、Generator上で右クリックしてテストを追加する際に自身が作成したテスト名が表示されていると思われます。実際に追加して挙動を確認してみてください。

image.png

最後に

以上となります。わかりづらい、もしくは追記が必要な個所などありましたらTwitter(@4_mio_11)又はコメントにてご連絡いただければ助かります。この記事自体への編集リクエストでも構いません(その場合は名前をこの記事の最後に追記させていただきます)。

EQSのテスト作成記事に関しては情報が少ないため少しでも誰かの役に立てたら幸いです。自分自身も調べていて勉強になることが多かったです。

参考

11
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
1