こんにちは。エンジニアの原です。
これまでヒストリアでは定期的にブログでUE4に関するTipsをお届けしておりました。
それは今後も変わる予定はありませんが、もう一歩進んだ情報を提供するために TechResearch というチームを結成しました。
TechResearchではブログとは別に、不定期で技術情報をお届け致します。
ブログで書くには少し内容が複雑だったり、量が多いものを紹介していく予定です。
記念すべき第一回の記事は UDP通信 に関してです。
UDPの活用事例
UE4ではネットワーク対応ゲームを作るための機能がとても充実しています。
こちらに関しては中村さんが UNREAL FEST 2015 YOKOHAMA にてご紹介されております。
UE4ではクライアント間での通信プロトコルにはUDPを使用しています。
UDPとは高速転送を行うためのシンプルなプロトコルです。
User Datagram Protocol
簡単にUDPの特徴を以下に列挙します。
・ヘッダサイズが小さい(8bytes)
・コネクションレス・プロトコルであるため、コネクションを確立せず一方的に送信する
・複数の相手に同時にデータを送信できる(ブロードキャストやマルチキャストが利用できる)
・パケットが到達する保証はない
・受信者までパケットが到達する場合にパケットが欠損する可能性がある
・パケットが到達する順番も保証されていない
不便なところが多いように見受けられますが、このようなシンプルなプロトコルだからこそ高速転送が可能となります。
ヘッダのレイアウトは以下の通りです。
16bit | 16bit | 16bit | 16bit |
送信元ポート | 宛先ポート | パケット長さ | チェックサム |
送信元ポート | パケットの送り元を識別するための情報です。 パケットを受け取ってから返答する際に利用されます。 |
---|---|
宛先ポート | パケットの宛先を識別するための情報です。 ポート番号は自由に設定できますが、別のシステムが使用している場合があるので バッティングしないように考慮する必要があります。 基本的に0~1023までは標準で使用されているので使えません。 ポート番号一覧 |
パケット長さ | パケット全体の長さです。 ヘッダ(8bytes)+データ(可変)の長さとなります。 |
チェックサム | パケットの整合性を検査するための情報です。 受信したパケットから計算されたチェックサムと、 パケット中に格納されたチェックサムが異なっていた場合、 パケットは破棄されます。 |
UDPは様々な分野で使われています。
UE4のようにネットワーク間で RPC(RemoteProcedureCall) のために利用することもありますが、
別アプリケーションとの連携にもUDPを用いる事は多いです。
・AssetViewer等のGUI上から設定されたプロパティをゲームエンジン(レンダリングエンジン)での描画に反映させたい
・画像認識モジュールから出力される認識情報を他のアプリケーションで使いたい
・モーションキャプチャーから出力されたデータをリアルタイムにゲームエンジンに反映して確認したい
これらの要望は全てUDPを用いた通信で解決できます。
単一アプリケーションでは解決できない問題が多くなり、様々なモジュールが開発される時代ですので、UDP通信でできること、できないことを知っておくことは非常に重要です。
RPCでの同期
UE4では RPC により別クライアントとの同期を行っています。
それぞれのクライアントで実行されたイベントが別クライアント上でも実行されるような作りになっています。
例えばThirdPersonテンプレートをマルチプレイする時、以下の情報がRPCにより同期されています。
- キャラクターのTransform情報
→ リモート実行される関数:UCharacterMovementComponent::ServerMove_Implementation
→ 加速度や位置、Viewの角度等を引数として受け取っています - カメラの位置と回転
→ リモート実行される関数:APlayerController::ServerUpdateCamera_Implementation
→ 加速度や位置、Viewの角度等を引数として受け取っています
また、UE4にてRPCを行うためにUDPを使ってデータを送信しているのは NetworkDriver です。
UNetDriver::InternalProcessRemoteFunction がアクターやサブオブジェクト、リモート実行する関数、各種パラメータを引数として受け取り、チャンネルに対してSendBunchしてパケットを送信しています。
このBunchは関数内で作られているため、それ自体はリモート実行する関数単位での送受信となります。
つまりはパケットロスへの対応として、過去分をまとめて送ったりという対応は現時点では存在しません。
ちなみにパケットのレイアウト(パケットの中での各種プロパティの配置情報)は FRepLayout が決めています。
UNetDriver::GetFunctionRepLayout にてキャッシュ済みレイアウト情報を検索し、
無ければ FRepLayout::InitFromFunction にて関数に紐づくプロパティ情報リストからレイアウトを決めています。
この辺りを読んでいけばレイアウト情報がどのように作られ、パケットサイズがどの程度になるのかを把握できます。
アプリケーション間でのUDP通信
UE4でのRPCは上記のような流れで処理されますが、実際にアプリケーション間でUDP通信を行いたい場合はRPCを使用することはできません。
Actorを介さない生のUDP通信を行いたい場合、UdpSocket を使用します。
今回は UdpSocket を使用するサンプルとして、以下のものを用意しました。
・C#で書かれたGUIアプリケーション上で物体のTransform情報と色情報のプロパティを設定できる
・GUI上でプロパティが変更されたら即座にUE4で作られたゲーム上でプロパティの変更をプレビューできる
こちらはGUIアプリケーション、UE4ゲームを合わせてGitHubにて公開しています。
https://github.com/historia-Inc/Tech_UDPDemo
デモ動画はこちらです。
UE4実装内容の説明
UdpSocket は UdpSocketBuilder によって作られます。
実際のコードは以下の様なものです。
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 |
bool UUdpSocket::CreateSocket() { if (IpAddress.IsMulticastAddress()) { Socket = FUdpSocketBuilder(TEXT("Multicast")) .WithMulticastLoopback() .WithMulticastTtl(1) .JoinedToGroup(IpAddress) .BoundToPort(Port) .Build(); } else { Socket = FUdpSocketBuilder(TEXT("Unicast")) .BoundToAddress(IpAddress) .BoundToPort(Port) .Build(); } if (!Socket) { UE_LOG(LogTemp, Error, TEXT("Build Socket failed.")); return false; } return true; } |
その後、例えばUDP通信で送られてきたデータを受け取りたい場合はレシーバーを作成します。
レシーバーにはデータ受信時のコールバックを登録することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool UUdpSocket::CreateReceiver() { Receiver = new FUdpSocketReceiver(Socket, FTimespan::FromMilliseconds(1), TEXT("Receiver")); if (!Receiver) { UE_LOG(LogTemp, Error, TEXT("Construct Receiver failed.")); return false; } Receiver->OnDataReceived().BindUObject(this, &UUdpSocket::OnDataReceived); return true; } |
今回のサンプルではコールバックの中でプロトコルのパースを行っています。
このプロトコルとはパケットの種類別に定義されています。(今回だとTransformとColorですね)
それぞれにパケットの内容に合わせたパース処理が書かれており、データをスタックしてタイムスタンプを見て最後のパケット情報を返すことができるようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void UUdpSocket::OnDataReceived(const FArrayReaderPtr& Reader, const FIPv4Endpoint& Sender) { if (GEngine) { FString DataStr = FString::FromHexBlob(Reader->GetData(), Reader->Num()); UE_LOG(LogTemp, Log, TEXT("%s"), *DataStr); } if (!Protocol->Parse(Reader)) { UE_LOG(LogTemp, Error, TEXT("UUdpSocket::OnDataReceived failed.")); } } |
UE4ではバイナリ情報のパース処理がとても簡潔に記述できます。
データ受信コールバックでは FArrayReaderPtr の参照を引数として受け取ることができますが、こちらは FArchieve を継承した構造体であるため、operator<< によるパース処理に対応しています。
このパース処理は基本的なプリミティブ型に加え、オブジェクトもパースできるような作りとなっています。
こちらの実装は FArchive 及びそれを継承した各クラスの Serialize 関数を見ていけば流れがわかります。
例えば今回のTransform情報をパースするための構造体は以下の様な実装となります。
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 |
USTRUCT(BlueprintType) struct FTransformData { GENERATED_USTRUCT_BODY() public: friend FArchive& operator<< (FArchive& Ar, FTransformData& Data) { auto& ReturnValue = Ar << Data.Location << Data.Rotation << Data.Scale << Data.TimeStamp; return ReturnValue; } public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Network|TransformData") FVector Location; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Network|TransformData") FRotator Rotation; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Network|TransformData") FVector Scale; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Network|TransformData") float TimeStamp; }; |
ソケットとプロトコルのインスタンスはGameModeで管理しているため、ゲーム中はGameModeからそれぞれのプロトコルを取得して最新の状態に更新するということをやることで、C#で作成したGUIからTransform及び色情報をリアルタイムに変更できるようになります。
終わりに
本文でも触れましたが、UDP通信はゲーム、ノンゲーム問わず多くの作品で活用されています。
ゲーム中で直接使用しなくても、ゲームを作るためのツールの中でUDP通信を利用するケースも多いです。
弊社が制作していたり、お手伝いさせていただいているプロジェクトの中にもUDP通信を利用しているものは多く存在します。
苦手な事や手間がかかる部分もありますが、UDP通信を扱えるようになると、できることの幅が一気に広がります。
ヒストリアでは新卒/中途採用を強化中です。
まだまだ若い会社ではありますが、数多くのUE4案件に関わらせて頂いております。
コンテンツ的にも、技術的にもより良いものを日々追求していっておりますので、もし興味がある方は是非一度会社見学にいらしてください。