69.4K Views
December 21, 23
スライド概要
UE5で利用できるIrisおよびネットワークの汎用的なTipsについて紹介しています。
※本ドキュメントは UE5 Deep Dive 2023にて講演した資料を配布用に編集したものです
Unreal Engineを開発・提供しているエピック ゲームズ ジャパンによる公式アカウントです。 勉強会や配信などで行った講演資料を公開しています。 公式サイトはこちら https://www.unrealengine.com/ja/
次世代ネットワーク通信実装Irisについて Senior Software Engineer, Developer Relations Takashi Suzuki 2023 Epic Games Japan
UnrealNetwork UnrealEngineの特徴の一つにマルチプレイの実 装が組み込まれている点が挙げられます そのネットワーク機能の歴史は深く、 始まりは16人マルチプレイに対応した Unreal Tournament まで遡ります
時代は オープンワールド+ 多人数マルチプレイへ 時代の移り変わりに合わせるように UnrealEngineも進化を続け 同じようにネットワーク機能にも様々な改良が施 されてきました
しかしこんな声も 勝ち確だったのに ラグのせいで捲られた!!!! ホストはカクついて不利なので 部屋は立てません!! 8人対戦から100人対戦に変更? XX機能について 今から学習します・・・ メモリ不足で サーバーが落ちるので もう一段階サーバーの スペックを上げて解決します! えっ!?予算がない?
我々開発者はこれらの声に耳を傾け 問題に立ち向かわなければいけません パフォーマンス コスト 開発効率
ネットワークに対する これらの課題への回答
次世代レプリケーションシステム Iris
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
Irisの目標 既存のレプリケーションシステムは非常に長い歴 史があり、追加されてきた様々な最適化の方法 があります。 多くは追加の作業が必要になり、膨大なワールド をネットワークで効率よく処理するためにはメンテ ナンスコストが高くなりすぎています。 Irisはプロジェクトに対するカスタムコードを最小 限にして最大のパフォーマンスを引き出すことを 目標にしています。 効率性 並列実行 互換性
効率性 キャッシュフレンドリー 仮想関数呼び出しの削減 省メモリ リニアデータレイアウトにより、キャッ シュ効率性を高める 従来のAActorの仮想関数呼び出しに よる情報取得を排除し、ゲーム側が明 確なAPIを通じて情報を送信 メモリ使用量はコストに直結する データのコピーやバッファを削減し省 メモリ化 デルタ圧縮や柔軟な量子化により帯 域幅の削減も
並列実行 ゲームとレプリケーションシステ ムを分離 それぞれは特定のAPIを介してのみ 影響しあい、局所性を維持する プッシュモデル マルチスレッディング ゲーム側から変更されたプロパティを 手動でマーク 処理を複数スレッドに分散することで 劇的な最適化効果が得られる予定 送信頻度制御にもなる
互換性 オプトイン ポーリングモデル ReplicationGraph 多くの後方互換性を維持したままIris を開発を進行 プッシュモデルの利用は必須ではあり ません レプリケーショングラフは利用出来ま せんが、同等の機能をより簡潔に実 装できるように
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
Legacy Iris未使用の旧来のNetDriverの動作 新旧の構造比較 Iris Iris有効化時の動作
Iris Legacy OSS OSS Socket Socket Connection Connection NetDriver ReplicationGraph ゲーム NetDriver Iris ゲーム
Iris Legacy ゲーム Actor ゲーム Actor Actor ReplicationState Data IsNetRelevantFor GetNetPriority GetLifetimeReplicatedProps ReplicateSubobjects IsNetRelevantFor GetNetPriority GetLifetimeReplicatedProps ReplicateSubobjects IsNetRelevantFor GetNetPriority GetLifetimeReplicatedProps ReplicateSubobjects Replication Fragments Add/Remove Actor Replicated Actor Create/Destroy Replication Bridge Create/Destroy NetHandle ReplicationProtocol NetDriver::ServerReplicateActors Replication System
Legacy
BuildConsiderList(); //Actor->NetFrequency
for( Connection : Connections )
{
PrioritizeActors( Connection, Actors ); //Actor->IsRelevantFor
//ProcessPrioritizedActors
for( Actor : Actors )
{
Actor->Channel->ReplicateActor()
}
}
for( Connection : Connections )
{
Connection->SendTo();
}
Iris
for( Connection : Connections )
{
UpdateFilterPrePoll(Connection);
//関連性を抽出
}
for( ActorHandle : ActorHandles )
{
PollAndCacheReplicationState(ActorHandle);
//互換性のため
}
for( Connection : Connections )
{
UpdateFilterPostPoll(Connection);
//ステート変更
Prioritize(Connection);
}
for( Connection : Connections )
{
Connection->SendTo()
}
ゲーム ReplicationState Data Add/Remove Actor Replication Fragments Replicated Actor Create/Destroy Replication Bridge Irisの俯瞰 Create/Destroy NetHandle ReplicationProtocol Replication System ステートデータ (プロパティのコピー) システム内部: コネクション毎: ・プロトコル ・ディスクリプタ ・プライオリティ ・フィルタ ・依存関係 ・スケジューリング ・シリアライゼーション
ゲーム ReplicationState Data Add/Remove Actor Replication Fragments Replicated Actor Create/Destroy Replication Bridge ReplicationSystem Create/Destroy NetHandle ReplicationProtocol Replication System ステートデータ (プロパティのコピー) システム内部: コネクション毎: ・プロトコル ・ディスクリプタ ・プライオリティ ・フィルタ ・依存関係 ・スケジューリング ・シリアライゼーション
ReplicationSystem Irisのコア ネットワークドライバーの初期化時に 生成される ネットワークドライバーの 旧レプリケーション処理のほとんどを オーバーライド #if UE_WITH_IRIS if (ReplicationSystem) { UpdateReplicationViews(); SendClientMoveAdjustments(); ReplicationSystem->PreSendUpdate(DeltaSeconds); } else #endif // UE_WITH_IRIS { Updated = ServerReplicateActors(DeltaSeconds); }
ゲーム ReplicationState Data Add/Remove Actor Replication Fragments Replicated Actor Create/Destroy Replication Bridge ReplicationBridge Create/Destroy NetHandle ReplicationProtocol Replication System ステートデータ (プロパティのコピー) システム内部: コネクション毎: ・プロトコル ・ディスクリプタ ・プライオリティ ・フィルタ ・依存関係 ・スケジューリング ・シリアライゼーション
送信側 NetObjectの生存期間の管理 FNetRefHandleの生成と削除 受信側 ReplicationBridge アクターやオブジェクトのインスタンス化 生存期間の管理 ActorReplicationBridge アプリケーション側から呼び出すことになるAPI群が記述さ れている 中間クラスとしてObjectReplicationBridgeもある
UNetDriver* NetDriver = Actor->GetNetDriver();
UReplicationBridge* ReplicationSystem = NetDriver->GetReplicationSystem();
レプリケーション開始
コード例
UReplicationBridge* Bridge;
Bridge = ReplicationSystem->GetReplicationBridge();
UActorReplicationBridgeBridge* ActorBridge;
ActorBridge = Cast<UActorReplicationBridge>(Bridge);
if( ActorBridge )
{
FNetRefHandle Handle = ActorBridge->BeginReplication(Actor, Params);
}
ゲーム Actor Object FNetRefHandle FNetRefHandl FNetRefHandle NetGUIDとほぼ同じもの アクターやオブジェクトと ReplicationSystem内 の表現を結びつける ReplicationSystem
FNetHandle FNetRefHandleと名前が似ているが別物 こちらはほとんど WeakObjectPtrの中で見られ るオブジェクトインデックスと同じもの FNetRefHandle ≠ FNetHandle
ゲーム ReplicationState Data Add/Remove Actor Replication Fragments Replicated Actor Create/Destroy Replication Bridge ReplicationFragments Create/Destroy NetHandle ReplicationProtocol Replication System ステートデータ (プロパティのコピー) システム内部: コネクション毎: ・プロトコル ・ディスクリプタ ・プライオリティ ・フィルタ ・依存関係 ・スケジューリング ・シリアライゼーション
クラス毎のReplicate対象プロパティリスト ObjectのGetLifetimeReplicatedPropsから得た情報を 素に作られる ReplictionFragments 特性毎に分類 この特性(Trait)は COND_SimulatedOnly,COND_InitialOnlyなどを指す ReplicationState この情報をもとにReplicationSystem内にプロパティのコ ピーが保持される
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
設定と有効化
● (UE5.2~)TargetRuleのbUseIrisはデフォルトでtrue
● (Default)Engine.iniに以下の設定を記述
● Experimental: Getting Started With Iris @ Epic Developer Community
○ https://dev.epicgames.com/community/learning/tutorials/Xexv/unre
al-engine-experimental-getting-started-with-iris
[/Script/Engine.Engine]
!IrisNetDriverConfigs=ClearArray
; UE5.2以前
;+IrisNetDriverConfigs=(NetDriverDefinition="GameNetDriver",bEnableIris=true)
; UE5.3以降
+IrisNetDriverConfigs=(NetDriverDefinition="GameNetDriver",bCanUseIris=true)
[SystemSettings]
; Required for Iris:
net.SubObjects.DefaultUseSubObjectReplicationList=1
; Required if net.Iris.PushModelMode is set to 1:
net.IsPushModelEnabled=1
; Required if using an engine version past 5.1 where Iris compilation is enabled by default:
net.Iris.UseIrisReplication=1
[/Script/IrisCore.PartialNetObjectAttachmentHandlerConfig] MaxPartCount=4096 [/Script/IrisCore.NetBlobHandlerDefinitions] +NetBlobHandlerDefinitions=(ClassName=NetRPCHandler) +NetBlobHandlerDefinitions=(ClassName=PartialNetObjectAttachmentHandler ) +NetBlobHandlerDefinitions=(ClassName=NetObjectBlobHandler) [/Script/IrisCore.DataStreamDefinitions] +DataStreamDefinitions=(DataStreamName=NetToken, ClassName=/Script/IrisCore.NetTokenDataStream, DefaultSendStatus=EDataStreamSendStatus::Send, bAutoCreate=true) +DataStreamDefinitions=(DataStreamName=Replication, ClassName=/Script/IrisCore.ReplicationDataStream, DefaultSendStatus=EDataStreamSendStatus::Send, bAutoCreate=true) [/Script/Engine.NetDriver] ; All Iris replication is handled by various DataStream implementations that are ticked via the DataStreamManager instance in this channel. +ChannelDefinitions=(ChannelName=DataStream, ClassName=/Script/Engine.DataStreamChannel, StaticChannelIndex=2, bTickOnCreate=true, bServerOpen=true, bClientOpen=true, bInitialServer=true, bInitialClient=true) [/Script/IrisCore.NetObjectPrioritizerDefinitions] +NetObjectPrioritizerDefinitions=(PrioritizerName=Default, ClassName=/Script/IrisCore.SphereNetObjectPrioritizer, ConfigClassName=/Script/IrisCore.SphereNetObjectPrioritizerConfig) +NetObjectPrioritizerDefinitions=(PrioritizerName=PlayerState, ClassName=/Script/IrisCore.NetObjectCountLimiter, ConfigClassName=/Script/Engine.PlayerStateCountLimiterConfig) [/Script/IrisCore.NetObjectFilterDefinitions] +NetObjectFilterDefinitions=(FilterName=Spatial, ClassName=/Script/IrisCore.NetObjectGridFilter, ConfigClassName=/Script/IrisCore.NetObjectGridFilterConfig) +NetObjectFilterDefinitions=(FilterName=NotRouted, ClassName=/Script/IrisCore.FilterOutNetObjectFilter, ConfigClassName=/Script/IrisCore.FilterOutNetObjectFilterConfig)
[/Script/IrisCore.ObjectReplicationBridgeConfig] ; Poll configs ;+PollConfigs=(ClassName=/Script/Example.Pawn, PollFramePeriod=0, bIncludeSubclasses=true) ; Filters DefaultSpatialFilterName=Spatial ; Clear all filters !FilterConfigs=ClearArray +FilterConfigs=(ClassName=/Script/Engine.LevelScriptActor, DynamicFilterName=NotRouted) ; Not needed +FilterConfigs=(ClassName=/Script/Engine.Actor, DynamicFilterName=None)) ; Info types aren't supposed to have physical representation +FilterConfigs=(ClassName=/Script/Engine.Info, DynamicFilterName=None) +FilterConfigs=(ClassName=/Script/Engine.PlayerState, DynamicFilterName=None) ; Pawns can be spatially filtered +FilterConfigs=(ClassName=/Script/Engine.Pawn, DynamicFilterName=Spatial)) +FilterConfigs=(ClassName=/Script/EntityActor.SimObject, DynamicFilterName=None)) +DeltaCompressionConfigs=(ClassName=/Script/Engine.Pawn)) +DeltaCompressionConfigs=(ClassName=/Script/Engine.PlayerState)) ; PrioritizerConfigs ; BaseEngineに記述済み +PrioritizerConfigs=(ClassName=/Script/Engine.PlayerState, PrioritizerName=PlayerState, bForceEnableOnAllInstances=true)
フィルタリング オブジェクトをどの接続に乗せるべきかを選択する仕組み 静的フィルタ 動的フィルタ カスタムのフィルタ オーナーだけに送信する bOnlyRelevantToOwner 2Dグリッドフィルタ UNetObjectGridFilter 固有設定を実装可能 シンプルに通信から除外 UFilterOutNetObjectFilter
フィルタリング設定 [/Script/IrisCore.NetObjectFilterDefinitions] +NetObjectFilterDefinitions=(FilterName=Spatial, ClassName=/Script/IrisCore.NetObjectGridFilter, ConfigClassName=/Script/IrisCore.NetObjectGridFilterConfig) +NetObjectFilterDefinitions=(FilterName=NotRouted, ClassName=/Script/IrisCore.FilterOutNetObjectFilter, ConfigClassName=/Script/IrisCore.FilterOutNetObjectFilterConfig) [/Script/IrisCore.ObjectReplicationBridgeConfig] ; Filters DefaultSpatialFilterName=Spatial ; Clear all filters !FilterConfigs=ClearArray +FilterConfigs=(ClassName=/Script/Engine.LevelScriptActor, DynamicFilterName=NotRouted) ; Not needed +FilterConfigs=(ClassName=/Script/Engine.Actor, DynamicFilterName=None)) ; Info types aren't supposed to have physical representation +FilterConfigs=(ClassName=/Script/Engine.Info, DynamicFilterName=None) +FilterConfigs=(ClassName=/Script/Engine.PlayerState, DynamicFilterName=None) ; Pawns can be spatially filtered +FilterConfigs=(ClassName=/Script/Engine.Pawn, DynamicFilterName=Spatial)) +FilterConfigs=(ClassName=/Script/EntityActor.SimObject, DynamicFilterName=None))
優先度 パケットに乗せるべきアクターを順序付けする 送信されなかったアクターは優先度が上がっていくのでいつかには送信される 静的優先度 動的優先度付け カスタムの優先度付け 固定値設定 AActor::NetPriority 位置ベースの優先度付け USphereNetObjectPrioritizer 固有設定を実装可能 送信数の制限 NetObjectCountLimiter
優先度の設定 [/Script/IrisCore.NetObjectPrioritizerDefinitions] +NetObjectPrioritizerDefinitions=(PrioritizerName=Default, ClassName=/Script/IrisCore.SphereNetObjectPrioritizer, ConfigClassName=/Script/IrisCore.SphereNetObjectPrioritizerConfig) +NetObjectPrioritizerDefinitions=(PrioritizerName=PlayerState, ClassName=/Script/IrisCore.NetObjectCountLimiter, ConfigClassName=/Script/Engine.PlayerStateCountLimiterConfig) [/Script/IrisCore.ObjectReplicationBridgeConfig] ; PrioritizerConfigs +PrioritizerConfigs=(ClassName=/Script/Engine.PlayerState, PrioritizerName=PlayerState, bForceEnableOnAllInstances=true)
アップデート頻度
アップデート頻度 NetUpdateFrequency ForceNetUpdate ああああ net.Iris.EnableForceNetUpdate True: ForceNetUpdateで強制更新 False: DirtyFlagで更新 ああああ
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
エンジンにIrisが含まれてビルド Build=Uniqueは不要です 弊社タイトルへ組み込みで検証中 Irisの現在地(UE5.3) アクター単位の PushModel 非同期ロード、リプレイ未対応
展望 https://jira.it.epicgames.com/issues/?jql=project%20in%2 0(UE%2C%20FORT)%20AND%20issuetype%20in%20(Epic %2C%20Initiative%2C%20Strategy)%20AND%20compone nt%20%3D%20%22UE%20-%20Networking%20-%20Iris% 22
ReplicationGraphと同等の機能実装 Irisの展望(5.4~) マルチスレッド化 コネクション毎の並列処理など プロパティレベルの PushModel Dirtinessと同時にReplicationStateを書き込む
Irisのコンセプト Table of contents Irisの構造 Irisの設定と有効化 現在地と展望 ネットワーク最適化Tips
PushModel Iris Legacy 現時点ではアクター毎の管理 Iris
PushModel
● Dirtyフラグをゲーム側から書き込む
● プロパティの変更検出をスキップし負荷を軽減
● Net.IsPushModelEnabled = true
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
・・・
FDoRepLifetimeParams SharedParams;
SharedParams.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS_FAST(AActor, bReplicateMovement, SharedParams);
・・・
}
void AActor::SetReplicatingMovement(bool bInReplicateMovement)
{
bReplicateMovement = bInReplicateMovement;
MARK_PROPERTY_DIRTY_FROM_NAME(AActor, bReplicateMovement, this);
}
パッケージビルドでのPushModel ● Target.csに bWithPushModel = true が必要 [RequiresUniqueBuildEnvironment] public bool bWithPushModel { get => bWithPushModelOverride ?? (Type == TargetType.Editor); set => bWithPushModelOverride = value; }
非同期読み込み Legacy
非同期読み込み ● メモリに無いクラスやアセットが複製された時に非同期ロードを行いロード されるまでパケットを遅延する仕組み ● net.AllowAsyncLoading=1を設定する ● 動作確認 ○ net.TrackAsyncLoadingGUIDThreshold=1.0 ;閾値(秒)
パケット受信処理
パケット受信処理 16.6ms ServerMove WorldTick Client1 WorldTick Client2 WorldTick Server
パケット受信処理 16.6ms WorldTick Client1 WorldTick Client2 33.3ms WorldTick Server
パケット受信処理 CharacterMovement Packet1 CharacterMovement CharacterMovement CharacterMovement Packet2 Packet3 Packet4
パケット受信処理対策 ● CharacterMovementの負荷に気をつける ● Overlap ● 複雑なコリジョン ● RootMotion ● コンポーネントの構造 ● サーバーの処理時間のバジェットに余裕を持つ ● ServerMove送信頻度を調整する(→次項)
ServerMove送信頻度
ServerMove送信頻度 ● 移動合成(MoveCombine) ● ● MaxMoveDeltaTime 移動パケット合成 ● ClientNetSendMoveDeltaTime
TickFlushの分散処理
TickFlushの分散処理 TickFlush WorldTick GameThread SlateTick WorldTick GameThread TickFlush WorkerThread SlateTick
TickFlushの分散処理 ● 情報共有:ReplicateのBroadcastTickFlushの並列化 ● https://udn.unrealengine.com/s/question/0D52L00004luuR5SAI/ ● ListenServer利用時 ● 平行して走る処理で書き換えなどが起こった場合にクラッシュを含む致命的な問 題が起こりえるため運用には注意が必要です
サーバーフレームレート
DedicatedServerフレームレート ● フレームレートをクライアントと一致させる必要は無い ● CPUコストを削減 ● DefaultEngine.ini [/Script/OnlineSubsystemUtils.IpNetDriver] ;サーバーが許容する帯域幅 MaxServerTickRate=20 ● 弊社タイトルでの設定値は20
帯域幅設定
帯域幅設定 ● クライアントからの要求量とサーバー側での範囲制限 ● DefaultEngine.ini [/Script/Engine.Player] ;クライアントが要求する初期帯域幅 ConfiguredInternetSpeed=123456 [/Script/OnlineSubsystemUtils.IpNetDriver] ;サーバーが許容するクライアント毎の帯域幅 MaxInternetClientRate=123000 ● ● APlayerController::SetNetSpeedによる指定 弊社タイトルでの設定値は200000
GameNetworkManagerによる帯域幅設定 ● ● ● デフォルト実装ではListenServerのみ 接続クライアント数による按分 明示的にUpdateNetSpeedsを呼び出す AGameNetworkManager* NetworkManager = GetWorld()->NetworkManager; if(NetworkManager) { NetworkManager->UpdateNetSpeeds(false); } ● DefaultGame.iniに設定を記述 [/Script/Engine.GameNetworkManager] TotalNetBandwidth=200000 MaxDynamicBandwidth=40000 MinDynamicBandwidth=20000
帯域幅の確認方法 ● DedicatedServer ● ログのClient netspeed is~を確認 LogNet: Client netspeed is 1234565 ● ListenServer ● Displayall Player CurrentNetSpeed
GameplayAbilityの モンタージュ同期
GameplayAbilityのモンタージュ同期 ● Gameplay Ability Sysytem @ UnrealEngine.com ● https://docs.unrealengine.com/5.3/ja/gameplay-ability-sys tem-for-unreal-engine/ ● GAのモンタージュ制御 ● 再生位置同期のために 毎Tick構造体が書き変わり送信対象に ● RepAnimMontageInfo ● 頻繁にGAのモンタージュ機能を使うのであれば対処が必要
GameplayAbilityのモンタージュ同期 ● UAbilitySystemComponentのTick内で更新される ● Tickを止めて AnimMontage_UpdateReplicatedData 呼び出しをブロックする ● GetShouldTick のオーバーライド ● UpdateShouldTick によりTickの状態を更新
ご清聴ありがとうございました 2023 Private & Confidential