dynamicを用いて動的にJSONを処理する
環境
- ruby 3.0.0p0
- faye-websocket
- Unity 2019.4.14f1
- websocket-sharp
内容
クライアント側をUnity+C#、サーバ側をRuby、通信プロトコルをWebsocketとした構成で、
サーバ側で生成された更新データ(JSON)を動的な変数を介してクライアント側で操作する。
確認
サーバ
サーバ側では、以下の様なRubyのスクリプトを用意してあげる。
このスクリプトは、websocket通信において発生する各イベント用の処理を実装している。
発生する各イベントは以下の通り。
| イベントハンドラ | イベント内容 |
| ---- | ---- | ---- |
| ws.on :open
| Websocketコネクションが確立された時に実行される. |
| ws.on :message
| Websocketコネクション上で通信データを受け取った時に実行される. |
| ws.on :close
| Websocketコネクションが切断された時に実行される. |
require 'faye/websocket'
require 'json'
require './lib/utils'
App = lambda do |env|
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env)
ws.on :open do
p "Connected! ws object ID >>> #{ws.object_id}"
end
ws.on :message do |event|
r = JSON.parse(event.data)
r["power"] = 3
r["health"] = 5
insert_list(r)
insert_hash(r)
dumped_json_str = JSON.dump(r)
p "submit data to #{ws.object_id}"
ws.send(dumped_json_str)
end
ws.on :close do |event|
p [:close, event.code, event.reason]
end
ws.rack_response
else
[200, { 'Content-Type' => 'text/plain' }, ["you do not use websocket.\n\n"]]
end
end
上記のスクリプトでは、Websocketコネクションでクライアントからメッセージを受け取った時(つまり、ws.on :message
のコードブロックが実行される)、そのメッセージはevent.data
として取り出され、JSON
ライブラリによりパースされている。
そして、パースされたデータはr
という変数に格納している。
処理確認
例えば、クライアント側から以下のようなJSON形式データ(String型)が送られてきたとする。
"{"user_id": 100, "user_rank": "silver"}"
これをサーバ側で受け取ると、r
は以下のようなHashデータになる。
{"user_id"=>100, "user_rank"=>"silver"}
そしてこのあとに、このHashデータに対して値を挿入している。
r["power"] = 3
r["health"] = 5
insert_list(r)
insert_hash(r)
insert_list
とinsert_hash
は、utils.rb
という自作ファイルに作っておいた関数である。
それぞれ、「Hashを受け取り、要素に配列を追加する」「Hashを受け取り、要素にHashを追加する」という処理を行う。
utils.rb
def insert_list(d)
sample_list = [10, 20, 30]
d["set_list"] = sample_list
end
def insert_hash(d)
sample_hash = {
"force" => "blue",
"rarity" => 5
}
d["set_hash"] = sample_hash
end
データが挿入された状態で、もとのHashデータr
の中身を確認すると以下の様になっている。
{
"user_id"=>100,
"user_rank"=>"silver",
"power"=>3,
"health"=>5,
"set_list"=>[10, 20, 30],
"set_hash"=>{"force"=>"blue", "rarity"=>5}
}
最後に、JSON形式データ(String型)にDumpし、Websocketコネクションでクライアント側に送信する。
dumped_json_str = JSON.dump(r)
ws.send(dumped_json_str)
ここまでで、動的言語であるRubyで生成したJSONをクライアント側に送信した。
クライアント
ここからは本記事の本題である、クライアント側の処理を確認していく。
クライアント側では、以下のようなC#のプログラムを用意してあげる。
using Newtonsoft.Json.Linq;
WSClient = new WebSocket(ENDPOINT);
WSClient.OnOpen += (sender, e) => { Debug.Log("WebSocket open"); };
WSClient.OnMessage += (sender, e) =>
{
Debug.Log("Data recieved!");
dynamic dynamic_object = JObject.Parse(e.Data);
};
WSClient.OnError += (sender, e) => { Debug.Log("Websocket error : " + e); };
WSClient.OnClose += (sender, e) => { Debug.Log("WebSocket closed..."); };
WSClient.Connect();
今回の様に、特にゲーム開発においてはクライアント側の開発にUnityを用いることが多いが、
そのときに使用するC#は動的言語ではなく静的言語である。
つまり、予めオブジェクトの構造を定義しておいて、コンパイル時に決定しておく必要がある。
しかし、今回の様にサーバ側から様々なJSON構造のデータを送信したい場合(例えば、ゲームサーバで処理した結果のデータに多くのパターンがある時など)、予め受け取るJSONの構造を決定させてしまうと、その構造のデータしかUnity側で受け取ることができなくなる。
C#側でも動的に変化する構造のデータを受け取りたい場合、dynamic
キーワードを用いることで、それが可能になる。これは、C#において動的型付け変数を生成するキーワードである。
上記のコードでは、以下の箇所で動的型付け変数を生成している。
dynamic dynamic_object = JObject.Parse(e.Data);
処理確認
C#においても、Websocket用のライブラリ(websocket-sharp)を用いて
通信可能なプログラムを作成している。
こちらでは、OnMessage
のイベントハンドラに登録されたコールバックが、
メッセージを受け取った時に実行される処理になる。
先ほどのサーバ側で生成したJSON形式データ(String型)を受け取った時、そのデータは、
e.Data
で取り出すことができる。e.Data
は以下の様になっている。
// これはString型である
{"user_id":100,"user_rank":"silver","power":3,"health":5,"set_list":[10,20,30],"set_hash":{"force":"blue","rarity":5}}
このe.Data
(String型)を、.NETライブラリであるNewtonsoft.Json
のJObjectでパースし、
動的型付け変数dynamic_object
に代入している。
このときの、dynamic_object
を確認すると以下のようになっており、JObject型である。
{
"user_id": 100,
"user_rank": "silver",
"power": 3,
"health": 5,
"set_list": [
10,
20,
30
],
"set_hash": {
"force": "blue",
"rarity": 5
}
}
そして、このdynamic_object
の各要素は、以下の様になる。
- 文字列や数値は、JValue型になる。
- 配列は、JArray型になる。
- 辞書は、JObject型になる。
- 配列や辞書はループ処理の適用が可能。
- 各要素に対して、ドットでアクセス可能。
- JValue型の文字列は、C#のstringに変換可能。
- JValue型の数値は、C#のintに変換可能。
つまり、以下の様な操作が可能となる。
string user_rank = dynamic_object.user_rank;
int power = dynamic_object.power;
foreach(var j_value in dynamic_object.set_list)
{
int item = j_value;
}
string force = dynamic_object.set_hash.force;
int rarity = dynamic_object.set_hash.rarity;
このように、C#側で用意されている型への変換や、ループ処理といった適用ができる。
そのため、dynamic
を用いて動的に変化するデータをクライアント側で受け取ったあと、
その内容に応じて好きな処理を適用できるようになる。
Discussion