やあshi3zだ。
さっきはあまりに簡単にチャットを書くことができたので、感動のあまりすぐ公開しちゃったけど、
node.jsとenchant.jsを使って簡単なネットゲームのプロトタイプを作ってみたぞ。
まだ荒削りだけど、ネットゲームのプロトタイプとしてはこんなもんだろう。
さて、どんな構造になっているか解説しよう
このゲームは週刊アスキーの連載「私立9leap高校ゲーム開発部」で掲載した「育ててポン!」というゲームをもとにして作っている。
これはドロイドちゃんを操作して、木を育て、最後にフルーツを収穫する、というタイムアタックのアクションゲームだ。
今回はこれをネットゲーム化してみる。
サンプルコードは以下のURLからダウンロード可能だ。
http://junk.wise9.jp/sodapon.zip
まずはサーバーだ。
前回のチャットではサーバはサンプルそのままだったけど、今回はサーバも少し違う。
サーバの中身はapp.jsというコードに入っている。
今回変更した部分だけを抜き出して以下に掲載する
app.js
io.sockets.on('connection', function (socket) {
id++; // IDをインクリメント
log('connected'+id);
socket.emit('msg setID', id); // ログインしてきたユーザにサーバ側でIDを割り振る
socket.on('msg send', function (msg) { // 他のプレイヤーの位置を交換
socket.emit('msg push', msg);
socket.broadcast.emit('msg push', msg);
});
socket.on('msg tree', function (msg) { // 木が増えたときに呼ばれるイベント
socket.emit('msg tree', msg); // 5
socket.broadcast.emit('msg tree', msg); //木が増えたことを他のプレイヤーに通知
});
socket.on('disconnect', function() { // 切断
log('disconnected');
});
});
サーバとクライアントのやりとりする決まり事をプロトコルと呼ぶ。
socket.ioの面白いところは、プロトコルをjavascriptの関数としてガリガリ書ける事だ。
「育ててポン!」本体のソースコードは長いので、ちょっと関係のありそうなところだけ抜粋する。
main.js
players=[null,null,null,null,null,null,null,null,null,null,null,null,null,null]; //プレイヤー配列
socket = io.connect('http://localhost:3000'); // サーバに接続
socket.on('connect', function() { //
log('connected');
socket.on('msg push', function (msg) { // 他のプレイヤーから位置が通知される
if(msg.id==gameID)return; //自分自身の場合は反応しない
if(!players[msg.id]){
players[msg.id]=new Player(); //新しくPlayerオブジェクトを生成する
players[msg.id].addEventListener('enterframe',players[msg.id].netMove); //Playerオブジェクトの動作をnetMoveメソッドにする
}
players[msg.id].tx=msg.x; //特定のプレイヤーの目的座標を(tx,ty)とする
players[msg.id].ty=msg.y;
});
socket.on('msg setID', function (msg) { // ログイン時にサーバから呼ばれるゲームIDの通知プロトコル
log("GameID:"+msg); // 8
gameID = msg; //ゲームIDが決定される
});
socket.on('msg tree', function (msg) { // 誰かが木を新しく生成した
t = new Tree(false); //新しい木を作る
t.x = msg.x; //木の位置を設定
t.y = msg.y;
});
});
sockei.ioでは、JSONで通信するため、任意のJavaScriptオブジェクトをそのまま他のクライアントに渡す事が出来る。
これが非常に便利なんだよね。
このゲームの場合、木はそれぞれのクライアントがランダムに生成して、「うちで(x,y)に木を生成したよ」というメッセージを他のクライアントに投げると他のクライアントでも木が生えてくるというワケ。
実際にどんなふうに「木を生成したよ」というメッセージを投げているかというと
initialize:function(origin){
enchant.Sprite.call(this,16,16); //Spriteクラスのコンストラクタ呼び出し
this.image = game.assets['tree.gif'];
game.rootScene.addChild(this);
this.reset();
if(origin) //このクライアントで木を生成した場合
socket.emit('msg tree', {x:this.x,y:this.y}); // 他のクライアントに通知
this.addEventListener('enterframe',this.move);
},
イニシャライザがこのようになっていて、各クライアントごとに100フレームに一回木を生成する
if(game.frame%5==0){ //5フレームごと
socket.emit('msg send', {id:gameID,x:player.x,y:player.y}); // 現在位置をサーバに送信
}
if(game.frame%100==0){ //100フレームごと
var t = new Tree(true); //新しい木を生成
}
});
毎フレームサーバーに送信するとサーバーの通信を圧迫して輻輳(ふくそう)が発生する。
通信パケットが渋滞するわけだ。
そこで5フレームに一回だけ現在位置を送信することとし、それぞれのクライアントでは5フレームごとに受け取った位置を補完しながら表示する。
vx = this.tx - this.x; //目標地点と現在位置の差異(vx,vy)
vy = this.ty - this.y;
len = Math.sqrt(Math.abs(vx*vx+vy*vy)); //距離(ピタゴラスの定理)
if(len>2){
vx = 3*vx/len; //距離を割って3を掛ける
vy = 3*vy/len;
}
this.x += vx;
this.y += vy;
//ドロイドちゃんの方向を変える
if(Math.abs(vx)>Math.abs(vy)){
if(vx>0)this.frame=3;
else this.frame=1;
}else{
if(vy<0)this.frame=2;
else this.frame=0;
}
}
各ドロイドちゃんはこんな感じで位置を補完する。
目標とする位置から現在位置を引いて方位ベクトル(vx,vy)を出し、それを長さ(len)で割る事で正規化して長さ1のベクトルを作り、そこに3を乗じている。
これはどういうことかというと、他のクライアントは常に5フレーム遅れて座標を受け取るが、さらに数フレーム遅れて動くということになる。
なぜこんな処理を入れてわざわざ遅くするのかというと、人間というのは一見して滑らかに見えるほうが、正確に動くよりもより自然であると認識するからだ。
このゲームは木を育てて実がついた状態で触ると、パーンとフルーツが弾けて出てくるが、この部分はランダムであり同期しないようになっている。
つまり、プレイヤーごとに見えている世界には食い違いが出来るようになっている。
これはインターネットという遅延の激しい環境でゲームを実現するために必要なある種の「割り切り」だ。
こうして、Aというプレイヤーが見ている世界とBというプレイヤーが見ている世界は微妙に食い違うことになる。
しかし、ゲーム全体を通してみると、この食い違いはほぼ許容される、というのがこの非同期的な通信ゲームの落としどころだ。
もちろん、LAN環境やゲームセンターなどであれば、通信速度や遅延(レイテンシ)の問題はかなり解決されるため、完全に画面を同期させる事も可能だ。
ただし、インターネット経由で遊ばせるにはかなり難しくなる。
ネットゲーム開発の面白さは、この「食い違い」をいかに違和感なく見せるか、ということだ。
「どの部分は厳密に同期が必要」で、「どの部分はどうでもいい」のか、という切り分けが、ネットゲームの面白さを決定してしまう。
このことはプラットフォームがプレステになろうがxboxになろうが本質的には同じだ。
今回、enchant.jsとnode.jsの組み合わせでこのプロトタイプを作るまでの時間はわずか1時間。
JavaScriptがいかに生産性の高い言語なのか、改めて驚かされる。
Related posts: