Skip to content

JavaScriptでMMOGをつくってみよう その8 マルチプレイ、協力して地面を掘る

今回は、前回実装したJSON-RPCもどきの仕組みを使ってゲームの実装を進めます。 

複数プレイヤーがサーバに接続し、サーバで生成した地形データを読み込み、キャラクターの位置を同期し、他のプレイヤーと一緒に地面を掘る作業ができるようになりました。

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

今回からUnity3Dのクライアント側もgithubにzipで固めて上げてあります。プレイアブルはビルドコマンド一発でできます。

https://github.com/kengonakajima/wise9mmo

 

さて今回作ったところまでのプレイ動画はこれ(720p,全画面でどうぞ):

やっていることは、

  • node main.js としてサーバを立ち上げ
  • Unity3DでWebプレイヤー用ビルド、実行。サーバへ接続
  • Unity3Dエディタ上で実行、サーバへ接続
  • 2人のプレイヤがログインしてる状態をつくって状態同期のテスト
  • プレイヤーキャラクタの位置や向きの同期

Chromeブラウザで動作しているクライアントと、Unity3Dのエディタ上で動くクライアントとが通信しています。

minecraftをお手本にして、立体陣取りMMOGの基本的な仕組みはできました。

まずサーバでの地形生成を見てみましょう。 field.jsではこう:

// 軸ごとのサイズ。 east-west size, north-south size, high-low size
exports.generate = function( hsize, vsize ) {
    var fld = new Field( hsize, vsize );

    fld.fill( 0,0,0, hsize,vsize,hsize, Enums.BlockType.AIR ); // 世界を空気で満たす
    fld.fill( 0,0,0, hsize,1,hsize, Enums.BlockType.STONE ); // 地盤を置く

    fld.fill( 5,1,5, 25,2,15, Enums.BlockType.STONE );   // 高台を置く
    fld.fill( 7,2,7, 22,3,12, Enums.BlockType.WATER );   // その上に水を置く

    return fld;
}

本来はperlinノイズというアルゴリズムを使って、山あり谷ありの地形を構成するのですが、それは後回しにして、いまは3次元の大きな直方体をつくるfill関数を何度か使って地形を生成しています。

ここでつくった地形を getField関数をクライアントから発行して取得しています。
nodeサーバに対してtelnetで接続して手入力でRPC入力するとこういう感じでかえってきます。

{"method":"getField","params":[0,0,0,3,3,3]}   ←送信
{"method":"getFieldResult","params":[0,0,0,3,3,3,[1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0]]}  ←受信

クライアント側でのRPC呼び出しはCommunicatorScript.jsというファイルに書かれています:

//bxyz: block座標
function ensureChunks( bx:int,by:int,bz:int ){
    var chx = bx / CHUNKSZ;
    var chy = by / CHUNKSZ;
    var chz = bz / CHUNKSZ;
    var chrange = VIEWRANGE / CHUNKSZ;

    for(var y= chy-chrange; y <= chy+chrange; y++){
        if(y=CHUNKMAX)continue;
        for(var x= chx-chrange; x <= chx+chrange; x++){
        if(x=CHUNKMAX)continue;            
            for(var z= chz-chrange; z <= chz+chrange; z++){
                if(z=CHUNKMAX)continue;
                var ch = chunks[ toChunkIndex( x,y,z ) ];
                if(ch==null){
                    chunks[ toChunkIndex(x,y,z) ] = new Chunk(CHUNKSZ,x,y,z);
                    send( "getField",
                          x*CHUNKSZ,y*CHUNKSZ,z*CHUNKSZ,
                          (x+1)*CHUNKSZ,(y+1)*CHUNKSZ,(z+1)*CHUNKSZ );
                    return; // 1ループに1かいまで
                }
            }
        }
    }
}

クライアントでは「Chunk」という、4x4x4ボクセルの単位で地形を区切って管理し、Chunkを32x32x32個メモリにポインタの配列として持っています。これを、1回の描画フレームに1回だけスキャンして、ロードしていきます。実際にスキャンする関数がこのensureChunks関数です。
ここでx,y,z座標をそのまま回すのではなくyを一番外側に持ってきているのは、高さ方向を最優先で(ひくいところから)ロードするためです。プレイヤーにとって一番必要なのが地面付近の地形データだからです。

こうしてRPCが発行されたらサーバ側ではこういう関数で地形を取得します:

function getField(x0,y0,z0,x1,y1,z1){
    var ary = fld.getBox( x0,y0,z0,x1,y1,z1);
    if(ary==null){
        this.send( "getFieldResult", x0,y0,z0,x1,y1,z1,[] );
    } else {
        this.send( "getFieldResult", x0,y0,z0,x1,y1,z1,ary );
    }
}

サーバ起動時に生成したマップの必要部分を送っています。クライアントの見える範囲は16ボクセル四方で、その範囲にまだロードしていないChunkが存在するかぎり、これを毎フレーム1個まで送り続けています。

Unity側では、Chunkをロードした瞬間、4x4x4=64個のGameObjectの生成処理が走ります。これが若干重くて、いまは50msぐらいかかります。そのため、ロード中は画面がカクカクしているのが、動画でもわかるとおもいます。
この問題は以前調べたとおりで、プログラムでポリゴンを生成するようにして、GameObjectの生成階数を減らしたら劇的に軽くなります。Chunk1個あたりGameObject1個に将来修正します。また、「外界から見えない位置にあるブロックは表示しない」という工夫でも大幅に軽くなります。

次は、キャラの移動の同期方法です。

キャラの動作を同期するにはいまはmove,jump, digの3つの関数を定義しています。

function move(x,y,z,pitch,yaw,dy,jm,dt){
    var ix = x/1000;
    var iy = y/1000;
    var iz = z/1000;
    this.pos = [ix,iy,iz];

    this.nearcast( "moveNotify",this.clientID, x,y,z,pitch,yaw,dy,jm,dt);
}
function jump(){
    this.nearcast( "jumpNotify", this.clientID);
}

以上はサーバのmain.jsのコードです。

moveは簡単ですね。クライアントから来た座標値をそのまま送るだけです。x,y,z座標だけではなく、pitchやyawも送信して、顔が向いている向きも送信しています。 これだけの情報をリアルタイムに同期するだけで、ただの立方体でも、なんだか愛着を持てる存在になるのが不思議です。

clientIDというのは、各クライアントが接続してきたときに割り振る一意のIDです。login関数を呼ばれたときにクライアントに送り、自分のIDを知ることができます。
nearcastというのは、距離的に近いキャラクターに対してだけ送る処理です。いまは距離は200と大きい値になってます。

キャラの移動をいちばんなめらかに同期する方法は、毎フレーム、位置や向きなどを送ることですが、通信量が無駄なので「落ちてる最中」以外は、0.2秒に1回送信ししてます。落ちてる最中は、50msに1回です。
jumpは、スペースキーを押した瞬間に送信しています。

次は地面を掘ったときの動きですが、

function dig(x,y,z){
    // todo: 無条件に受けいれてる
    var b = fld.get(x,y,z);
    if( b != null && b == modField.Enums.BlockType.STONE ){
        fld.set( x,y,z, modField.Enums.BlockType.AIR);
        this.nearcast( "changeFieldNotify", x,y,z, modField.Enums.BlockType.AIR);
        sys.puts("digged");
    }    
}

これもサーバ側のコードです。
クライアントでは、地面を掘った瞬間にこのRPCを発行します。
サーバ側では現在、無条件に操作を受け入れて、サーバのメモリに載っているFieldのデータを上書きしていますが、ゲームの内容を実装するときは、ここに、持っているアイテムによる制約などを加えていくことになります。

実はクライアントでは、dig関数を送信すると同時に、画面に表示されているブロックを消しています。これは、操作感を良くするためです。

将来、制約が増えて「掘るのに失敗」があり得るようになった場合は、掘ったつもりが、一瞬の後には消えることになるでしょう。

以上、今回は「minecraft立体陣取り」MMOGとしての根幹部分を実装しました。

現時点でクライアントのコードはだいたい800行、サーバは300行と、だいたい予想通りな感じになりました。

特に目立った困難はないのですが、ひとつ苦労するのは、Unity3D上でJavaScriptを用いた開発をする場合には、ソースコードレベルのデバッガが使えないという問題です。C#を使う場合はMonoDevelopが使えるのですが。
3次元のゲームオブジェクトの複雑な変化をソースコードデバッガ無しで追いかけるのはなかなか骨が折れます。将来は追加されると良いですね。

一方で、サーバとクライアントとが(ほぼ)同じ言語であるというのは、精神的なストレスがかなり小さいという感じもしました。

仕様修正の速度はとても早く、半日で500回ぐらいビルドして試せるようです。今のところ、サーバとクライアントの両方をJavaScriptで実装してみるというのは、なかなかいい感じだと思います。

次回からは、この枠組みの上に、ゲームを実装していきましょう。

(続く)

このエントリーをはてなブックマークに追加
はてなブックマーク - JavaScriptでMMOGをつくってみよう その8 マルチプレイ、協力して地面を掘る
Post to Google Buzz
Share on GREE

Related posts:

  1. JavaScriptでMMOGをつくってみよう その7 JSON-RPC風通信

Facebook comments:

Post a Comment

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