Canvas+node.js+socket.ioで簡易オンラインゲーム作ってみた

このエントリーをはてなブックマークに追加
はてなブックマーク - Canvas+node.js+socket.ioで簡易オンラインゲーム作ってみた

Canvas+node.js+socket.ioで簡易オンラインゲーム作ってみました!
ほんとに簡易!


もともとはサークルの交流会のLTデモのためにちょちょっと作ったものなのでゲーム性はほとんどないです。
作成時間は半日くらい。その後の細かな手直しに4時間くらい。

js暦2ヶ月、node.js暦1ヶ月という短さですが、node.jsとsocket.ioの手軽さには本当に感服です!

サンプル

とりあえずブラウザでアクセス
Chrome16、Firefox7で動作確認。表示だけならAndroid2.3でも可。
※Avastなどのウイルス対策ソフトが動いてるとサーバーと接続できないみたいです

2chのプログラミングスレで晒してもなんとか動いてたので、たぶん、普通に動くはずです。

操作方法

矢印キーで移動、スペースキーで加速
物理をすっかり忘れてしまって未知なる力で動かしてるので、加速回りは怪しいかも

CPUもいますが勝手に自殺しちゃうので、
Chromeのタブ+シークレットウィンドウ使うか、Firefox+Chromeでアクセスすれば自分対自分ができます。

開発環境

node.js 0.6.0
socket.io 0.8.7
express 2.5.1

ソース

最近git使い始めたので、なんとなくgithubにもあげてみました。
https://github.com/pxsta/OnlineBallAtack-socketio

仕様

サーバ側でゲームのメインループまわして移動量計算・あたり判定、
クライアント側は定期的にボールの位置情報を受信してCanvasに描写してるだけです。

ユーザーの識別にはSocket.ioのセッションIDを使用。
再接続/リロード時のためにExpressのセッションIDとSocket.ioのセッションIDを紐つけています。
ExpressのセッションIDはサーバのみが保持して、クライアント側には他クライアントのセッションIDは知らせていません。

サーバ(メインループ部のみ)

src/server.js
//メインループ
var run = function()
{
    setInterval(function(){
           update();
           sync();
        },1000.0/MyApp.config.FPS);
};

//ゲームデータの更新
var update = function()
{
    var ballArray = MyApp.ballList.toArray();
    for(var i=0;i<ballArray.length;i++){
        //ここであたり判定とかボールが落ちてないかの確認とか
    }
    
    //各ボールを現在のスピード等に基づいて移動させる
    MyApp.ballList.update(param);
};

//クライアントに同期させる
var sync = function(){
    var ballArray = MyApp.ballList.toArray();
    var sendMessage = {ballList:MyApp.ballList.toJSON()};
    
    for(var i=0;i<ballArray.length;i++){
        //各クライアントにすべてのボールの位置情報を送る
        var socketID = ballArray[i].getSocketID();
        io.sockets.socket(socketID).volatile.emit("updateBallInfo",JSON.stringify(sendMessage));
    }
}

サーバ側で、updateとdrawを定期的に実行というありがちなメインループをまわす。
今回はdrawをsyncに変えて、描写の代わりにクライアントにゲームの状態を送信。

クライアント

src/client/client.js
//サーバからボールの情報を受信したときのイベントハンドラ
connection.on('updateBallInfo', function (msg) {
    var messageJson = JSON.parse(msg);
    
    //ボールの状態を更新する
    MyApp.ballList.get(messageJson.ballList[i].socketID).updateValue(messageJson.ballList[i]);    
});

//メインループ
var run = function()
{    
    MyApp.mainLoopID=setInterval(function()
    {
        update();
        draw();
    },1000.0/MyApp.config.FPS);
};

var update = function(){
    //落ちたボールをリストから削除したり
    //自分に落ちてるフラグがセットされてたら画面メインループ停止させたり
    //サーバから受信した各ボールの情報をもとにゲームオーバー判定とかするだけ。
};

//canvasに描写する
var draw = function()
{
    var ctx = MyApp.context;
    var offset = MyApp.getOffset();
    
    //Canvasをゼロクリアする
    ctx.clearRect(0,0,MyApp.canvasSize.width,MyApp.canvasSize.height);


    //マップを描写
    MyApp.map.draw(ctx,MyApp.getOffset());
    
    // ボールを描写
    MyApp.ballList.draw(ctx,MyApp.getOffset());
     
};
“updateBallInfo”としてサーバから定期的に送られてくるボール情報を受信して、ローカルに各ボールの状態を更新・保存、
その状態を元にボールを描写。

ボールの移動

src/client/client.js
$(window).keydown(function(e){
    //サーバにキー押下情報を送る
    connection.emit("keydown",code);
}
ボールの移動は、
クライアント側はキーイベントを監視して押されたボタンの情報をサーバに送るのみ。

src/server.js

//クライアントがキーを押した時
connection.on('keydown',function(code){
    var moveVector={x:0,y:0,mode:"normal"};
    if(37<=code&&code<=40||code==32){
        if(code==37){
            moveVector.x+=(-1);
        }
        else if(code==39){
            moveVector.x+=1;
        }
        else if(code==38){
            moveVector.y+=(-1);
        }
        else if(code==40){
            moveVector.y+=1;
        }
        else if(code==32){
            //ターボ
            moveVector.mode="tarbo";
        }
    }
    //ボールの行動バッファに加える
    MyApp.ballList.get(connection.id).setActionBuffer(function(){
        MyApp.ballList.get(connection.id).addSpeed(moveVector);
        MyApp.ballList.get(connection.id).setMode(moveVector.mode);
    });
});
サーバ側は受信したキー押下情報を元にスピードの上げ下げ。
バッファに1つだけ保存して、次のupdate内で実行。

クライアントからkeydownメッセージ受信した時点でスピード更新してもいい気がしたけど、
あるクライアントはメッセージ送信が1ms/回、あるクライアントは500ms/回とかだとまずい気がしたのでバッファに1つだけ保存して、updateで定期的に実行するようにしました。

とりあえず

updateとdrawの定期実行というよくあるメインループをサーバ側でやらせて、
drawをsyncに変えて描写の変わりにクライアントにゲーム状態を送るようにして、
クライアントではupdateの代わりにイベントハンドラでゲームの状態を受信して、それをdrawで定期的に描写してるだけです。

チート的なことをされないために、あたり判定などのメインループのコードはサーバ側だけに書いてます。
ボール移動の際も、クライアントからキーコードを送ってもらうだけで、具体的な処理はサーバ側で行っています。

ballAtack.jsでボールの基本的なクラスを定義して、serverBallAtack.jsでballAtack.jsの各クラスを継承して、ExpressのセッションID・ボール移動処理などのサーバのみに必要な値・メソッドを追加しています。

問題点

  • サーバは60FPSでゲームの状態を送信してますが、それに通信速度が追いつかないと問答無用でカクカクします。
  • 通信途切れたら動けません(再接続はできます)
  • クライアントからsocket.emit(“hoge”,”あばばばば・・・(1MB分続く・・・)”)みたいなの実行されて巨大なデータ送られ続けたらどうなるんだろう?
これでも読めば何か変わるんでしょうかね。


敷居の低さ

そもそもこれを作ったのは大学のサークル同士の交流会のLTのためなのですが、

前日になって、
スライドできてない><→node.js+socket.ioのことでも話すか!→デモでも作って穴埋めしよう→これならサクッと作れそう!
という単純な動機で作りました。

js暦2ヶ月、node.js暦1ヶ月ちょいの自分ですが、それでも半日程度で作ることができました。
そもそも、通信部分はサーバ/クライアント合わせてわずか100行ちょっとで、その他は普通のゲームと同じです。

ゲームに限らず、webサービスでリアルタイムなコンテンツをサクッと追加するのにも使えそうです!
何かおもしろいものないかなあ。

このエントリーをはてなブックマークに追加
はてなブックマーク - Canvas+node.js+socket.ioで簡易オンラインゲーム作ってみた

Dansette