執筆バージョン: Unreal Engine 5.6.1
|
こんにちは。エンタープライズエンジニアの方井です。
本日はUE版のEpicDeveloperAssistantを試してみようと思います。
「EpicDeveloperAssistantってなによ。」そんな方もいるのではないでしょうか?
本国のUnrealFestの基調講演を見た方は、UEFN版で知っているかと思います。
もし知らず気になる方は、上に埋め込まれているEGJ公式のツイートのURLへアクセスしてみてください!
公式が提供する、UEに特化したチャット形式のアシスタントが現れると思います!
本当はUE5.7に組み込まれたWidgetとしての動作を見るべきなのですが、EDA側がUE5.6までしか対応していないようで…
本日はUE5.6.1で進めていきます。
本日作成するのは、特殊なReplicationGraphです。
前回、日本語でのReplicationiGraphでの解説が少ないと思い、このような記事を作成しました。
[UE5] ReplicationGraphを組んで、同期内容を絞る
しかしながら、今となってはEDAに訊き放題!これを利用して良い感じにGraphを組み立てることができるのか、検証していきましょう。
1. EDAに訊く
相手が特定のActorComponentを持つ場合は自身が操作するActorから5m以上離れるとReplicationされなくなり、持たない場合はReplicationするようなReplicationGraphおよびReplicationGraphNodeを、UE5.6に適合する形で書いて
EDAへブラウザからアクセスし、上記の命令を投稿しました。
すると、以下の文章とともに、GraphとNodeについて.hと.cppのソースコードが計4つ出力されました。
以下はUnreal Engine 5.6対応で、「特定のActorComponentを持つActorは操作Actorから5m以上離れたらReplicationしない」「持たない場合は常にReplicationする」カスタムReplicationGraphNodeとReplicationGraphの実装例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
#pragma once #include "ReplicationGraph.h" #include "CustomReplicationGraphNode.generated.h" UCLASS() class UCustomReplicationGraphNode : public UReplicationGraphNode { GENERATED_BODY() public: // 追加するActorリスト TArray<AActor*> ActorList; // 追加用関数 void AddActor(AActor* Actor); // Replication時に呼ばれる関数でActorリストを絞り込む virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) override; // フィルタリング対象のコンポーネントクラス UPROPERTY(EditDefaultsOnly) TSubclassOf<UActorComponent> FilterComponentClass; // 距離閾値(cm: 500cm=5m) UPROPERTY(EditDefaultsOnly) float CullDistance = 500.0f; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
#include "CustomReplicationGraphNode.h" #include "GameFramework/Actor.h" #include "Engine/NetConnection.h" void UCustomReplicationGraphNode::AddActor(AActor* Actor) { if (Actor) { ActorList.Add(Actor); } } void UCustomReplicationGraphNode::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) { if (ActorList.Num() == 0) { return; } UNetConnection* NetConnection = Params.ConnectionManager->GetNetConnection(); if (!NetConnection || !NetConnection->PlayerController) { return; } // 操作Actorの位置取得(Possessed Pawn優先) FVector ControllerLocation = FVector::ZeroVector; APawn* Pawn = NetConnection->PlayerController->GetPawn(); if (Pawn) { ControllerLocation = Pawn->GetActorLocation(); } else { ControllerLocation = NetConnection->PlayerController->GetActorLocation(); } for (AActor* Actor : ActorList) { if (!Actor || !Actor->IsValidLowLevel()) { continue; } // 指定コンポーネントを持つか判定 bool bHasComponent = Actor->FindComponentByClass(FilterComponentClass) != nullptr; if (bHasComponent) { // 距離判定:5m以内ならReplicate if (FVector::Dist(Actor->GetActorLocation(), ControllerLocation) <= CullDistance) { Params.OutGatheredReplicationActorLists.AddActor(Actor); } } else { // コンポーネント無しは無条件でReplicate Params.OutGatheredReplicationActorLists.AddActor(Actor); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
#pragma once #include "ReplicationGraph.h" #include "CustomReplicationGraphNode.h" #include "MyReplicationGraph.generated.h" UCLASS() class UMyReplicationGraph : public UReplicationGraph { GENERATED_BODY() public: virtual void InitGlobalGraphNodes() override; UPROPERTY() UCustomReplicationGraphNode* CustomNode; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
#include "MyReplicationGraph.h" #include "CustomReplicationGraphNode.h" #include "MySpecialComponent.h" // フィルタ対象コンポーネントのヘッダー void UMyReplicationGraph::InitGlobalGraphNodes() { Super::InitGlobalGraphNodes(); CustomNode = NewObject<UCustomReplicationGraphNode>(this); check(CustomNode); // フィルタ対象のコンポーネントをセット CustomNode->FilterComponentClass = UMySpecialComponent::StaticClass(); CustomNode->CullDistance = 500.0f; // 5m // グラフに登録 AddGlobalGraphNode(CustomNode); } |
前回と比べると、明らかに足りていない…!!とはいえ、キモとなる実装は手に入りました。前回同様、UBasicReplicationGraphに組み入れる形で進めましょう。
2. 新規プロジェクト作成

リスポーン地点のような安心感。いや、なんだこの見た目は?!UE5.6からTemplateが一新されたんでした。実験の分かりやすさを優先してThirdPersonTemplateを選びます。また、Replicationの処理を記述するため、C++で作成します。
こちらのプロジェクトに、EDAから得たソースコードを加えます。
3. ビルドが通るよう人の手を加える
先ほども説明した通りこのソースコードは足りていないものも多く、ビルドが通りません。特定のActorComponentに対する部分が架空のファイル名/クラス名になっている他、そもそもの変数や関数の使い方が誤っているところもあります。
前回を参考に各部を修正していきましょう。(完璧を求めてEDAに問い続けても良いですが、、、自分で書いた方が今は早いですね)
まず、CustomReplicationGraphNode.hです。謎のAddActor関数を取りやめ、RreplicationされるActorが追加されたときに呼ぶと規定されているNotifyAddNetworkActor関数を追加します。また、NotifyRemoveNetworkActor関数も同様に追加します。
結果、このような形になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
UCLASS() class UCustomReplicationGraphNode : public UReplicationGraphNode { GENERATED_BODY() public: // 追加するActorリスト TArray<AActor*> ActorList; // Replication時に呼ばれる関数でActorリストを絞り込む virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) override; virtual void NotifyAddNetworkActor(const FNewReplicatedActorInfo& ActorInfo) override; virtual bool NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound = true) override; // フィルタリング対象のコンポーネントクラス UPROPERTY(EditDefaultsOnly) TSubclassOf<UActorComponent> FilterComponentClass; // 距離閾値(cm: 500cm=5m) UPROPERTY(EditDefaultsOnly) float CullDistance = 500.0f; }; |
続いて、CustomReplicationGraphNode.cppです。先ほど追加したNotifyAddNetworkActor関数等の定義を追加します。これらは単純にManagedActors配列の操作を行います。
GatherActorListsForConnection関数ではまず、GetNetConnection関数が見つからないとエラーになっています。単純にNetConnectionの変数を取得します。
また、Pawnが取得できない場合はPlayerControllerの位置を取得しようとしますが、AController側の処置で位置を取得できないようになっているため、elseブロック自体を削除します。
最後に、OutGatheredReplicationActorLists変数はOutGatheredReplicationListsの誤りなので、そこも修正します。
結果、このような形になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
void UCustomReplicationGraphNode::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) { if (ActorList.Num() == 0) { return; } UNetConnection* NetConnection = Params.ConnectionManager.NetConnection; if (!NetConnection || !NetConnection->PlayerController) { return; } // Replicationリスト FActorRepListRefView ReplicationActorList; // 操作Actorの位置取得(Possessed Pawn優先) FVector ControllerLocation = FVector::ZeroVector; APawn* Pawn = NetConnection->PlayerController->GetPawn(); if (Pawn) { ControllerLocation = Pawn->GetActorLocation(); } for (AActor* Actor : ActorList) { if (!Actor || !Actor->IsValidLowLevel()) { continue; } // 指定コンポーネントを持つか判定 bool bHasComponent = Actor->FindComponentByClass(FilterComponentClass) != nullptr; if (bHasComponent) { // 距離判定:5m以内ならReplicate if (FVector::Dist(Actor->GetActorLocation(), ControllerLocation) <= CullDistance) { ReplicationActorList.Add(Actor); } } else { // コンポーネント無しは無条件でReplicate ReplicationActorList.Add(Actor); } } Params.OutGatheredReplicationLists.AddReplicationActorList(ReplicationActorList); } void UCustomReplicationGraphNode::NotifyAddNetworkActor(const FNewReplicatedActorInfo& ActorInfo) { if (AActor* Actor = ActorInfo.Actor) { ActorList.AddUnique(Actor); } } bool UCustomReplicationGraphNode::NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound) { if (AActor* Actor = ActorInfo.Actor) { ActorList.Remove(Actor); return true; } return false; } |
続いて、MyReplicationGraphですが、ちょっと足りなさすぎますね。前回のものを持ってきて、独自ノードの部分だけを入れ替えましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
UCLASS(transient, config=Engine) class EDA_TEST_API UMyReplicationGraph : public UReplicationGraph { GENERATED_BODY() public: // Actorをクラス単位でReplication設定していく virtual void InitGlobalActorClassSettings() override; // グラフのノードを作成して繋げる virtual void InitGlobalGraphNodes() override; // 接続(プレイヤー的な意味合い)ごとに考えたいノードを作成して繋げる virtual void InitConnectionGraphNodes(UNetReplicationGraphConnection* RepGraphConnection) override; // Actorインスタンスが追加された時に呼ばれ、ノードに振り分ける virtual void RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo) override; // Actorインスタンスが破棄された時に呼ばれ、ノードに振り分けたものを取り除く virtual void RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo) override; // ServerがActorsをReplicateする関数本体 virtual int32 ServerReplicateActors(float DeltaSeconds) override; // XY平面にGrid分けして、そのGridに振り分ける公式提供ノード UPROPERTY() TObjectPtr<UReplicationGraphNode_GridSpatialization2D> GridNode; // 常にReplicateして欲しいものを入れる公式提供ノード UPROPERTY() TObjectPtr<UReplicationGraphNode_ActorList> AlwaysRelevantNode; // 接続(プレイヤー的な意味合い)ごとに考えた上で、常にReplicateして欲しいものを入れる公式提供ノードと接続の組み合わせ UPROPERTY() TMap<UNetConnection*, UReplicationGraphNode_AlwaysRelevant_ForConnection*> AlwaysRelevantForConnectionList; /** Actors that are only supposed to replicate to their owning connection, but that did not have a connection on spawn */ // 接続(プレイヤー的な意味合い)ごとに考えてReplicateしたいけど、生成時には接続がまだ無かったActor群(つまりbOnlyRelevantToOwner) UPROPERTY() TArray<TObjectPtr<AActor>> ActorsWithoutNetConnection; // AlwaysRelevantForConnectionListから接続を引数に探す関数 UReplicationGraphNode_AlwaysRelevant_ForConnection* GetAlwaysRelevantNodeForConnection(UNetConnection* Connection); // ★独自のReplicationをするための独自ノード UPROPERTY() TObjectPtr<UCustomReplicationGraphNode> myReplicationNode; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
void UMyReplicationGraph::InitGlobalActorClassSettings() { // BaseReplicationGraphからInitGlobalActorClassSettings関数全体を移す } void UMyReplicationGraph::InitGlobalGraphNodes() { // BaseReplicationGraphからInitGlobalGraphNodes関数全体を移す // ★ここから独自、末尾に追記する // 独自ノードを作成する myReplicationNode = CreateNewNode<UCustomReplicationGraphNode>(); // グラフのRootにぶら下げる AddGlobalGraphNode(myReplicationNode); } void UMyReplicationGraph::InitConnectionGraphNodes(UNetReplicationGraphConnection* RepGraphConnection) { // BaseReplicationGraphからInitConnectionGraphNodes関数全体を移す } void UMyReplicationGraph::RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo) { // ★ACharacterを継承したものは全て独自ノード管理とする if (ActorInfo.Class->IsChildOf(ACharacter::StaticClass())) { myReplicationNode->NotifyAddNetworkActor(ActorInfo); } else { // BaseReplicationGraphからRouteAddNetworkActorToNodes関数のensureMsgfマクロ以降をelseブロック内に移す } } void UMyReplicationGraph::RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo) { // ★ACharacterを継承したものは全て独自ノード管理としたので、削除するときはそこから取り除く if (ActorInfo.Class->IsChildOf(ACharacter::StaticClass())) { myReplicationNode->NotifyRemoveNetworkActor(ActorInfo); } else { // BaseReplicationGraphからRouteRemoveNetworkActorToNodes関数のif (ActorInfo.Actor->bAlwaysRelevant)以降をelseブロック内に移す } } int32 UMyReplicationGraph::ServerReplicateActors(float DeltaSeconds) { // BaseReplicationGraphからServerReplicateActors関数全体を移す } UReplicationGraphNode_AlwaysRelevant_ForConnection* UMyReplicationGraph::GetAlwaysRelevantNodeForConnection(UNetConnection* Connection) { // BaseReplicationGraphのGetAlwaysRelevantNodeForConnection関数を基に、型を変えた関数に書き換える UReplicationGraphNode_AlwaysRelevant_ForConnection* Node = nullptr; if (Connection) { if (auto v = AlwaysRelevantForConnectionList.FindRef(Connection)) { if (v) { Node = v; } else { UE_LOG(LogNet, Warning, TEXT("AlwaysRelevantNode for connection %s is null."), *GetNameSafe(Connection)); } } else { UE_LOG(LogNet, Warning, TEXT("Could not find AlwaysRelevantNode for connection %s. This should have been created in UBasicReplicationGraph::InitConnectionGraphNodes."), *GetNameSafe(Connection)); } } else { // Basic implementation requires owner is set on spawn that never changes. A more robust graph would have methods or ways of listening for owner to change UE_LOG(LogNet, Warning, TEXT("Actor: bOnlyRelevantToOwner is set but does not have an owning Netconnection. It will not be replicated")); } return Node; } |
4. 特定のActorComponentを作成する
今回指示したReplicationGraphは、言わば特定ActorComponentを持って離れると検知されないというものになります。
これはステルス機能があるということですね!ですので、NinjaComponentという名前で作成します。
C++で作成します。特に変更は無いためソースコードをここでは出しません
続いて、UMyReplicationGraphのInitGlobalGraphNodes関数でUMyReplicationGraph::InitGlobalGraphNodesのインスタンスを作成した際、FilterComponentClass変数にNinjaComponentを指定してください。
|
// ★ここから独自 // 独自ノードを作成する myReplicationNode = CreateNewNode<UCustomReplicationGraphNode>(); myReplicationNode->FilterComponentClass = UNinjaComponent::StaticClass(); // グラフのRootにぶら下げる AddGlobalGraphNode(myReplicationNode); |
5. Ninjaアビリティを作成する
「特定のActorComponentを持った場合」というグラフを組んだ通り、Ninjaアビリティ発動でNinjaComponentを付与しようと思います。また、このアビリティは時限式で解除されるようにします。
はじめに、GameplayAbilityを親クラスにBlueprintアセットを作成します。そして、ActivateAbilityイベントで呼ばれる処理を記述します。Authorityがあれば先ほど作成したNinjaComponentを付与し、一定時間後(NinjaTime)にアビリティが終了されるようにします。

アビリティの設定を行います。まず、TriggersにあるAbilityTriggersはEDA_Test.Action.Ninjaを追加して、このTagがActivateされたときにアビリティが発動するようにします。
次に、TagsにあるAssetTagsは同様にEDA_Test.Action.Ninjaを追加します。

6. Test用Characterを作成する
「特定のActorComponentを持った場合」というグラフを組んだ通り、アビリティを発動してActorComponentを付与されるようなPlayerCharacterを作成します。
BP_ThirdPersonCharacterを継承したCharacterクラスを作成します。そして、アビリティを扱えるようComponentsにAbilitySystemComponentを追加します。

次に、作成したNinjaアビリティをAbilitySystemに与えるため、BeginPlayからGiveAbilityノードを呼びます。

続いて、0キーを押すことでアビリティを発動するようにします。サーバー側で発動してほしいため、Remoteだった場合はRunOnServerイベントへ飛ぶようにし、TryActivateAbilitiesByTag関数で発動トリガーであるEDA_Test.Action.Ninjaを呼びます。

最後に、追加されたNinjaComponentを削除するための関数を作成します。ActorComponentは所有者しか消すことができないため、付与される側に処理を載せます。

先ほど作成したAbilityアセットを再度開いてアビリティ終了時に呼ばれるイベントに、作成したComponent削除関数を呼び出す処理を追加します。

7. Test用GameModeを作成する
独自のCharacterを作成したため、GameModeにPlayerCharacterの設定を行います。
プロジェクト作成時に同時に作成されたGameModeを継承したGameModeアセットを作成し、DefaultPawnClassを先ほど作成したCharacterアセットに変更します。
そして、テストするレベルのWorldSettingsのGameModeOverrideに設定して、このGameModeが使用されるようにします。

8. 試す
DedicatedServer設定でClientを2つ用意し、実際に動かしてみましょう。
0キーを押してNinja状態になり、ほかのプレイヤーから離れると見えなくなります。Ninja状態が解除されると再び同期され、正しく表示されます。遠くで時限式のステルススキルを発動して近付き、相手が気付いた時には既に格闘戦の距離、みたいな感じでしょうか。
ビルドを通して動かすための手直しがほどほどに必要でしたが、それでも仕様を伝えるだけで望むReplicationGraphをEpicDeveloperAssistantが書いてくれました!
少し難易度が高い、いちいちつらつら書いていられない複雑なものも一旦ひな形をEDAに書いてもらうことで、それをキチンとしたものに直すだけの簡単な作業になりましたね。
もちろん仕様をちゃんと定義し、EDAの出力をレビューし、キチンと直す部分は理解している人間が必要です。EDAに遅れを取らないよう、こちらもしっかり知識を最新に追いつけていきたいものです。
以上で、EpicDeveloperAssistantの説明は終了です。
それではアシスタントともに、良きUE生活を!!