クロスプラットフォーム対応なブラウザゲームをつくるならPhaserがおすすめ
ainoya
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。
こんにちは。〈英単語サプリ〉〈英語サプリ〉担当のainoyaです。
英単語サプリは現在iOSとAndroidの2つのプラットフォームでアプリを提供していますが、アプリ開発のほかにもプロダクトの拡大を目的として様々な技術検討を行っています。今回はその検討の一環として、ブラウザゲーム開発フレームワークのPhaserを使って、どのようなことができるか試してみました。
クロスプラットフォームなアプリ開発の選択肢としてWebを選ぶこと
クロスプラットフォームなゲームアプリ開発を行いたいとなった場合、まず候補に挙げられるのがUnityやCocos2d-xなどの専用フレームワークでしょう。これらは高い表現力や動作の高速性、豊富なアセットがとても魅力的です。しかしながら弊社のようなWeb開発の現場でこれらのフレームワークを採用するのは、エンジニアの学習コストや、知識不足からの保守性の不安などいくつかの懸念点がついてまわります。
代替案として、ブラウザ上で動くWebアプリケーションの形態で開発するのはどうでしょうか。大抵の端末にはブラウザが入っているはずなので、前者の場合と同様クロスプラットフォームなアプリを簡単に提供することができます。ところが現実はそう甘くなく、クロスプラットフォーム開発を売りにしていた開発フレームワークのほとんどは、ブラウザ間の差異を吸収しきれない、モバイル端末のスペックの問題で結局満足に動かないことがほとんどでした。そのような経緯で、ブラウザベースのスマホゲームアプリが少なくなっていったように思われますが、近年のブラウザ仕様の成熟化やHTML5の普及、モバイル端末の高スペック化に伴い、ブラウザベースのゲームアプリの可能性が再燃しつつあります。
参考: スマートフォンゲームがネイティブからHTML5へ回帰したい背景 & WebGLレンダリングもできる2DゲームライブラリPhaserでGO! - Qiita
Phaserとは
PhaserはCanvasとWebGLをベースとしたHTML5ゲーム開発フレームワークです。
GitHub上で開発が進められており、Photon StormというHTML5ゲームの開発会社が開発の母体となっています。Phaserの特徴として以下のような点を挙げてみました。
とくにドキュメントが異様に充実しているのが良い点で、サンプルを組み合わせるだけでJavaScriptに余り馴染みのないエンジニアでも1日とかからずに簡単なゲームを作ることができます。
Phaserで簡単なゲームを作ってみよう
それでは早速、簡単なゲームを作りながらPhaserの機能と使い方について探っていきましょう。
今回作るもの
今回はサンプルとして、「英単語の意味を制限時間内に回答する」ゲームを作ってみます。画面の構成は次のようなものにしたいと思います。
実装の要点は以下となります。
- 制限時間に応じてライフ表示を減らしたり、ゲームオーバーにする
- ユーザのタップ操作で解答選択を検出する
- 背景が流れ、その上をアバターが走るアニメーションをつける
作りたいゲームが定まったところで、プロジェクトのセットアップから順を追って作り方を説明していきます。
プロジェクトのセットアップ
Phaserの開発には、一般的なWebアプリケーションのフロントエンド開発環境と全く同じツールが使えます。プロジェクトのセットアップには、Yeomanのgeneratorがいくつか出回っていますので、これを使うのがお手軽です。今回はrcolinray/generator-phaser-typescriptを使用しました。PhaserはTypeScriptでの開発をサポートしており、型定義ファイルが提供されていたので、こちらにチャレンジしてみることにします。
# yoコマンドとgeneratorのインストール
$ npm install -g yo generator-phaser-typescript
generatorのインストールが終わったら、実際にプロジェクトを生成してみます。セットアップ情報としていくつか情報を聞かれるので適当な値を入力します。
$ git init eitango-game && cd eitango-game
$ yo phaser-typescript
...
# You're using the fantastic PhaserTypescript generator.
? What is this game called? Eitango-game
# 画面の縦横サイズを聞かれるが、自動伸縮するように後で変更するのでそのままでよい
? How wide is the game canvas? 640
? How tall is the game canvas? 480
? Which phaser version do you want to use? 2.4.4
create app/index.html
...
create bower.json
セットアップが終わったら、grunt
コマンドを実行してみます。開発環境がlive-reloadモードで立ち上がります。ブラウザにはまず最初にYeomanのおじさんのロゴが表示されます。おじさんをクリックすると、真っ黒の画面に遷移します。セットアップした状態ですでに2つのシーンが用意されていて、クリックで遷移するようになっているようです。
パッケージの構成
Yeoman generatorで生成されたファイルを見ながら、grunt
で見た最初の挙動がどうなって起こったのか見ていきます。生成されたファイルは次の通りです。
.
├── Gruntfile.js
├── app
│ ├── assets # ゲーム素材
│ │ └── images
│ │ ├── menu-background.png # 背景画像(おじさんの画像)
│ │ └── preload-bar.png # アセット読み込み時の進捗バー
│ ├── index.html # ゲーム画面を表示するhtml
│ ├── scripts # ゲームのソースコード
│ │ ├── Game.ts # ゲーム初期化のコンストラクタ
│ │ └── State
│ │ ├── Boot.ts # 起動直後に呼ばれる初期化コード
│ │ ├── Main.ts # ゲーム本体の画面(真っ黒な画面)
│ │ ├── Menu.ts # メニュー画面(おじさんを表示する画面)
│ │ └── Preload.ts # プリロード画面のコード
│ └── vendor
├── bower.json
└── package.json
ソースコードの動き方を追ってみる
TypeScriptで書かれたゲームの制御を見て、挙動を追いかけていきます。まず、ゲームを全体管理しているらしきファイル名のGame.ts
を見てみると、コンストラクタ内で画面の設定と各画面に対応するクラスをstate
にマッピングしていることがわかります。マッピングが終わった後、state.start('boot')
によって状態を遷移させているようです。
Gameクラスのコンストラクタでは、画面サイズの指定ができるほか、ゲーム描画の方式をWebGL/Canvasどちらかに設定することが可能です。画面サイズの指定には、ピクセル値ではなくパーセンテージで描画範囲の大きさを指定することができます。今回はモバイルで表示した時の様子が見てみたいので、縦横どちらも"100%"
で画面一杯に表示する設定にしました。
/// <reference path="../vendor/phaser-official/typescript/phaser.d.ts"/>
/// <reference path='State/Boot.ts'/>
/// <reference path='State/Preload.ts'/>
/// <reference path='State/Menu.ts'/>
/// <reference path='State/Main.ts'/>
module Eitango {
export class Game extends Phaser.Game {
constructor() {
super(640, 480, Phaser.AUTO, 'game-div');
this.state.add('boot', State.Boot);
this.state.add('preload', State.Preload);
this.state.add('menu', State.Menu);
this.state.add('main', State.Main);
this.state.start('boot');
}
}
}
window.onload = () => {
var game = new Eitango.Game();
}
次に呼び出されるBoot.ts
では、プリロード(先読み処理)画面で使う進捗バーの画像を読み込んでいるだけで、それが終わったらすぐにプリロード画面に対応するpreload
というstateに遷移するようになっています。(初期状態のプリロード画面は中で1個しか画像読み込みをしていないため早過ぎて見えないと思います)
module Eitango.State {
export class Preload extends Phaser.State {
private preloadBar: Phaser.Sprite;
preload() {
// 進捗バーとして表示する画像を指定
this.preloadBar = this.add.sprite(0, 148, 'preload-bar');
this.load.setPreloadSprite(this.preloadBar);
// "menu"の画面で使用するおじさんの画像をプリロードする
this.load.image('menu-background', 'assets/images/menu-background.png');
// Load remaining assets here
# ここでゲームに使用する画像・音楽などを読み込んでいく
}
create() {
// preload()が完了したら呼ばれ、"menu"stateへ遷移させる
this.game.state.start('menu');
}
}
}
Preload.ts
は、その名前の通りゲームに使用する画像を予め読み込んでおくためのstateとして使われています。preload()
やcreate()
メソッドはPhaserのイベントにフックされており、stateのライフサイクルに応じて決まったタイミングで呼び出されます。プリロードが完了すると、今度はmenu
stateに遷移します。
module Eitango.State {
export class Menu extends Phaser.State {
background: Phaser.Sprite;
create() {
// おじさん画像の設定
this.background = this.add.sprite(80, 0, 'menu-background');
// タップイベントの登録
this.input.onDown.addOnce(() => {
this.game.state.start('main');
});
}
}
}
menu
stateはおじさんを表示している画面に対応します。プリロードした画像は、先ほどのload.image
メソッドで第一引数に指定した文字列をキーにしてthis.add.sprite
で読み出すことができます(サンプル)。また、input.onDown.addOnce()
でタップイベントを検出してmain
stateに遷移させています。
遷移先のmain
state(Main.ts
)は、背景が真っ黒な画面に対応しています。ゲーム本編の実装は、ここに記述していけば良さそうです。
全体の見通しがついたところで、このコードに行わせたい処理を書き加えていきます。
図形を配置してみよう
まずはMain.ts
にゲーム画面のレイアウトを書いていきます。レイアウトの構築処理が呼び出されるタイミングはState#create()
メソッドで呼ばれるようにしておくとよさそうです。
四角形や直線など、基本的な図形を書くAPIはPhaser.Graphics
に用意されています。例えば直線や長方形を表示したい場合は次のように書けばよいです。この挙動はPhaser公式ドキュメントのサンプル集で簡単に試せるようになっています。
// 直線を描画する
graphics.moveTo(width, y);
graphics.lineStyle(1, 0xCCCCCC, 1);
graphics.lineTo(width, height);
// 長方形を描画する
graphics.lineStyle(0);
graphics.beginFill(0xFFFFFF);
graphics.drawRect(x, y, width, height);
graphics.endFill();
背景を動かしてみよう
画面のレイアウトができたら、次は背景を動かしてみます。背景画像の表示には、Phaser.TileSprite
を使うのが良さそうです(サンプル)。TileSprite
には、背景に画像を自動で繰り返し敷き詰めるなどの機能が備わっています。
キャラクターが走っている感じを出すために、背景は一定の速度で横に動かしてみます。tilePosition
で背景画像の表示位置が変更できるので、Stateのフレーム更新毎に呼ばれるupdate()
メソッドで毎回位置を更新するようにします。
update() {
//"background"に設定されたtileSpriteの位置を横にずらして更新する
background.tilePosition.x -= 1;
}
動くキャラクターを配置しよう
今度は、画面上に動くキャラクターを配置します。画像を読み込んで動かす場合は、animationの機能を使います(サンプル)。Loader#spritesheetを使ってアニメーションがコマ別に展開されたスプライトシートを読み込み、AnimationManager#playで再生します。
// Preload.ts
this.load.spritesheet('girl-run', 'assets/sprites/girl-run.png', 350, 655, 12);
// Main.ts
var girl = this.game.add.sprite(x, y, 'girl-run');
var run = girl.animations.add('run');
girl.animations.play('run', 15, true);
ゲーム操作を受け付けるようにしてみよう
画面作りが一通り終わったので、次に回答選択肢をタップしたときの入力を受け付けられるようにします。入力操作はPhaser.Inputで検出でき、コールバック関数にイベント発生時行いたい処理を記述します。スプライト画像の場合は、ドラッグや当たり判定を考慮した移動制限など細かい制御ができます。サンプルを参考にしてください。
var answerBox = new Phaser.Rectangle(x, y, width, height);
game.input.onDown.add(function(pointer){
if(menu.answerBox.contains(pointer.x, pointer.y)){
// 回答の正誤判定処理を記述
}
}
効果音とBGMをいれてみよう
最後の仕上げに、サンプルを参考にしながら効果音とBGMを入れてみます。スプライト画像同様、Preload.ts
で先に音源を読み込んでおき、ゲーム本編で音源を読みだします。ブラウザ(端末)が対応していれば、ogg/m4a/mp3など異なる種類の音源でも再生可能になっています。
// Preload.ts
// BGM
this.load.audio('bgm', 'assets/sounds/bgm/bgm.m4a');
// 効果音
this.load.audio('ok', 'assets/sounds/se/ok.m4a');
// Main.ts
private bgm;
private ok;
create(){
// 音源の読み込み
this.bgm = this.game.add.audio('bgm');
this.ok = this.game.add.audio('ok');
}
start(){
// BGMをループ再生する
this.bgm.loopFull(0.6);
}
//回答判定メソッド
answer(){
if(answer.isCorrect){a
//正解なら正解音を再生
this.ok.play();
}
}
いろいろな端末で動かしてみよう
完成したゲームデモを、iOSシミュレータで動かしてみました。上は再生時の動画です。実機のiPhone6でも表示確認しましたが、Safari, Chrome共に軽快に動作していました。Android端末でも試したところ、最近の端末(AQUOS Zeta SH-03G)では問題なく動作しましたが、古い端末(AQUOS PHONE CL IS17SH)では描画のカクつきや音声が再生されないなど、正常に動作しませんでした。ブラウザレンダリングのため、端末にはやはりある程度のスペックが要求されるようです。
Phaserはお手軽にブラウザゲームが作れるフレームワーク
Phaserを使うと、モバイル対応のブラウザゲームが簡単に作れることが分かりました。ゲームデモ作成と動作テストを行って見た結果、Phaserは下記のような開発要件のときに使えそうなフレームワークだと感じました。
- あまり複雑でなく、レンダリング速度を必要としない簡単な2Dゲーム
- Unity/Cocosエンジニアがおらず、Webエンジニアだけで手っ取り早く1ソースでクロスプラットフォームなゲームを提供したい時
- プロトタイプ作成フェーズなどで操作感確認のために簡単なゲームデモを作りたい時
また、上記の観点で教育分野などでゲーミフィケーションをテーマにしたアプリ開発にも向いていると感じました。小中高の教育の現場では近年ICT教材を利用した授業が普及しつつありますが、使われている端末は現場によって多種多様なものです。Phaserもある程度の端末スペックを要求するため、まだまだ十分な技術とは言い難いですが、こうした分野においてクラスプラットフォームで動作するブラウザベースのアプリケーションは今後重要な存在になってきそうです。
参考
- HTML5ゲームエンジン〈Phaser〉 ( 初心者やモバイルアプリエンジニアにやさしくていい感じです )