ほげたつブログ

プログラムとアニメーションをかじって生きてる

UE4 - Actorレプリケーションのパケットをキャプチャしてみる

こちらは「UE4 Advent Calendar 其の弐」2日目の記事です。

Unreal Engine 4 (UE4) 其の弐 Advent Calendar 2015 - Qiita

今回のお題は「Actorレプリケーションのパケットをキャプチャしてみる」です。

概要

UE4にはネットワークマルチプレイを実装するための機能が色々と揃っています。
その辺りは今年のアンリアルフェスでもalweiさんが語られてましたし、そろそろ周知されてきた頃ですかね。
(自分が作ったWebApiプラグインに関しても紹介されています。ありがたや。)

www.slideshare.net

そうなってくると気になるのは内部実装について。

  • 実際に送られているパケットはどのようなものなのか。デルタ圧縮とかやってる?
  • 親が切断されてしまった場合は子が自動で昇格してくれるのか。
  • セッションから退出した場合に復帰はできるのか。
  • 移動量とかの補間ってどんな感じ?
  • パケットロスした場合の挙動はどうなるのか。過去分をまとめて送るとか対策はある?


ざっと考えるだけでも色々出てきます。
他にも実際に使う時には調べるべきものは色々とあるでしょう。

ひとまず今回はレプリケーション時のパケットをキャプチャしてみます。

パケットキャプチャ環境を整える

今回はパケットキャプチャで検索すると大量に情報が出てくる「Wireshark」を使用します。

Wireshark · Go Deep.

Wireshark の使い方は下記サイトを参考にしました。

beginners-network.com

ちなみに今回の検証と関係ありませんが、ARPスプーフィングすることで別マシンのゲートウェイを任意のPCに変更することができるらしいですね。
これでゲーム機等で送受信されているパケットを見ることもできます。

ottati.hatenablog.com


パケットキャプチャしてみる

ひとまず ThirdPerson テンプレートをベースに OnlineSubsystemSteam でセッション管理を行い、キャプチャしてみました。

f:id:hogetatu:20151201012017p:plain

定期的にパケットが送受信されているのが確認できます。
送られているパケットを見てみたところ、プロパティ名がそのまま名前で入っているようなこともなく、サイズを考慮してシリアライズされていることがわかります。
ただ、放置/移動/ジャンプをしてもパケット量が変わらないため、デルタ圧縮(前回フレームからの差分更新による通信量の削減対応)は行っていないようですね。
ふむふむ…。



なんかこれで終わるのは微妙ですね



やはりキャプチャしてみるだけだと、「過去分をまとめて送るようなパケットロス対応が行われているか」等のわからない事もあります。
仕方ないのでエンジンコードを眺めてみます。

実際にレプリケーションはどの辺りでやってるのか

先に言っておきますが、これを書いている現在の日時は 12/1 1:39 です。
エンジンコードを読むにはチョト眠いです。
なので深く読み込むというよりは必要な所だけさらっと読むだけで勘弁して下さい。
(本当にパケットキャプチャだけで終わらせる予定だったんです。。)
以下、実際に自分がレプリケーション周りの実装を追った時に辿った道のりです。


まず、エンジンコードは膨大なので取っ掛かりを見つけることから始めます。


UDPでパケットを送受信しているので、ソケット周りのコードを見つけてブレークポイントを張ることから始めます。
単純なUDP通信に関しては既に調べ済みで、UdpSocketSender/UdpSocketReceiver クラスで実装されていることはわかっています。
とりあえず Send 関数に張ってみたところ、レプリケーションを行ってもブレークポイントで止まることはありませんでした。
残念ですが、次の取っ掛かりを探します。


アクターのレプリケーションなので Actor.cpp を眺め始めます。
するといくつかのネットワーク対応周りの関数内で NetDriver なるものを見つけることができます。
ひとまずこのクラスの中身を見てみます。


NetDriver.h と NetworkDriver.cpp に UNetDriver クラスの実装を見つけました。
さらっと見た感じだと、アクターのレプリケーションはこのクラスが行っているように見えます。
更にエンジン全体に grep をかけると、UNetDriver を継承している IpNetDriver や WebSocketNetDriver を見つけることができました。
名前だけでも見当は付きますが、中身の実装を眺めてみると、UNetDriver を通信手法に合わせて拡張したものだということがわかります。
OnlineSubsystemSteam を使用しているので IpNetDriver が使われているだろうという予想でブレークポイントを張ってみると、案の定そのようになっていました。


ということで、UNetDriver の中身をもう少し見てみます。
UNetDriver クラス内にあからさまにレプリケーションを行ってそうな ServerReplicateActors という関数があります。
もう眠いので早く終わらせたいです。さらっとだけ眺めましょう。
どうも UNetConnection クラスがチャンネルの保持等を担当しているようなので見てみます。


UNetConnection クラスのヘッダーを眺めてると、WriteBitsToSendBuffer という関数を見つけました。
中身を見てみると、SendBuffer に書き込みを行っているようです。
早く終わらせて寝たいので、とりあえずここにブレークポイントを張ってみます。


・・・かかったー!


ここまで来ればもう勝ったも同然です。
コールスタックを確認します。

f:id:hogetatu:20151201030926p:plain

CharacterMovementComponent が起点となっているので、移動に関するレプリケートだと予想できます。
書き込もうとしているのは UNetDriver::InternalProcessRemoteFunction なので、そちらを見てみます。


InternalProcessRemoteFunction ではアクターやサブオブジェクト、リモート実行する関数、パラメータを引数に取り、それらの情報を Bunch に書き込んでから送信しています。
Bunch は関数内で作られているので、処理単体のパケット情報しか送信されていない事がわかります。
また、パケットのレイアウトは FRepLayout で決められています。
ちなみに FRepLayout::SendPropertiesForRPC を見てみると、プロパティをシリアライズしている箇所を見つけることができました。
そして最終的にチャンネルが SendBunch してパケットを送信しています。


過去分をまとめて送ってるとかはない!
以上!寝る!


…この関数内で net.RPC.Debug なるコンソールコマンドを見つけました。
これに 1 を指定すると、通常のログレベルでは出ていない RPC のログを Warning として出力することで可視化できます。

f:id:hogetatu:20151201035022p:plain

便利だ…知らなかったぜ…。

明日のアドカレ

明日(3日目)は katze さんによる入力デバイスを使えるようにするプラグインの話です。
入力デバイスとかプラグインとかそれ系の話は好きなので、とても楽しみですね。