マルチプレイ協力連打ゲームを作った

オンラインイベントの演出の一環としてプレイする「マルチプレイ協力連打ゲーム」を制作しました。備忘録として使用した技術やサービス、思考の記録をまとめておきます。


概要

プレイヤーは各々のデバイスで2Dの連打ゲームをプレイします。全員の連打回数の合計をリアルタイムに同期して各自のデバイスに表示させます。

要件
・プレイ人数は5人程度
・プレイヤーは自宅から参加
・デバイスはPCまたはタブレット
・ブラウザでプレイ
・期間限定でプレイできれば良い(短期イベントの演出としてプレイするゲームなので定常的なアクセスがあるわけではない)

PlayCanvasというゲームエンジンとPhotonというリアルタイム通信エンジンを使って制作しました。
PlayCanvasはJavaScriptで記述されています。PhotonのJavaScript SDKというツールがあるのでそれを使ってリアルタイム通信します。

制作期間:2週間
制作人数:1人
私自身ゲーム開発自体がほぼ始めて(大学の授業で少しだけやったくらい)で、右も左もわからない状態から始めました。

使用サービス

PlayCanvas

PlayCanvasは、JavaScriptで記述されたWebGLエンジンであり、OSSのエンジンとSaaS型のエディターを兼ね備えているゲームエンジンです。このエンジンを使用することで、Three.jsやBabylon.jsなどの他のエンジンと同様の方法で3Dゲームを作成できます。また、ビジュアルエディターとコードエディターを備えたエディターがあり、クラウド上でプロジェクトを作成し、公開することができます。エディターは、ドラッグアンドドロップによるGUIからの素材の操作や配置が可能で、FBXやOBJなどの3Dモデル素材を簡単に扱えます。

Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信するプロジェクトを作る #JavaScript - Qiita

【なぜPlayCanvasを選んだのか】
制作期間2週間でゲーム開発未経験ということもあり、初めからゲームエンジンを使用しての開発を検討していました。

ゲームエンジンを選ぶにあたって以下の制約がありました。
・ブラウザで動作すること
・容量が小さいこと

ゲーム開発といえばUnityかなと考えながら(というかそれしか知らなかった)色々調べたところ、PlayCanvasの存在を知りました。
そして以下の理由からPlayCanvasで開発することを決めました。
・Unityと比較して軽量なブラウザゲームを開発できる
・操作感はUnityと変わらないか少し劣るくらい
・ブラウザ上で開発が可能(外ではノートPC、家ではデスクトップという環境だったので、デバイスごとに開発環境をセットアップする必要がないというのは便利でした)
こちらの投稿で紹介されている方法を使えばマルチプレイの見通しが立つ

Photon

Photon製品を使用すると、世界中であらゆるプラットフォーム向けのマルチプレイヤーゲームを構築し発表することができます。 プレイヤー数のスケーリングについては、1人めのユーザーから数百人まで、需要に合うように当社が行いますので、ゲームの開発に集中していただけます。

Photon Engine

Unityで広く使われているようなので信頼性も高く、また同時接続が20人までは無料とのことなの今回の開発に適していると判断しました。

このあたりの投稿を参考にしてPlayCanvasとPhotonの準備、同期の仕方を学びました。
Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信するプロジェクトを作る #JavaScript - Qiita
【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】 #JavaScript - Qiita

ゲームの流れ

主なゲームの流れは以下のようになります。

  1. スタート画面

  2. GOボタンをクリックでゲーム画面に遷移

  3. スペースキーを押すと321でカウントダウンしてゲームスタート

  4. 連打ゲーム
    プレイヤーがスペースキーを合計300回連打する
     ・全員の押した回数を合計して常に同期
     ・完了のパーセンテージが表示される
     ・押された回数にしたがって手の模様がだんだんと表示される
      イメージはiPhoneの指紋認証登録

  5. 100%になるとイベントのストーリームービーが流れる

連打ゲームで表示される手の模様のイメージ

実装

1~3の画面遷移やボタンクリック、タッチ入力・キーボード入力、カウント表示などは、PlayCanvas公式のチュートリアルにあるゲームFlappy Birdを参考に制作しました。
(余談…Flappy Bird、流行っていた頃はまだ小学生か中学生だったので存在も忘れていたんですが、見た途端に家族と一緒にやっていた当時の記憶が蘇って懐かしい気持ちになりました。めっちゃめちゃハマってました…)

5のストーリームービーの再生は以下の記事を参考に作りました。
【JavaScript】PlayCanvasを使ってモバイル・デスクトップで動く3D空間上で動画が見れるゲーム作る【WebGL】 #JavaScript - Qiita

連打回数同期

今回の開発の肝である4の連打回数同期の部分です。
プレイヤーA、Bがこのゲームをプレイしているとすると、大まかな流れは以下のようになります。

  1. プレイヤーAがスペースキーを離したタイミングでスコアが+1され他プレイヤーにスコアが送られる。Aはスコアを画面に表示する。

  2. プレイヤーBはAから受け取ったスコアを自身の持つ変数scoreに反映させてこのスコアを画面に表示する。

【実際のコードの一部】

// --- SPACEキーを離したときに同期する ---
this.app.keyboard.on(pc.EVENT_KEYUP, function(e) { // キーを離したときに実行
    e.event.preventDefault();
    else if(this.syncstate === 'play') {
        if(e.key === pc.KEY_SPACE) {
            this.app.fire('game:addscore');
            const syncscore = {
                s: this.score
            }
            this.raiseEvent(EVENT_LIST.Gameaddscore, Object.assign({},syncscore));
            if (this.percent > 99) {
                this.app.fire('game:rendafinish');
                this.syncstate = 'clear';
            }
        }
    }
}.bind(this), this);

// --- スコアを送信する側の処理「game:addscore」 ---
this.app.on('game:addscore', function() {
    this.score++;
    this.percent = Math.round((this.score/GOAL)*100);
    this.app.fire('ui:score', this.percent);
}.bind(this), this);

// --- スコアを受信する側の処理「sync:addscore」 ---
this.app.on('sync:addscore', function(content) {
    if(content.s > this.score){
        this.score = content.s;
    }
    else{
        this.score++;
    }
    this.percent = Math.round((this.score/GOAL)*100);
    this.app.fire('ui:score', this.percent);
}.bind(this), this);

ネット遅延や連打による通信過多で受信とカウントアップの順序が前後し、画面に表示されているパーセンテージが減少してしまうことがありました。(恐らく下図のようなことが起こってる?)
そのため、sync:addscoreの中でcontent.s > this.scoreと分岐させました。送られてきたスコアが現在持っているスコアよりも小さかった場合は受信側のスコアを+1します。

通信が遅れた場合

改善点

・スコア同期の仕組み
上の図を作っているときにcontent.s > this.scoreの場合もthis.score++でカウントアップすればもっとシンプルで良かったかも?と思いました。
ただ、この場合「スペースキーを押した」という情報だけをプレイヤー間で通信することになり、スコア自体は通信に乗りません。
もし誰かの通信が途中で切断されてしまうと、プレイヤーの見ているスコアがそれぞれ違うということになりかねません。
その上、イベント内でのこのゲームの目的は「正確に全員の押した回数をカウントすること」よりも、「プレイヤー間で協力して数字を積み重ねていくこと」です。なのでやはりcontent.s > this.scoreはスコア自体を共有するので間違っていなかったような気もします。

・ゲームサイズが大きすぎ
最終的なゲームのサイズは12.8MBでした。Flappy Birdは400KB、Photonを用いた3D通信ゲームは1.29MBとかなり差があります。
データ容量増大の要因は明らかに11.7MBもある素材データですね。
制作当時気付いていれば軽量化の検討ができたんですが、ゲーム自体の完成がかなりギリギリだったのでそこまで手が回りませんでした。。。

学び

今回は2Dでしたがゲームエンジンを使えば3Dのゲームも作るのは難しくなさそうだなと思いました。ゲームって意外と簡単に作れることがわかったし、だんだん出来上がっていって実際に自分がプレイできるのが楽しかったです。

開発の流れでいうと、ゲームを作る前の段階でイベント企画側との会合を重ねて、要件定義やイメージのすり合わせが事前にしっかりとできていたのがかなり良かったです。実装するときにはあとは作るだけで、余計なやり取りなく進められました。2週間という初心者には短い期間での制作でしたが、うまく時間を使えたかなと思います。


参考

【JavaScript】Photon + PlayCanvasを使ってモバイル・デスクトップで動く一人称視点のマルチプレイができる空間を作る【WebGL】 #JavaScript - Qiita

Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信するプロジェクトを作る #JavaScript - Qiita

キーボード入力の基礎 | Learn PlayCanvas

【JavaScript】PlayCanvasを使ってモバイル・デスクトップで動く3D空間上で動画が見れるゲーム作る【WebGL】 #JavaScript - Qiita

Flappy Bird | Learn PlayCanvas

この記事が気に入ったらサポートをしてみませんか?