📖

dynamicを用いて動的にJSONを処理する

5 min read

環境

  • 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_listinsert_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を用いて動的に変化するデータをクライアント側で受け取ったあと、
その内容に応じて好きな処理を適用できるようになる。