【UE4】RoboRecallを参考にNavLinkProxyを使いやすくする

#UE4.25以降では不要なテクニックになりました
UE4.25から、「コンポーネントの始点/終点位置とLinkRelativeStart/Endを同期する機能」が追加されたようです。よってこの記事で紹介するテクニックは不要となりました。

https://twitter.com/Dv7Pavilion/status/1390504945976102912

それでも何らかの理由でこの記事で紹介するテクニックを利用したい場合は、コメントの方にUE4.26.2で動作させるために必要な修正を書いていますので、そちらを参照してください。


#はじめに
[以前書いたNav Link Proxyの記事](http://qiita.com/Dv7Pavilion/items/0250474f93934caf1815)ではNav Link Proxyが持つ**Smart Link**を設定しAIがそこに到達することでイベントを発生させるという手順を踏んでジャンプをさせました。

NavLinkProxyが持つ厄介な点に**SmartLinkはSimpleLinkと同期して位置が設定されない**、というものがあります。
![1.png](https://qiita-image-store.s3.amazonaws.com/0/104467/c1e603e6-c02d-4992-0e5a-557bcd67c8e8.png)
NavLinkProxyの到達イベントを使わないのであれば不要な設定なのですが、使う場合はレベル上に配置する作業が以下のようになります。

NavLinkを配置 -> Simple Linkを調整 -> SimpleLinkのLeft, Rightの座標をコピー -> SmartLinkのStart, Endに貼り付け

かなり面倒です。5~10個程なら難なく配置出来るかもしれませんが、これが倍以上増え、更には仕様変更により再調整しなければならない。という事が起きたときにはちょっとした喧嘩が起きるレベルだと思います。

今回はこういった面倒くさい作業を軽減出来るようにNavLinkProxyをカスタムしようと思います。

**※ここで紹介していくテクニックはRoboRecallを参考にしたものです**


#NavLinkProxyを継承する
SmartLinkはブループリント上ではアクセス出来ないためC++を使います。
「新規追加 -> 新規C++クラス」
ダイアログが出たら
「全てのクラスの表示 -> **NavLink**で検索 -> NavLinkProxyをクリック -> 次へをクリック -> 名前を決めてクラスを作成」
![2.png](https://qiita-image-store.s3.amazonaws.com/0/104467/ba7353cf-4085-2c62-c287-2bd7f181ed79.png)
![3.png](https://qiita-image-store.s3.amazonaws.com/0/104467/77ff1a08-2477-b966-e26d-8987cd5d5e34.png)
![4.png](https://qiita-image-store.s3.amazonaws.com/0/104467/6b39a25e-5763-174d-638b-b9d2d9a37c9d.png)
※名前を決める所でエラーが出ていますが自分は既に同名のクラスを作成しているためです。初めて追加される方はエラーは出ずクラスを作成ボタンも押せます。

##NavJumpLinkProxy.h 

```cpp
NavJumpLinkProxy.h 
-------------------------------------
#pragma once

#include "AI/Navigation/NavLinkProxy.h"
#include "NavJumpLinkProxy.generated.h"

/**
 *
 */
UCLASS()
class MYAITESTPROJECT_API ANavJumpLinkProxy : public ANavLinkProxy
{
	GENERATED_BODY()

public:
	ANavJumpLinkProxy(const FObjectInitializer& ObjectInitializer);

#if WITH_EDITOR
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
	virtual void PostEditMove(bool bFinished) override;
	virtual void PostRegisterAllComponents() override;

	virtual void SyncLinkDataToComponents();
	virtual void OnChildTransformUpdated(USceneComponent* UpdatedComponent, EUpdateTransformFlags UpdateTransformFlags, ETeleportType TeleportType);
	bool bRegisteredCallbacks;
#endif


protected:

#if WITH_EDITORONLY_DATA
	// SmartLinkのStart位置を分かりやすくするためのボックス
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
		UBoxComponent* StartEditorComp;
	// こちらはEnd位置をわかりやすくするためのボックス
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
		UBoxComponent* EndEditorComp;
#endif

};

```

**override**が付いている関数はUE4エディタでの動作を定義します。
overrideが付いていない関数はこのクラス、またはクラスを継承した別のクラスで定義する独自の処理です。

##NavJumpLinkProxy.cpp

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

#include "MyAITestProject.h"
#include "NavJumpLinkProxy.h"

#include "AI/Navigation/NavLinkCustomComponent.h" // <- Add


ANavJumpLinkProxy::ANavJumpLinkProxy(const FObjectInitializer& ObjectInitializer)
	:Super(ObjectInitializer)
{
	// Simple Linkの配列を空にする
	PointLinks.Empty();

	// Smart Linkを有効にする
	bSmartLinkIsRelevant = true;

	// 以下の処理はレベル編集中にわかりやすくするための処理で
	// 実際のAIに対する振る舞いとは関係はない
#if WITH_EDITORONLY_DATA
	StartEditorComp = CreateDefaultSubobject<UBoxComponent>(TEXT("StartBox0"));
	StartEditorComp->InitBoxExtent(FVector(20.f, 20.f, 10.f));
	StartEditorComp->SetupAttachment(RootComponent);
	StartEditorComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	StartEditorComp->bGenerateOverlapEvents = false;

	EndEditorComp = CreateDefaultSubobject<UBoxComponent>(TEXT("EndBox0"));
	EndEditorComp->InitBoxExtent(FVector(20.f, 20.f, 10.f));
	EndEditorComp->SetupAttachment(RootComponent);
	EndEditorComp->RelativeLocation = FVector(0, 0, 500.f);
	EndEditorComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	EndEditorComp->bGenerateOverlapEvents = false;
#endif
}
```
コンストラクタではアクタの初期設定やコンポーネントの生成と設定を行っています。
SimpleLinkはもはや必要ないので配列を空にします。
bSmartLinkIsRelevantにTRUEを設定して、SmartLinkを有効にします。
UBoxComponentは位置をわかりやすくするための目印ですので、コリジョンに関する設定はOFFにします。

```cpp
/* 
*	エディタ上でプロパティを変更した際に呼ばれるイベント
*/
void ANavJumpLinkProxy::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
#if WITH_EDITORONLY_DATA
	// Update the editor boxes. This assumes we aren't changing the location of the start/end components in this edit action.
	UNavLinkCustomComponent* const LinkComp = GetSmartLinkComp();
	if (LinkComp)
	{
		StartEditorComp->SetWorldLocation(LinkComp->GetStartPoint());
		EndEditorComp->SetWorldLocation(LinkComp->GetEndPoint());
	}
#endif // WITH_EDITORONLY_DATA
	UE_LOG(LogTemp, Log, TEXT("Call PostEditChangeProperty"));
	SyncLinkDataToComponents();
	Super::PostEditChangeProperty(PropertyChangedEvent);
}


/*
*	移動、回転、拡縮したりレベル上に配置した時等に毎回呼ばれる
*/
void ANavJumpLinkProxy::PostEditMove(bool bFinished)
{
	UE_LOG(LogTemp, Log, TEXT("Call PostEditMove"));
	SyncLinkDataToComponents();
	Super::PostEditMove(bFinished);
}


/*
*	このアクタの全てのコンポーネントが登録された後に呼ばれるイベント
*/
void ANavJumpLinkProxy::PostRegisterAllComponents()
{
	Super::PostRegisterAllComponents();

#if WITH_EDITORONLY_DATA
	if (!bRegisteredCallbacks && !IsTemplate())
	{
		// 子コンポーネントのトランスフォームを更新
		bRegisteredCallbacks = true;
		StartEditorComp->TransformUpdated.AddUObject(this, &ANavJumpLinkProxy::OnChildTransformUpdated);
		EndEditorComp->TransformUpdated.AddUObject(this, &ANavJumpLinkProxy::OnChildTransformUpdated);
	}
#endif // WITH_EDITORONLY_DATA
}

/*
*	２つのボックスコンポーネントとSmart LinkのStart, Endのトランスフォームを同期する
*/
void ANavJumpLinkProxy::SyncLinkDataToComponents()
{
#if WITH_EDITORONLY_DATA
	// StartEditorComp、EndEditorCompの相対位置を取得
	FVector const NewLinkRelativeStart = GetActorTransform().InverseTransformPosition(StartEditorComp->GetComponentLocation());
	FVector const NewLinkRelativeEnd = GetActorTransform().InverseTransformPosition(EndEditorComp->GetComponentLocation());

	FVector LeftPt, RightPt;
	ENavLinkDirection::Type Dir;
	// Smart LinkのStart位置、End位置、向きを取得
	GetSmartLinkComp()->GetLinkData(LeftPt, RightPt, Dir);
	// Smart LinkのStart、Endの位置と向きを設定
	GetSmartLinkComp()->SetLinkData(NewLinkRelativeStart, NewLinkRelativeEnd, Dir);
#endif // WITH_EDITORONLY_DATA
}

void ANavJumpLinkProxy::OnChildTransformUpdated(USceneComponent* UpdatedComponent, EUpdateTransformFlags UpdateTransformFlags, ETeleportType TeleportType)
{
	SyncLinkDataToComponents();
}
```
PostEditChangeProperty関数はコメントにも書かれていますがプロパティを変更した際に呼ばれるイベントです。
アクタをクリックした時に詳細パネルにアクタのプロパティが表示されます。
![5.png](https://qiita-image-store.s3.amazonaws.com/0/104467/9c0d3992-7c00-9078-1e8b-d3992744032b.png)
移動、回転、拡縮を直接打ち込む。チェックボックスにチェックを入れたりした際に呼ばれます。

PostEditMove関数はアクタをクリックした時にマニピュレータが表示されますが、これを用いてアクタを移動、回転、拡縮した際に呼ばれるイベントです。
※赤、青、緑の矢印です
![6.png](https://qiita-image-store.s3.amazonaws.com/0/104467/b270620c-605b-4aaf-5442-009234ecd7db.png)

詳細パネルを弄った時に呼ばれるのは**PostEditChangeProperty**
マニピュレータを弄ったときは**PostEditMove**が呼ばれると覚えればOKでしょう。

**SyncLinkDataToComponents関数**はStartEditorCompとEndEditorCompの相対位置を取得し、SmartLinkのStartとEndに設定しています。
SyncLinkDataToComponetsをプロパティを弄った時やマニピュレータを弄った時に毎回呼び出す事で常にBoxComponentとSmartLinkが同期しています。


#動作確認をしてみる
プログラムが書けたら**コンパイル**を忘れずに実行します。
エラーが出ず無事にコンパイル出来たら、このクラスを基にブループリントを作成します。
![7.png](https://qiita-image-store.s3.amazonaws.com/0/104467/5c2e1c1e-ee81-85b6-9971-4f6eb00e6d26.png)
名前は**「BP_NavJumpLink」**とします。
イベントグラフに以下の処理を追加します。
![8.png](https://qiita-image-store.s3.amazonaws.com/0/104467/93d5a01e-f88d-dc2e-c21f-02341f1d6e7a.png)
※C_Jumperは[こちら](http://qiita.com/Dv7Pavilion/items/0250474f93934caf1815)で作成しています。
後はBP_NavJumpLinkをコンパイルして、レベル上に配置します。

StartEditorCompとEndEditorCompをそれぞれ移動させると、SmartLinkのLink Relative StartとLink Relative Endの位置が同じになっているはずです。
![9.png](https://qiita-image-store.s3.amazonaws.com/0/104467/cea69e35-3886-1621-a937-952522b67987.png)

#おわりに
C++でNavLinkProxyを継承し、SmartLinkを自動的に設定出来るようにしました。
これによりNavLinkProxyの配置作業が結構楽になったかと思います。
SmartLinkに対してブループリントでアクセス出来るならC++を書かずとも良いのですが、現状はこういった方法を使うしかないのかなと思います。

以上です。お疲れ様でした。