Skip to content

JavaScriptでMMOGを作ってみよう その5 RPCとORM

この連載では、MMOG開発ではまだ実績が少ないJavaScriptを使っていますが、

拙著「オンラインゲームを支える技術」という本では実績のある制作技法を紹介しています。そちらもどうぞ!

さて前回はnode.jsとUnityの間で、TCP socketを使った通信をする方法を調べました。

 

ゲームサーバの構成はこんな感じでした:

MySQL – (TCP接続) – ゲームサーバ – (TCP接続) – Unity3Dクライアント

 

今回はゲームサーバの設計をするために、ゲームサーバとクライアントの間のRPCの実装方法と、ゲームサーバからMySQLの機能を使う方法の2つを調べます。

今回、次回、と地味な調査が続きますが、コードを書く前によく調べておくと、後でほとんど手戻りを無くすことができます。

 

まずゲームサーバとクライアントとの間のRPCをどう実装するか。

「minecraft立体陣取り」では、キャラクターの移動、ブロックを壊す、ブロックを設置する、アイテムを使う、地形をロードする、チャットなどいろいろな処理をサーバに要求することになります。

大規模なMMOGでは要求の種類が何百もあって、整数や文字列だけでなく、座標、配列、構造体、など色々なデータ構造を使って指示をする必要があります。専用の定義言語をつかってRPCを実装するスタブソースコードを自動生成して、通信内容の誤りを未然に防ぎます。

しかし・・・本連載では、JavaScriptの「配列に何でも入れられる」という動的な性質を利用して、RPCについてはガチガチなしくみを作らないことにします。さてこれが吉と出るか凶と出るか?

node.jsの jsonrpcというパッケージのAPIをそのまま参考にして、こんな感じのAPIにしましょう。サーバー(node.js)側でも、クライアント(Unity側)でも、できる限り同じ書き方で定義ができるように。

以下は、受信をする場合の疑似コードです。

function add(a,b) { // リモートから呼ばれる関数

return a + b;

}

var rpc = createRPC();

rpc.exposeFunction( "add", add );  //  add関数を登録、サービス開始。

rpc.listen( 7000 ); // (サーバのみ)要求待ち受け開始。

 

add関数の引数aとbには、JSONで書かれたデータが来ます。文字列、整数、それらの単純配列、連想配列など。

期待していたのとは異なる型の値が入力された場合、例外が発生するかもしれません。その場合は単にエラーを返して、処理をあきらめるようにしましょう。

次は送信側の擬似コードです。とてもRPCとは言えないようなものです。

conn.call( "add", 12, 34 );

conn.call( "add", "aa", "bb" );

connというのは送信相手への接続のことです。この接続に対して関数を呼び出します。一つ目の引数が関数名で、2個目以降が引数です。

Unityでは、関数に対して可変長の引数を与えることができないので、泥臭いですが、 関数オーバーロードを使って、複数バージョンのcall関数を定義する必要がありました。UnityのJavaScriptは、いったんC#に変換している関係上、動的な機能にはかなり制約があります。

 

MMOGのRPCに特有なこととして、「関数呼び出しと返値がほとんど一対一対応にならない」ということがあります。たとえば add関数は「足し算の結果」という返値が明確に必要ですが、MMOGで「キャラが移動した」とサーバからクライアントに伝えるときは、

move(who,x,y,z)

という風にRPCを呼び出します。この関数は、返値が不要なのです。

ほとんどのRPCは返値が不要です。要求もしてないのに、勝手に送られて来る(プッシュ)のです。ようは、RPCが全然、1対1に対応しないのです。

したがって、node.jsで多い「RPC呼び出しの結果が返ってきたときに呼び出される無名関数を登録しておく」というイディオムは、あまり必要ではないです。かえってコードがスパゲティになるかもしれません。

なので、最初からこの機能を組みこまずに進めます。

 

RPCの具体的な実装方法はこんな感じの、ユルい方法でいきましょう。

 

次、

・ゲームサーバからMySQLを利用する方法(ORM)について

MySQLはプレイデータを保存するために必要です。node.js用の sequelizeというパッケージを試したところ、かなり使えそうです。

sequelizeに付いてたサンプルがキレイに動いたので、中身を見てみます:

<pre>var Sequelize = require("./sequelize").Sequelize // sequelizeモジュールを読む</pre>
<pre>var sys = require( "sys") ;</pre>
<pre>var sequelize = new Sequelize('test', 'wise9', '', { // MySQLに接続
  host: "localhost",
  port: 3306
})

var Project = sequelize.define('Project', { // Projectテーブルに対応する型を定義
  title: Sequelize.STRING,
  description: Sequelize.TEXT
})

var Task = sequelize.define('Task', { // Taskテーブルに対応する型を定義
  title: Sequelize.STRING,
  description: Sequelize.TEXT,
  deadline: Sequelize.DATE
})

var project = new Project({  // Project テーブルの1行分のデータを初期化
  title: 'my awesome project',
  description: 'woot woot. this will make me a rich man'
})

var task = new Task({ // Taskテーブルの1行分のデータを初期化
  title: 'specify the project idea',
  description: 'bla',
  deadline: new Date()
})

// テーブルを作成
Task.sync(function(){ sys.puts( "task.sync fin" ); })
Project.sync(function(){     sys.puts( "project.sync fin" );})

// 書き換えして保存
task.title = 'a very different title now'
task.save(function(){
    sys.puts( "task.save fin");
    var t2 = Task.findAll(function(tasks) { // 保存が終わったら、成功コールバックの中でfind
        sys.puts( "task selected:" + tasks[0].title );
    })
})

<span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; line-height: 19px; font-size: 13px;">

sequelizeを使えば、DBアクセスを非同期化できるため、あるプレイヤーのキャラクタデータをMySQLに保存している間の数ミリ秒のCPU時間を、別のプレイヤーのための処理にわりあてることができるようになります。

MMOGでは複雑なゲーム処理をほとんどゲームサーバで実装するため、MySQLの特殊な機能を使う必要がありません。だから sequelizeのような単純で小さなツールで十分です。ちょっと速度が心配ですが、ゲームロジックは全部JavaScriptで実装するので、DBアクセスを高頻度でやること自体が無いので問題ありません。プレイヤー1人あたり数十秒に1回ぐらいの入出力しかないでしょう。

もしかするとORM自体が不要かもしれないですが、SQLを手で書くよりは効率的になりそうなのでこれで進めてみることにします。

 

以上、RPCとORMについて調査を進めました。大きな問題は無さそうです。

次回は、minecraftの通信内容を調査して、クライアントとサーバの間の実際の通信内容をだいたい決めましょう。

(続く)

 

このエントリーをはてなブックマークに追加
はてなブックマーク - JavaScriptでMMOGを作ってみよう その5 RPCとORM
Post to Google Buzz
Share on GREE

Related posts:

  1. JavaScriptで MMOGをつくってみよう その4
  2. JavaScriptで MMOG をつくってみよう その1

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*