UE5 GameplayTargetingSystem についてのメモ

# 概要
UnrealEngine5.2 から追加された GameplayTargetingSystem についてのメモ。



## 更新履歴
| 日付 | 内容 |
|:-----|:----|
|2023/09/30| 初版 |
|2023/11/21| C++リクエストコードのハンドル解放処理の修正   |


## 参考
以下の記事を参考にいたしました、ありがとうございます。
[UE公式：ゲームプレイ ターゲット システム](https://docs.unrealengine.com/5.2/ja/gameplay-targeting-system-in-unreal-engine/)
[UE公式：Gameplay Targeting System のデバッグ](https://docs.unrealengine.com/5.2/ja/gameplay-targeting-system-debugging-in-unreal-engine/)
[ゲームプレイ ターゲット システムのリファレンス](https://docs.unrealengine.com/5.2/ja/gameplay-targeting-system-reference-in-unreal-engine/)
[Gameplay Targeting System Plugin について](https://massa8080.hatenablog.com/entry/2023/05/14/154824)


## 環境
Windows10
Visual Studio 2022
UnrealEngine 5.2.1

## 関連ソース
"Engine\Plugins\Experimental\GameplayTargetingSystem\Source\GameplayTargetingSystem\TargetingSystem\TargetingSubsystem.h"
"Engine\Plugins\Experimental\GameplayTargetingSystem\Source\GameplayTargetingSystem\Types\TargetingSystemTypes.h"


# TargetingSystem とは
任意の位置からターゲットを探す仕組みを提供するプラグインです。データドリブンなシステムで、同期型と非同期型の両方が用意されているようです。


# 導入準備
[プラグイン]から`Targeting System`を有効にすることで利用可能になります（UEエディタ再起動が入ります）。
また、C++上で扱うには `プロジェクト名.Build.cs` にモジュール名 `TargetingSystem` を追加する必要があります。

```cs:Build.cs
using UnrealBuildTool;

public class MyProject : ModuleRules
{
	public MyProject(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] {
			"Core",
			"CoreUObject",
			"Engine",
			"InputCore"
		});

		PrivateDependencyModuleNames.AddRange(new string[] {
			"EnhancedInput",
			"GameplayAbilities",
			"GameplayTags",
			"GameplayTasks",
			"TargetingSystem"     // ←追加
		});

　　// ..省略..
```


# ターゲットリクエスト
ターゲットリクエストを行うためには入力データとして `TargetingPreset`と`TargetingSorceContext` が必要です。
また出力結果を受け取るには `FTargetingRequestHandle` を使って `TargetingSubsystem`経由で受け取ります。

### TargetingPreset
ターゲッティングシステムが行うタスクの定義の設定です。タスクは複数設定可能で設定した順序に実行されます。
`Targeting Preset`を継承したデータアセットです。以下3タスクを設定したターゲットプリセットアセット例。

![Preset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/641b59d7-89a9-bbae-bd9b-80068a31d09f.png)



### TargetingSourceContext
ターゲッティングシステムの処理でのソース情報（開始位置など）を定義します。
前述の`Targeting Preset`で設定したタスクがこの情報を使用します。
`Source Location` がターゲッティング開始位置となりますが、`Source Actor`を設定している場合はそちらの情報を優先して使うようです。（各Taskの作りによる？）

![Context.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/3392ba19-06a1-4e78-c8aa-db43e18e99ea.png)



## BPでの即時リクエスト
`ExecuteTargetingRequest`ノードに対し、`TargetingPreset`と`TargetingSourceContext` を入れて受け取り用のイベントを設定します。
ターゲット処理が完了したらイベントが実行されるので、`TargetingRequestHandle` を引数に `TargetingSubsystem`から結果を受け取ります（`GetTargetingResult` もしくは `GetTargetingResultActors`）。
終わったら、ハンドルを `RemoveAsyncTargetingRequestWithHandle` で始末します。このハンドルを始末する処理を行わないとターゲットリクエストの結果はいつまでも残り続けるようです。

![ExecuteTargetingRequest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/0e3e5995-a15d-b33b-8844-070bcc788ebb.png)

:::note alert
`RemoveAsyncTargetingRequestWithHandle` は入力にハンドルを指定できないため正しく動作していません（UE5.3でも同様の模様）。ターゲッティングリクエストハンドルを削除するには現状C++コードを使うしかないようです。
:::


## BPでの非同期リクエスト
`Start Async Targeting Request` を使用するとできるようです。
ただ `TargetingSelectionTask_AOE`などの一部タスクは非同期での処理はされないようです。

![ASync.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/3b50a8b5-77fc-0ae2-af3f-59893b81096f.png)



## C++での即時リクエスト
C++で行う場合は以下のように。
`UTargetingPreset`は継承したBPに持っておくようになっていますが、 `ConstructorHelpers`を使って取得するようにすることも可能だと思います。


```cpp:.h
#include "CoreMinimal.h"
#include "GameFramework/Character.h"

class UTargetingPreset;

UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()

// ..省略..

public:
	// ターゲッティングリクエスト
	UFUNCTION(Category = "Targeting", BlueprintCallable)
	void RequestTargeting();
	// ターゲットティング結果受け取り
	UFUNCTION(Category = "Targeting", BlueprintCallable)
	void ResultTargeting(FTargetingRequestHandle TargetingHandle);
	// ターゲッティングプリセット
	UPROPERTY(EditAnywhere, Category="Targeting")
	TObjectPtr<UTargetingPreset>	TargetingPreset;

};
```


```cpp:.cpp
#include "GameplayTargetingSystem/TargetingSystem/TargetingSubsystem.h"
#include "GameplayTargetingSystem/Types/TargetingSystemTypes.h"

// リクエスト
void AMyCharacter::RequestTargeting()
{
	auto _TargetingSubsystem = UTargetingSubsystem::GetTargetingSubsystem(this->GetWorld());
	
	
	FTargetingSourceContext _Context;
	_Context.SourceActor		= this;
	_Context.InstigatorActor	= this;
	_Context.SourceLocation		= this->GetActorLocation();
	_Context.SourceSocketName	= NAME_None;
	_Context.SourceObject		= this;
	
	UTargetingPreset* _Preset = TargetingPreset;
	FTargetingRequestDelegate _CompletionDelegate;
	_CompletionDelegate.BindUObject(this, &ThisClass::ResultTargeting);
	
	// ターゲッティングハンドル作成
	auto _TargetingHandle = UTargetingSubsystem::MakeTargetRequestHandle(_Preset, _Context);
	
	// 即時ターゲッティングリクエスト実行
	_TargetingSubsystem->ExecuteTargetingRequestWithHandle(_TargetingHandle, _CompletionDelegate);
	
}

// 結果受け取り
void AMyCharacter::ResultTargeting(FTargetingRequestHandle _TargetingHandle)
{
	auto _TargetingSubsystem = UTargetingSubsystem::GetTargetingSubsystem(this->GetWorld());
	TArray<FHitResult>	_OutTargets;

	// サブシステムから結果受け取り
	_TargetingSubsystem->GetTargetingResults(_TargetingHandle, _OutTargets);

	// アクター名を表示
	for(const auto& _It : _OutTargets){		
		UE_LOG(LogTemp, Log, TEXT("%s"), *_It.GetActor()->GetFName().ToString());
	}
	
#if 0
	// ターゲットハンドル解放（このタイミングではスレッド競合する可能性があるため1フレーム遅らせて実行するべき）
	//UTargetingSubsystem::ReleaseTargetRequestHandle(_TargetingHandle);
#else
 	// ターゲットハンドル解放
 	_TargetingSubsystem->RemoveAsyncTargetingRequestWithHandle(_TargetingHandle); 
#endif 
}
```

:::note alert
上記コードでターゲットハンドル解放に `ReleaseTargetRequestHandle` を使っていますが、このタイミングでの解放はスレッド競合を起こす可能性があるため、1フレーム遅らせて実行するか、非同期解放の `RemoveAsyncTargetingRequestWithHandle` を使うべきです。
:::


## C++での非同期リクエスト

`ExecuteTargetingRequestWithHandle` を `StartAsyncTargetingRequestWithHandle` に変えることで非同期リクエストになります。結果受け取りは即時リクエストと同じです。

```cpp:.cpp
#include "GameplayTargetingSystem/TargetingSystem/TargetingSubsystem.h"
#include "GameplayTargetingSystem/Types/TargetingSystemTypes.h"

// リクエスト
void AMyCharacter::RequestTargeting()
{
	auto _TargetingSubsystem = UTargetingSubsystem::GetTargetingSubsystem(this->GetWorld());
	
	
	FTargetingSourceContext _Context;
	_Context.SourceActor		= this;
	_Context.InstigatorActor	= this;
	_Context.SourceLocation		= this->GetActorLocation();
	_Context.SourceSocketName	= NAME_None;
	_Context.SourceObject		= this;
	
	UTargetingPreset* _Preset = TargetingPreset;
	FTargetingRequestDelegate _CompletionDelegate;
	_CompletionDelegate.BindUObject(this, &ThisClass::ResultTargeting);
	
	// ターゲッティングハンドル作成
	auto _TargetingHandle = UTargetingSubsystem::MakeTargetRequestHandle(_Preset, _Context);
	
	// 非同期ターゲッティングリクエスト実行
	_TargetingSubsystem->StartAsyncTargetingRequestWithHandle(_TargetingHandle, _CompletionDelegate);
}
```

## ソート結果の受け取り
ソートタスクを使った場合、その結果の受け取りは`FTargetingTaskSet`から取得します。以下サンプルコード。

```cpp:.cpp
	// リクエストハンドルからソート結果を取得
	auto _TDResultsSet = FTargetingDefaultResultsSet::Find(_TargetingHandle);
	for(const auto& _It : _TDResultsSet->TargetResults){
		// ソートに使ったスコアを取得
		float _Score = _It.Score;
		
		UE_LOG(LogTemp, Log, TEXT("%s [Score = %.2f]"), *_It.HitResult.GetActor()->GetFName().ToString(), _Score);
	}

```

:::note alert
`Score`はソート用に計算された値です。UE5.2では`TargetingFilterTask_SortByDistance` のソート処理にバグがあり正しく動作しません。UE5.3では修正されています。
:::


# タスクの作成
選択、フィルタ、ソートの各タスクはそれぞれ
`TargetingTask`、`TargetingFilterTask_BasicFilterTemplate`、`TargetingSortTask_Base` を継承して自作タスクを作成できます。


## フィルタタスク
フィルタタスクの作成例です。ソースアクター前方ベクトルとの内積をフィルタ条件にしています。

```cpp:TargetingFilerTask_Test.h
#pragma once

#include "GameplayTargetingSystem/Tasks/TargetingFilterTask_BasicFilterTemplate.h"
#include "GameplayTargetingSystem/Types/TargetingSystemTypes.h"

#include "TargetingFilterTask_Test.generated.h"


UCLASS(Blueprintable)
class UTargetingFilterTask_Test : public UTargetingFilterTask_BasicFilterTemplate
{
	GENERATED_BODY()

public:
	UTargetingFilterTask_Test(const FObjectInitializer& ObjectInitializer);

	// ターゲティングリクエストを処理するために派生クラスによって呼び出される評価関数
	virtual bool ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const override;

protected:
	// 有効な内積（ValidDotProduct ～ 1,0f）
	UPROPERTY(EditAnywhere, Category = "Targeting Filter | Data", Meta = (AllowAbstract=true))
	float	ValidDotProduct = 0.8f;

};
```

```cpp:TargetingFilerTask_Test.cpp
#include "TargetingFilterTask_Test.h"
#include "GameFramework/Actor.h"

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

// フィルター評価関数
bool UTargetingFilterTask_Test::ShouldFilterTarget(const FTargetingRequestHandle& TargetingHandle, const FTargetingDefaultResultData& TargetData) const
{
	AActor* _SourceActor = nullptr;
	if (FTargetingSourceContext* _SourceContext = FTargetingSourceContext::Find(TargetingHandle)){
		_SourceActor = _SourceContext->SourceActor;
	}
	
	AActor* _TargetActor = TargetData.HitResult.GetActor();
	
	
	if(_SourceActor && _TargetActor){
		FVector _SourceLoc = _SourceActor->GetActorLocation();
		FVector _TargetLoc = _TargetActor->GetActorLocation();
		
		FVector _Fwd = _SourceActor->GetActorForwardVector();
		FVector _Tar = (_TargetLoc - _SourceLoc).GetSafeNormal();
		// 内積計算
		float _Dot = FVector::DotProduct(_Fwd, _Tar);
		if( _Dot < ValidDotProduct){
			// 内積が有効値以下
			return(true);
		}else{
			return(false);
		}
	}

	return(true);
}
```


# GameplayAbilityでの使用
ゲームプレイアビリティでターゲッティングを行う `PerformTargetingRequest` と、フィルタリングを行う `PerformFilteringRequest` が用意されています。

## PerformTargetingRequest
`Target Preset`を入力し使用します。`TargetingSourceContext`はタスク内部で作成され、`SourceActor`にアビリティシステムコンポーネントの`AvatarActor`が設定されるようです（他はデフォルト）。

![PerformTargetingRequest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/2fc092fc-9fbf-d59c-3889-f55f00c5b7ae.png)


## PerformFilteringRequest
フィルタリング処理を行います。`InTargets` に予めフィルタリングを行うアクターのリストを入れます。

![PerformFilteringRequest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/736b06d4-b1ff-a015-d5b0-c8c6e0f9047f.png)



# まとめ
データ定義でターゲットを検索できるため用途によってタスクの差し替えができ、柔軟に対応ができそうです。ただ用意されているタスクだけではやや不十分な感じがするので自作タスクを用意する必要が結構ありそうです。