このページの本文へ

WebSocket&Canvas パフォーマンス最適化のコツ (2/4)

2011年07月21日 13時00分更新

文●ハン☆スタ制作委員会/マインドフリー株式会社

  • この記事をはてなブックマークに追加
本文印刷

WebSocketによるプッシュ通信の最適化

 Handy StadiumではWebSocketで通信しているので、ボールの位置座標などをフレームごとにプッシュして同期すれば、2台のiPhone間でゲーム画面の状態をまったく同じにできるはずです。Handy Stadiumの当初の実装では、投手のiPhoneの傾きとボールの位置を1フレームごとに送信し、2台のiPhone間でボールの位置を完全に同期しようと試みました。ボールを投げた後も、投手側のiPhoneの傾きによってボールの曲がり方やスピードをリアルタイムで変えられるようにしたかったのです。

 初期のJavaScriptのコードは以下のようになります。


(function(){
  var ws = new WebSocket('ws://' + location.hostname + ':8000/');
  var _isOffence = false;
  var _ball = {x:0,y:0,v:3,radian:Math.PI/2,ax:0,ay:0};
  var _currentAcc = {x:0,y:0};
    
  //iPhoneの傾き取得
  window.addEventListener('devicemotion',function(e){
    //このアプリはiPhoneを横向きで操作するので、それに合わせてxとyを入れ替える。
    _currentAcc.x = e.accelerationIncludingGravity.y / 20;
    _currentAcc.y = -e.accelerationIncludingGravity.x / 20;
  });
  
  //メッセージが届いたときの処理
  ws.addEventListener('message',function(e){
    var data = JSON.parse(e.data);
    var type = data.type;
    if(type == 'game.change'){
      //チェンジしたときにサーバから自分がオフェンスかどうかのデータを受け取っている
      _isOffence = data.yourTurn == 'offense';
    }else if(type == 'game.ballState'){
      //ディフェンスから受け取ったボールの情報から位置を補正する
      if(_isOffence){
        _ball.x = data.x;
        _ball.y = data.y;
        _ball.v = data.v;
        _ball.radian = data.radian;
        _ball.ax = data.ax;
        _ball.ay = data.ay;
      }
    }else if(type == 'game.throw'){
      //ボールが投げられたときに初期化する
      _ball.x = 240;
      _ball.y = 160;
      _ball.v = 3;
      _ball.radian = Math.PI / 2;
      _ball.ax = 0;
      _ball.ay = 0;
    }
  });
  
  //画面をタップしたときにボールを投げる
  docuemnt.getElementById('eventLayer')
  .addEventListener('touchstart',function(e){
    if(!_isOffense){
      _ws.send(JSON.stringify({type:'game.throw'}));
    }
  });
  //1フレームごとの処理
  (function(){
    if(!_isOffense){
      _ball.ax = _currentAcc.x;
      _ball.ay = _currentAcc.y;
    }
    var vx = Math.cos(_ball.radian) * _ball.v + _ball.ax;
    var vy = Math.sin(_ball.radian) * _ball.v + _ball.ay;
    _ball.v = Math.sqrt(vx*vx+vy*vy);
    _ball.radian = Math.atan2(vy,vx);
    
    _ball.x = _ball.x + vx;
    _ball.y = _ball.y + vy;
    if(!_isOffence){
      //ディフェンスの場合に1フレームごとにボールの状態を送信する(ここがボトルネック!)
      ws.send(JSON.stringify({type:'game.ballState',x:_ball.x,y:_ball.y,v:_ball.v,radian:_ball.radian,ax:_ball.ax,ay:_ball.ay}));
    }
    setTimeout(arguments.callee,34);
  })();
})();


 ところが、実際にiPhoneで試してみると、WebSocketのメッセージ送信が期待どおりに動きませんでした。iOSのモバイルSafariでは、1フレーム(約34ms)ごとにws.send()メソッドを呼び出すと、1フレームごとではなく、3フレームごとに3つのメッセージが同時に送信されてしまいます。iOSのモバイルSafariでは、処理の負荷が高いと、ws.send()メソッドを呼び出した瞬間には送信されず、一定のタイミングでまとめて送信されるようです。

 これでは情報が飛び飛びにやり取りされてしまい、ボールがあたかもワープしたように動いて見えてしまいます。リアルタイム通信のためのWebSocketなのにリアルタイムで送信されないのはよく分からない挙動ですが、この制限下で完全にリアルタイムで同期させるのは非常に難しいので、

  1. ボールを投げた
  2. バットを振った
  3. ボールがバットに触れた

 など、特定のアクションが起きたときだけプッシュ通信することにしました。改良後のプログラムでは、ボールを投げたときにiPhoneの傾き情報も一緒に送信しています。ボールを投げた後は同期処理をしないので、ボールは非常にスムーズに動くようになりました。

 改良後のソースコードは以下になります。


(function(){
  var ws = new WebSocket('ws://' + location.hostname + ':8000/');
  var _isOffence = false;
  var _ball = {x:0,y:0,v:3,radian:Math.PI/2,ax:0,ay:0};
  var _currentAcc = {x:0,y:0};
    
  window.addEventListener('devicemotion',function(e){
    _currentAcc.x = e.accelerationIncludingGravity.y / 20;
    _currentAcc.y = -e.accelerationIncludingGravity.x / 20;
  });
  
  ws.addEventListener('message',function(e){
    var data = JSON.parse(e.data);
    var type = data.type;
    if(type == 'game.change'){
      _isOffence = data.yourTurn == 'offense';
    }else if(type == 'game.ballState'){
      _ball.x = data.x;
      _ball.y = data.y;
      _ball.v = data.v;
      _ball.radian = data.radian;
      _ball.ax = data.ax;
      _ball.ay = data.ay;
    }else if(type == 'game.throw'){
       //ボールが投げられたときに初期化する
       //その際傾きの情報も送られてくるのでその値でax,ayを初期化。
      _ball.x = 240;
      _ball.y = 160;
      _ball.v = 3;
      _ball.radian = Math.PI / 2;
      _ball.ax = data.ax;
      _ball.ay = data.ay;
    }
  });
  
  docuemnt.getElementById('eventLayer')
  .addEventListener('touchstart',function(e){
    if(!_isOffense){
      //game.throwイベントを送信する。その際に、傾きの情報も送信データに含める。
      _ws.send(JSON.stringify({type:'game.throw',ax:_currentAcc.x,ay:_currentAcc.y}));
    }
  })
  (function(){
    var vx = Math.cos(_ball.radian) * _ball.v + _ball.ax;
    var vy = Math.sin(_ball.radian) * _ball.v + _ball.ay;
    _ball.v = Math.sqrt(vx*vx+vy*vy);
    _ball.radian = Math.atan2(vy,vx);
    
    _ball.x = _ball.x + vx;
    _ball.y = _ball.y + vy;
    
    //1フレームごとに同期しないので、ws.send()は呼び出さない
    setTimeout(arguments.callee,34);
  })();
})();


 このほかの改良できそうな点として、WebSocketで送信するメッセージの形式があります。現在のHandy StadiumではJSON形式を使用していますが、JSONのパース処理に多少時間がかかっているかもしれません。パース処理に時間がかからず、メッセージのサイズを小さくなるようなフォーマットで通信できればさらに軽くなるでしょう。まだまだ改良の余地はありそうです。

この連載の記事

一覧へ
Web Professionalトップページバナー

この記事の編集者は以下の記事をオススメしています

ASCII.jp会員サービス 週刊Web Professional登録

Webディレクター江口明日香が行く