開発合宿レポート 2015 秋 第1回 MMOゲームを作る

10月初旬にピクセルグリッドの開発合宿が、参加者8名で行われました。成果物の評価は「技術」「おもしろ」「役立ち」の軸で行われ、小山田のMMOゲームの基本実装は「技術」軸でトップ評価でした。

発行

著者 小山田 晃浩 フロントエンド・エンジニア
開発合宿レポート 2015 秋 シリーズの記事一覧

はじめに

10月初旬にピクセルグリッドの開発合宿*が、参加者8名で行われました。

*注:ピクセルグリッドの開発合宿

開発合宿の主旨などに関しては、次の記事を参照してください。

3日間の開発が終わったあと、それぞれ開発の成果物を「技術」「おもしろ」「役立ち」という3つの観点から評価、採点しました。採点は、自分以外のすべての参加者の成果物について、それぞれの軸について1〜3位の人物を投票します。

小山田が解説する成果物は、投票の結果「技術」の軸で、もっとも高い評価を受けたものです。

MMOをWebでも

みなさんはMMOゲームあるいはMOゲームをプレイしたことはありますか? MMOゲームとは、Massively Multiplayer Online Game、つまり、多人数同時接続型のゲームを意味します。ネトゲとも呼ばれていますね。私はMMOゲームをとても楽しくプレイしています。そして、ネット上の同じ空間で同じ時間を過ごす、MMOゲームの体験をWebにもってきたいといつも考えています。さいわい、現在のWebブラウザにはWebGLもWebSocketも備わっており、MMOの体験をJavaScriptで実現することができます。

WebGLを使う

WebGLはみなさんもご存知の通り、2Dまたは3DのグラフィックのローレベルAPIです。WebGLがあることで、Webブラウザ上で3Dのリアルタイムレンダリングを実現できます。ゲームをより楽しく魅力的にするためには、マップのレンダリングやキャラクターのアニメーションが必要となります。

WebGLをよりわかりやすく利用するためのJavaScriptライブラリであるthree.jsには高度なアニメーションのシステムが備わっており、走る、ジャンプなど、複数のアクションをうまく切り替えたり、ミックスすることができます。

また、私はこれまでに、地面を歩くためのJSライブラリを作成していますので、これを利用することで、3Dの世界を歩き回ることができます。

Socket を使う

ここからが今回の合宿で実現した仕組みです。前述したとおり、私はこれまでにシングルプレイヤーが3Dの世界を歩き回る仕組みは作っています。ではマルチプレイヤーを実現するにはどうすればいいのでしょうか?

まずは、実際のMMOゲームについて考えてみましょう。多くのMMOはクライアントとサーバーでそれぞれゲームを動かしています。それぞれのクライアントはサーバーに操作状況や現在位置を送信します。サーバーはその操作状況に不正がないかを確かめるためにサーバー側でもゲームを動かし、クライアントから送られてきた操作や現在位置などに矛盾がないか確認します。そして問題がなければ、すべてのクライアントに、全プレイヤーの現在位置や状態(歩いているか、ジャンプしているかなど)を送り返します。

現在のWebブラウザではWebSocketを使うことで似た仕組みを実現できます。今回はサーバー側ではゲームを動かしていないために、やり方次第ではチート行為(クライアント側での不正操作)ができてしまいますが、MMOの体験がJavaScriptを通してWebでも技術的に可能ということがわかりました。

以下はNode.js上で動いているJavaScriptのコードの一部です。利用したのは、http*とSocket.IO*の2つの技術だけです! 定期的にすべてのプレイヤー情報がクライアントに送られてくることによって、同時接続を実現しています。

*注:http、Socket.IOとは

Node.jsにおけるhttpとSocket.IOに関しては、以下の記事なども参照してください。

var server = require( 'http' ).createServer();
var io     = require( 'socket.io' )( server );
var port   = process.env.PORT| 3004;

// 省略

// すべてのプレイヤーリスト格納場所
var players = {};

// クライアントがサーバーに接続したらaddnewplayerが発生、サーバー上で以下が実行される
socket.on( 'addnewplayer', function ( arraivalAvatarData ) {

  // 自分だけに「既に接続している他のプレイヤーリスト(`players`)」を送信
  io.to( socket.id ).emit( 'myid', {
    myID : socket.id,
    players : players
  } );

  // 「接続しているプレイヤーリスト(`players`)」に自分を追加
  players[ socket.id ] = {
    chatID       : arraivalAvatarData.chatID,
    id           : socket.id,
    name         : arraivalAvatarData.name,
    type         : arraivalAvatarData.type,
    position     : arraivalAvatarData.position,
    velocity     : arraivalAvatarData.velocity,
    direction    : arraivalAvatarData.direction,
    isGrounded   : arraivalAvatarData.isGrounded,
    isIdling     : arraivalAvatarData.isIdling,
    isJumping    : arraivalAvatarData.isJumping,
    isOnSlope    : arraivalAvatarData.isOnSlope,
    isRunning    : arraivalAvatarData.isRunning,
    jumpStartTime: arraivalAvatarData.jumpStartTime,
    inputTimeout : arraivalAvatarData.inputTimeout,
    lastupdate   : Date.now()
  };

  // 送信元を含まない、既に接続済みのプレイヤーに通知
  socket.broadcast.emit( 'addnewplayer', players[ socket.id ] );

  // 省略

} );

// 100ミリ秒ごとに「接続しているプレイヤーリスト(`players`)」をすべてのクライアントに通知
function run () {

  // 省略

  setTimeout( run, 100 );
  io.sockets.emit( 'sync', players );

};

上記コードを見てわかる通り、100ミリ秒に一度の頻度で全プレイヤーの位置情報の同期が行われています。つまり、99ミリ秒間の位置情報は空白になってしまいます。どうすればこの空白を埋めることができるのでしょうか?

この問題を解決するために、同期の際にそれぞれのプレイヤーがどの方角を向いているか、そして歩いているか止まっているかを合わせて取得しています。この情報の取得によって、自分以外のプレイヤーは100ミリ秒後(次の同期)まで、同期情報なしで向いている方向に歩き続けることができます。これを繰り返すことで、ある程度のレイテンシー(遅延)があっても多くのプレイヤーが自然な移動をしている状態が実現できるわけです。

次のデモは、Google Chrome(左)とFirefox(右)からの接続です。

もしあなたがMMOゲームをプレイしたことがあるのなら、"ワープ"しているプレイヤーを見たことがあるかもしれません。これはサーバーの同期のタイミングにうまく合っていない、大きな遅延をしているユーザーがいるためなのです。

2つのソケットサーバー

今回の合宿で行った実験では、3D空間内の現在位置共有に加え、チャットも実装しました。Socket.IOを利用したチャットの実装はとても基本的な技術ですので、試したことがある人も多いのではないでしょうか。

先ほどの位置情報の共有もSocket.IOを利用していましたが、位置情報共有とチャットとはまったく異なる機能です。ですのでサーバーを分け、位置情報専用のSocketサーバー、チャット専用のSocketサーバーという2サーバー構成にしました。

もしあなたがMMOゲームをプレイしたことがあるのでしたら、マップは移動できるけれど、チャット機能だけ利用できない、なんていう経験もあるのではないでしょうか。機能ごとにサーバーを分けている場合に、一部のサーバーがダウンしまうとこの状態になるわけですね。

WebサーバーでSocket.IOを動かす

フロントエンド実装をしている多くの方はgulpなどを通して普段からNode.jsに触れているでしょう。ただしそれはローカルのサーバーでNode.jsを動かしているにすぎず、WebアプリケーションにNode.jsを取り入れる場合には、Webサーバー上でNode.jsを動かす必要があります。Node.jsに対応していると同時に、WebSocketにも対応しているサーバーとなるとかなり数が限られます。今回私はWindows Azureを利用しました。

というのも、最初の30日間は無料で、その後、従量課金に移行しても重たい処理をさせなければほぼ0円で利用できるためです*。

*注:Azureの課金

Windows Azureは、サーバのCPUやメモリの使用量が一定の値を超えた場合に課金されます。また、悪意ある多重アクセスによる高負荷での意図しない課金を防ぐために月間使用量の上限を設定することができます。

Azureの導入はかなり簡単でした。Azureのアカウントを作成、その後ログインし、画面左下の「New」ボタンからWebアプリケーションを作成します。GitHubと連携させた後、任意のリポジトリーとブランチを指定します。これにより、Azureは指定したブランチへのpushを監視し、pushが行われると同時に、自動でデプロイを行います。

ログインした直後のダッシュボード。すでにプロジェクトを作っていればそれが列挙される。

「New」ボタンを開いた状態。ここからWebアプリケーションのプロジェクトを新規作成する。

新規Webアプリケーションの名前を付ける。

GitHubのリポジトリをはじめとする、さまざまなサービスと連携することができる。今回はGitHubを選択した。

任意のリポジトリとブランチを選択。

pushすればそれが自動でデプロイされる。

Socket.IOを含む、WebSocketを利用する場合には「CONFIGURE」内の「WEB SOCKETS」の設定をONに切り替えた後Saveし、サーバーを再起動する必要があります。この設定変更はコマンドラインを利用しなくても、すべてAzureのWeb上の管理画面から行うことができます。

まとめ

今回は3日間という中で、MOアクションゲームのベースを作成することができました。マルチプレイヤーの同時接続は、これまでのWeb、つまり一方通行の通信(http)の文書(Webページ)にはなかった、時間と空間の共有という体験を可能にします。そしてこれをWebブラウザで動かすことができるのです。

今回は3D表示も、サーバーサイド(Node.js)ですらもJavaScriptで実現することができました。JavaScriptはネイティブ言語と比べると速度は出ません。しかしながらパフォーマンスを調整すれば、ポータブルゲーム程度の性能は引き出すことができるでしょう。今回の結果で私はJavaScriptがどれだけ柔軟な言語かを改めて確かめることができました。

もう一つ、いつも思うことは、「ゲームのバグはおもしろい」ということです。バグからは、その裏にどのような実装があるのかを知るきっかけになります。ラグによるワープは、サーバーとクライアント間の通信がリアルタイムではないことを示していますし、欠けたマップは、マップがGoogle Mapsと似た仕組みのタイルで構成されていることを示しています。

ファンタジーアース ゼロ © 2009-2015 SQUARE ENIX CO., LTD. All Rights Reserved.

もしあなたがエンジニアかつゲームが好き、という場合には、ゲームのバグを注意深く観察してみると、新たなゲームの楽しみ方が見つかるかもしれませんね。