執筆バージョン: Unreal Engine 5.5.4
|
こんにちは。エンタープライズエンジニアの方井です。
本日はReplicationGraphを組んでみようと思います。
ReplicationGraphとはなんでしょうか。公式では以下のページに詳しくまとまっております。
レプリケーション グラフの機能とレプリケーション グラフ ノードの概要。
一番上の概要はかなりよくまとまっているためこれ以上簡単に説明できるかは難しいですが簡単に言うと、
「全部のActorに対して毎回同期するかチェックするのは大変だから、グラフ構造にしてチェックを飛ばすグループを作ったり処理を単純化して負荷を軽減しよう」
という話になります。ここでいうグラフとはBlueprintエディタやMaterialエディタ等の処理のグラフではなく、データ構造的な意味合いのグラフです。この機能を活用することで負荷軽減が行えることはもちろん、Replicationしないことで何かを隠す(非表示にする)ことの代替にもなります(それが必要な場面は分かりませんが…)。
それでは実際にReplicationGraphを組んで、自分だけの同期内容を作成しましょう。
1. 新規プロジェクト作成

リスポーン地点のような安心感。新規プロジェクト作成画面です。実験の分かりやすさを優先してThirdPersonTemplateを選びます。また、Replicationの処理を記述するため、C++で作成します。
2. 独自のReplicationGraphノードを作成する
UReplicationGraphNode_ActorListを継承するクラスをC++で作成します。そして、以下のように編集します。
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
|
#pragma once #include "CoreMinimal.h" #include "ReplicationGraph.h" #include "MyReplicationGraphNode_ActorList.generated.h" UCLASS() class REPLICATIONGRAPHTEST_API UMyReplicationGraphNode_ActorList : public UReplicationGraphNode_ActorList { GENERATED_BODY() public: // 毎周期呼ばれるReplicateするActorを選択する関数 virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& params)override; // Debug表示用 virtual void LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const override; // このノードにActorインスタンスを追加する時に呼ぶ関数 virtual void NotifyAddNetworkActor(const FNewReplicatedActorInfo& ActorInfo) override; // このノードからActorインスタンスを削除する時に呼ぶ関数 virtual bool NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound = true) override; protected: // 今回制御したいActorインスタンスのリスト UPROPERTY() TArray<AActor*>; MyReplicataionActors; // 制御に使用するTag名 static const FName UnReplicateTag; }; |
ソースファイルは以下のような形です。
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
|
#include "MyReplicationGraphNode_ActorList.h" #include "AbilitySystemComponent.h" #include "GameplayTagContainer.h" // Game Ability Systemによって、このTagを付与されていたら他の人に同期しない const FName UMyReplicationGraphNode_ActorList::UnReplicateTag = TEXT("ReplicationGraphTest.State.Invisible"); void UMyReplicationGraphNode_ActorList::GatherActorListsForConnection(const FConnectionGatherActorListParameters& params) { ReplicationActorList.Reset(); for (const auto& target : MyReplicataionActors) { if (const auto abc = target->GetComponentByClass(UAbilitySystemComponent::StaticClass())) { // 検査対象のAbilitySystemComponentを取得する const auto abilitySys = Cast(abc); if (abilitySys->HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag(UnReplicateTag))) { // tagを持っている場合はReplicateしない continue; } } // tagを持っていなければReplicateリストに加える ReplicationActorList.Add(target); } // Replicateリストが空っぽじゃなきゃ、Replicate対象に加える if (!ReplicationActorList.IsEmpty()) { params.OutGatheredReplicationLists.AddReplicationActorList(ReplicationActorList); } } void UMyReplicationGraphNode_ActorList::LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const { // ReplicationGraphにあるDebug表示機能に則る DebugInfo.Log(NodeName); DebugInfo.PushIndent(); { // このノードが監視してるActorインスタンスと、今Replicate対象としたActorインスタンスを出す FActorRepListRefView tmp; for (const auto& i : MyReplicataionActors) { tmp.Add(i); } LogActorRepList(DebugInfo, TEXT("World"), tmp); LogActorRepList(DebugInfo, TEXT("Rep"), ReplicationActorList); } // このノードにぶら下がっているノードも同様にDebug表示する for (UReplicationGraphNode* ChildNode : AllChildNodes) { DebugInfo.PushIndent(); ChildNode->LogNode(DebugInfo, FString::Printf(TEXT("Child: %s"), *ChildNode->GetName())); DebugInfo.PopIndent(); } DebugInfo.PopIndent(); } void UMyReplicationGraphNode_ActorList::NotifyAddNetworkActor(const FNewReplicatedActorInfo& ActorInfo) { // 加える MyReplicataionActors.Add(ActorInfo.Actor); } bool UMyReplicationGraphNode_ActorList::NotifyRemoveNetworkActor(const FNewReplicatedActorInfo& ActorInfo, bool bWarnIfNotFound) { // 削除しつつ、その成否を返す return (MyReplicataionActors.Remove(ActorInfo.Actor) > -1); } |
内容を簡単に言えばこのノードに振り分けられたActorについて、AbilitySystemComponent内に特定のTag(ReplicationGraphTest.State.Invisible)があればほかの人に同期しないというものになります。
3. 独自のReplicationGraphを作成する
続いて、グラフを作成していきます。ここで作業簡略化のためにエンジンに同梱されているUBaseReplicationGraphという標準的な機能が実装され終わっているクラスを継承して、独自部分だけを作成出来たら良かったのですが、そのクラスにはAPIマクロが付与されておらず外部モジュールから利用することはできませんでした。そのため、UBaseReplicationGraphの実装をそのまま頂きつつ、独自の実装も盛り込んだUReplicationGraphを継承するクラスをC++で作成します。そして、以下のように編集します。
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
|
#pragma once #include "CoreMinimal.h" #include "ReplicationGraph.h" #include "MyReplicationGraph.generated.h" class UMyReplicationGraphNode_ActorList; struct FNewReplicatedActorInfo; UCLASS(transient, config=Engine) class REPLICATIONGRAPHTEST_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 GridNode; // 常にReplicateして欲しいものを入れる公式提供ノード UPROPERTY() TObjectPtr AlwaysRelevantNode; // 接続(プレイヤー的な意味合い)ごとに考えた上で、常にReplicateして欲しいものを入れる公式提供ノードと接続の組み合わせ // 宣言とAPIマクロの関係から、BasicReplicationGraphから型を変更 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> ActorsWithoutNetConnection; // AlwaysRelevantForConnectionListから接続を引数に探す関数 UReplicationGraphNode_AlwaysRelevant_ForConnection* GetAlwaysRelevantNodeForConnection(UNetConnection* Connection); // ★独自のReplicationをするための独自ノード UPROPERTY() TObjectPtr myReplicationNode; }; |
ソースファイルは以下のような形です。UBaseReplicationGraphの実装をそのまま書くのは規約に反するため、コメントアウトを参考に以下リンクから導入してください。
UE5.5.3 BasicReplicationGraph.cpp
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 79 80 81 82 83 84 85 86
|
#include "MyReplicationGraph.h" #include "GameFramework/Character.h" #include "UObject/UObjectIterator.h" #include "Engine/ChildConnection.h" #include "MyReplicationGraphNode_ActorList.h" void UMyReplicationGraph::InitGlobalActorClassSettings() { // BaseReplicationGraphからInitGlobalActorClassSettings関数全体を移す } void UMyReplicationGraph::InitGlobalGraphNodes() { // BaseReplicationGraphからInitGlobalGraphNodes関数全体を移す // ★ここから独自、末尾に追記する // 独自ノードを作成する myReplicationNode = CreateNewNode(); // グラフの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; } |
基本的には、ACharacter継承のものだけを独自ノードに入れ、他はBaseReplicationGraphと同様にルールに従って振り分けております。つまり、ACharacter継承かつAbilitySystemで特定のTagを持った場合は、他人にReplicateされることなくなりました。
4. 独自のReplicationGraphを使用する
作成したReplicationGraphを使用するよう、Engineへ伝えましょう。今回は特定のGameModeの際に動くようにしたいと思います。プロジェクト作成時に同時に作成されたGameModeのC++コンストラクタに以下のように追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
#include "ReplicationGraphTestGameMode.h" #include "MyReplicationGraph.h" #include "UObject/ConstructorHelpers.h" AReplicationGraphTestGameMode::AReplicationGraphTestGameMode() { // set default pawn class to our Blueprinted character static ConstructorHelpers::FClassFinder PlayerPawnBPClass(TEXT("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter")); if (PlayerPawnBPClass.Class != NULL) { DefaultPawnClass = PlayerPawnBPClass.Class; } // ReplicationDriverを変更する UReplicationDriver::CreateReplicationDriverDelegate().BindLambda([](UNetDriver* driver, const FURL& url, UWorld* world)->UReplicationDriver* { // 今回作成した独自ReplicationGraphを返す return NewObject(GetTransientPackage()); } ); } |
5. (Optional)アビリティ発動時のエフェクトを作成する
Optional、つまりやらなくても特に問題無い作業です。
「AbilitySystemで特定のTagを持った場合」というグラフを組んだ通り、AbilitySystemを使用します。発動時、何も画面に反映されていないと不安なのでそれが分かるエフェクトActorを作成します。
Actorを作成してNiagaraParticleSystemComponentを追加し、そこにEngine組み込みのアセットであるFountainLightweightを設定します。また、Replicates設定をONにし、同期できるようにします。

6. Invisibleアビリティを作成する
「AbilitySystemで特定のTagを持った場合」というグラフを組んだ通り、Invisibleアビリティ発動でTagを付与しようと思います。また、このアビリティは時限式で解除されるようにします。
はじめに、GameplayAbilityを親クラスにBlueprintアセットを作成します。そして、ActivateAbilityイベントで呼ばれる処理を記述します。Authorityがあれば先ほど作成したエフェクトをSpawnし、一定時間後(InvisibleTime)にアビリティが終了されるようにします。

また、アビリティ終了時に呼ばれるイベントに、先ほどSpawnしたエフェクトを破棄する処理を追加します。

アビリティの設定を行います。まず、TriggersにあるAbilityTriggersはReplicationGraphTest.Action.Invisibleを追加して、このTagがActivateされたときにアビリティが発動するようにします。
次に、TagsにあるAssetTagsは同様にReplicationGraphTest.Action.Invisibleを追加し、ActivationOwnedTagsにReplicationGraphTest.State.Invisibleを追加します。これにより、アビリティ発動中はOwnerに対してReplicationGraphTest.State.Invisible付与されることになります。ReplicationGraphで書いた条件と同じですね!

7. Test用Characterを作成する
「AbilitySystemで特定のTagを持った場合」というグラフを組んだ通り、アビリティを発動してReplication対象から外れるようなPlayerCharacterを作成します。
BP_ThirdPersonCharacterを継承したCharacterクラスを作成します。そして、アビリティを扱えるようComponentsにAbilitySystemComponentを追加します。

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

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

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

9. 試す
DedicatedServer設定でClientを2つ用意し、実際に動かしてみましょう。
通常は位置が正しく同期されていますが、0キーを押してInvisible状態になるとほかのプレイヤーからは見えなくなります。Invisible状態が解除されると再び同期され、正しく表示されます。Invisible状態かどうかはエフェクトが表示されているかどうかでわかりますね。
見事、ReplicationGraphに独自のノードを埋め込み、動的にReplication可否を切り替えて同期内容を変更することができました!!データが同期されていないということはClient側へのパケットにもRAMにも載っていないため、どう頑張っても相手の位置を特定することはできません。不正防止の観点からも使えるテクニックかもしれません。
以上で、デモの説明は終了です。
それでは、良いReplication生活を!!