概要
Unreal Engine 4 (以下UE4)でUDP経由でJSON文字列を受け取る場合、文字列の取り扱いに気をつけよう。
UDPで受信したデータはバッファが使いまわされるため可変長データ、特に文字列ではサイズをよく見て扱う必要がある。
またJSON変換用のAPIには2通りあるので用途に応じて変えよう。
着想に至った経緯
- UE4でネットワーク上の外部デバイスから送られてくる操作情報を反映させたい。
- デバイスから送信する情報はこちらで設計できるものとして、UE4以外のものでも通用する形にしたい。それこそWebブラウザでも使いたいので、できればJSON。
- そのために中継サーバを立てるようなことはしない = WebSocketはこの時点で候補から除く。
- デバイスからのデータが実際に受信されたかどうかは不問 = この時点でUDPにしたい。
という経緯のもと、UE4でUDPでJSONを受け取る機能を実装します。
デバイス側の送信部
Pythonで記述してUbuntuマシンで動作。
JSON文字列をUTF-8にエンコードして送信。
UE4での機能設計
UE4にはブループリントという優れたビジュアルプログラミング環境が存在する。
実装する機能はこのブループリントで取り扱えることを前提とする。
これは既にアニメーションブループリント(以下、アニメーションBP)でキャラクタの挙動を記述しているものがあるためである。
一方、UDPもJSONもブループリントだけでは完結できない。
C++によるAPIが提供されているため、C++で実装を行っていく。
C++で実装を行っても、ブループリントに情報を公開する手段は提供されている。
アニメーションBPでC++の機能を実装するため、
UAnimInstanceを継承したクラスとして作成していく。
最終的にはアニメーションBPの親クラスを作成したクラスで置き換えることになる。
今回はWindows10のUE 4.25, 4.26.1で作成している。
UE4でのUDP
参考にさせていただいたサイト
要約すると、
FUdpSocketBuilder
を使ってFSocket
を作成。
さらにそれを使ってFUdpSocketReceiver
を作成し、
OnDataReceived().BindUObject()
でコールバックを設定するといい。
コールバック関数の型は
void func(const FArrayReaderPtr&, const FIPv4Endpoint&)
となっていて、FArrayReaderPtr
にデータへの参照が渡されてくる。
なお、このコールバックの設定はコンストラクタで行ってはならない。
ブループリントの性質上C++のクラスではなく、それを継承したクラスが使われるため、
コンストラクタでコールバックを設定してしまうとうまく動かない。
UE4でのJSON
調べた限り2通り方法がある。
FJsonSerializableを用いた方法
参考にさせていただいたサイト
https://historia.co.jp/archives/7636/
この方式は、FJsonSerializable
を継承した構造体を定義し、
然るべきマクロを設定することで文字列と構造体の相互変換を行うことができる。
FJsonSerializerを用いた方法
参考にさせていただいたサイト
https://gist.github.com/gamerxl/6c8f4426866c82f7327d063343d02fe9
この方式は、TJsonReaderFactory
を使って文字列からTJsonReader
を作成。
FJsonObject
というオブジェクトに格納する。
FJsonSerializer::Deserialize(JsonReader, JsonObject)
で格納し、
JsonObject->GetStringField("field_name")
などとして読み出す。
(Get***Field
は型ごとに用意されている。)
あれ、文字列…
ここで問題となるのは、文字列の取り扱いである。
- Python側ではUTF-8エンコードで送信している。
- UE4側でUDPで受け取った情報(
const FArrayReaderPtr& data
とする)はuint8*
型のポインタ(data->GetData()
)と、受信サイズ(data->GetNum()
)にアクセス可能である。
つまり、UE4側で受け取った文字列をUTF-8からUE4で解釈できる型(TCHAR
およびFString
)に変換しなければならない。
文字コードの変換はマクロが用意されているので、
UTF8_TO_TCHAR(data->GetData());
で、行けると思うじゃん?
実はこのコードだと大変まずいことが起きる。
受信した文字列のサイズが前回よりも小さい場合、バッファは前回と同じサイズが確保されたままになっている。
また、UDPで受信した文字列の末尾に\0
は入らない。
(このルールはあくまでC/C++の文字列のルールなのでバイナリ送信には無関係だから)
すると何が起きるかというと、
前回受信した文字列の上に今回受信した分の文字列が上書きされた状態でバッファの最後まで読もうとする。
そうするとJSON文字列としては無効なので変換に失敗する。
回避策として、例えばUTF8_TO_TCHAR
にサイズ指定ができればよいが、それはできない。
受信バッファを直接操作はconstなのでできない。
ので、受信データサイズも受け取れるという性質を利用し、
サイズを正しく調整した一時バッファを作成して対処することにした。
さらにここで困ったのは、UTF8_TO_TCHAR
は生のポインタを要求してくるところ。
まさか今更malloc/free
はありえないので、
(メモリ管理を手で書くのは古くてイケてない以上に危険なのでやるべきでもやらせるべきでもないです。)
std::vector<char> buf
を作って&buf[0]
を渡すという古典的なやり方で回避。
まとめ
- UE4でUDPを受信する場合、バッファは同じものが使いまわされる。可変長のデータ、特に文字列を受け取る際には要注意。
- UE4でJSONを取り扱う場合、やり方は2通りある。