PhaserでHTML5ゲームを作ってみた
はじめに
インディーゲームを作っておりますnyorokoと申します。
以前はUnityでゲームを制作していたのですが、PhaserでHTML5ゲームを作ってみたので、記事にしてみました。
私自身が初心者であるため解説記事というよりは開発手記という体裁にはなっていますが、それでもPhaserはUnityと違って日本語の情報が非常に少ないので、多少なりとも有用であると信じて投稿することにしました。
完成したもの
YouTubeのvideoIDが不正です
きっかけ
私の初めての作品はUnityで作ったダウンロードしてプレイするタイプのPCゲームでした。
しかし、
- PCゲームという時点でハードルが高い
- ダウンロードする気になりにくい(面倒、ウイルスの懸念等々)
という欠点があるため、閲覧数の割にはダウンロードがそこまで伸びませんでした。
そこで、次回作はスマホでもプレイできるゲームにしようと思ったのですが、色々調べてみたところ、
- AppleでもGoogle Playでも、デベロッパーとして登録料が必要
- AppleでもGoogle Playでも、アップロードに審査が必要
- iOSゲームの場合、開発にmac(Xcode)が必須
という欠点があり、いつもTwitterでお世話になっているインディーゲーム開発者のみなさまによると、特に2点目の審査が大きな障壁となっているようです。
私の選好である、
- デバイスを選ばずにプレイできる
- ダウンロードが不要もしくは簡単
- フリーのプラットフォームである
- 審査が無い又は緩い
を全て満たすのは、itch.ioというインディーゲームのプラットフォームにHTML5ゲームとして公開することだという結論に至りました。
HTML5ゲームのフレームワークの選択
Unityではできないのか?
Unityでは、PCゲームやスマホゲームはもちろん、ビルドオプションでWebGLを指定すればHTML5ゲームも作成することができます。
ただし、現時点ではUnityのWebGLはスマホでのプレイに非対応とのこと(強引に動かすことはできるらしいですが)でしたので、別のフレームワークが必要になりました。
どのフレームワークにするか
HTML5ゲームの開発環境としてPlayCanvasというものを見つけ、最初はこれにしようとしましたが、無料版だと色々制約があることや私は2Dゲームを作るので3Dに強いフレームワークである必要がないことを考え、Phaserにしました。
Phaserについて
Phaserとは、HTML5の2Dゲームを作ることができるオープンソースのJavascriptライブラリです。
UnityやPlayCanvasのような開発環境ではなく一つのライブラリなので、例えばヴィジュアルエディタはありませんが、個人的には意外と問題になりませんでした。
現在はPhaser3までリリースされており、日本では知名度が低いですが海外では活発で大きなコミュニティが形成されています。
以下で、私見ではありますがPhaserのメリットとデメリットを列挙します。
Phaserのメリット
- オープンソースのライブラリであり、もちろん無料で広告も入らない
- 現在も更新が続いており、使えなくなるリスクが少ない
- Unityと比較して軽量
- レンダリングにPixi.jsを使っており、ゲーム製作の経験が無くてもPixi.jsやp5.jsを使ったことがあれば雰囲気が掴みやすい
- 「英語なら」情報が多い
- あくまでもJavascriptの一ライブラリに過ぎないので、Webの技術と組み合わせることができる
例: ゲームのパラメータ入力画面としてJSXで作成したフォームを用いる
Phaserのデメリット
- 2Dゲーム用のライブラリである
HTML5で3Dゲームが作りたい場合、PlayCanvasを使うか、Three.jsを使うなどが考えられます。 - ヴィジュアルエディタが無い
- 日本語の情報が非常に少ない
本記事を投稿した理由として、少しでも日本人のPhaserユーザーを増やしたいというのがあります。 - ソースコードが丸見え
Javascriptですので難読化はできますが暗号化はできません。本当に重要な処理はサーバーサイドで行うようにしましょう。
開発手記
開発環境の構築
今回は、VSCode + TypeScript + Viteを使用しました。
TypeScriptを使用した理由としては、
- ある程度規模が大きくなりうるため型安全性が担保された方が嬉しい
- ゲームライブラリであるPhaserには大量のクラスやプロパティがあり、それを調べるためにいちいちドキュメントを参照するのは非常に煩雑で、エディタの入力補完無しだと非常に厳しい
が挙げられます。
特に、2点目は本当に重要で、エディタの入力補完のためだけでもTypeScriptを採用する価値があったと感じております。
ビルドツールにViteを使用した理由は、webpackよりも高速であることが挙げられます。
開発環境の構築にあたり、以下の記事を参考にさせていただきました。
Phaser入門
私は過去にp5.jsというライブラリを使ってジェネラティブアートを作成したりごく簡単なゲームを作ったりしたことがありました。
そのため、Phaserの挙動についてもある程度類推することができる部分はあったのですが、それでもいきなりPhaserを触っても特にSceneの遷移のあたり挙動がよくわからなかったので、以下の記事を参考にしてまずはチュートリアルとしてPhaserの理解を深めることとしました。
Phaserの基礎から解説してくださっている非常に良い記事で、大変助かりました。
また、ContainerやSceneの遷移のあたりについても理解を深めることができました。
なお、この記事ではビルドツールにwebpackを使っておりますが、viteを使う場合は静的アセットはインポートするかpublicフォルダに置くかしなければならない点に注意してください(私はここでハマりました。詳しくは後述します。)。
どんなゲームを作るか
私のコンセプトは”Simple law, Complex behavior”であり、単純な法則の組み合わせで面白い挙動をするようなゲームを作りたいと思ってます。
当時はカーリングの日本女子の快進撃が話題となっていたので、実装も簡単そうでしたし手始めにカーリングゲームを作ってみることにしました。
数学や物理学が好きなので、PhaserにはUnityのようにPhysicsという物理演算エンジンが既にありますが、こちらは使わずに自分でSpriteを拡張してカーリングのストーンを作成しました。
大まかな構成
シーンは以下の4つとしました。
- タイトル
- ステージセレクト
- シングルモード
- VSモード
シングルモードは、詰将棋のように特定の局面で相手に勝つモードです。
VSモードは、ローカルでの二人対戦モードです。
また、よくあるカーリングゲームと差別化を図るために、スウィーピング(氷面を磨いて摩擦を少なくすること)や回転の影響を計算に織り込み、実際の挙動に近い本格的なものにしました。
当記事はPhaserの紹介という性格が強いため割愛しますが、カーリングのストーンの物理的挙動は意外と面白く(特に回転の影響)、機会があれば語りたいと思っております。
DOMの利用
Phaserのメリットとして
あくまでもJavascriptの一ライブラリに過ぎないので、Webの技術と組み合わせることができる
例: ゲームのパラメータ入力画面としてJSXで作成したフォームを用いる
を挙げましたが、まさにこの点がUIの構築に活きてきました。
最初はSpriteにEventListenerを追加してボタンにしようと思っていたのですが、「HTMLのゲームなんだし普通のWebサイトにあるみたいなボタンを使えるのでは?」と思って調べてみたところ、DOMが使えることを知りました。
ただ、PhaserでのDOMの使用については私の知る限りだと日本語の情報があまり多くなかったため、以下で詳しく解説したいと思います。
まずは実例から提示したいと思います。具体的には、以下のようなUIを作ることができます。
今回は、CSSフレームワークには上記の記事と同じくBulmaを、アイコンにはMaterial Design Iconsを使用しております。
ボタン
タイトル画面のモードセレクトボタンに使用しています。
メッセージ
メッセージダイアログは使い所が多いです。
例えばこのようにCreditを表示するために使っています。
フォーム
VSモードのパラメータ入力画面にフォームを使用しています。
入力された値をVSモードのSceneに引き渡しています。
placeholderも簡単に挿入できるので、DOMを利用するメリットを最も感じたUIでした。
実装方法
例えば、あるSceneのcreate()
内に以下のようなコードを記述してボタンのDOMを生成することもできます。
export default class someScene extends Phaser.Scene{
constructor(){
...
}
...
create(){
const button = document.createElement('div')
button.className = 'button is-primary'
const spanIconElem = document.createElement('span')
spanIconElem.className = 'icon'
const iconElem = document.createElement('i')
iconElem.className = document.className = 'mdi mdi-play'
const spanTextElem = document.createElement('span')
spanTextElem.innerText = 'Start'
spanIconElem.appendChild(iconElem)
button.appendChild(spanIconElem)
button.appendChild(spanTextElem)
this.add.dom(xPos, yPos, button)
...
}
...
}
しかし、いちいちclassName
やinnerText
を編集して子要素をappend
するやり方だと面倒ですし可読性も低いです。
さらに、このように簡単なボタンならまだしも、内部に要素をたくさん持つフォームとなるといよいよ訳がわからなくなります。
そこで、JSXを使うことで、例えば上記のようなボタンは以下のように表現できます。
const button = (
<button class="button is-primary">
<span class="icon">
<i class= "mdi mdi-play"></i>
</span>
<span>
Start
</span>
</button>
)
export default button
import button from "JSXのパス"
export default class someScene extends Phaser.Scene{
constructor(){
...
}
...
create(){
this.add.dom(xPos, yPos, button)
}
...
}
PhaserでJSXを使用する場合の設定方法はこちらの記事が参考になります。
サンプルコード
実際のゲームに用いたコードは以下の通りです。
メッセージ
const message = (msgClass = 'is-success', title = 'title', body = 'body', hasDeleteButton = false) => {
return (
<article class={"message is-large "+msgClass} style="box-shadow:0 0.5em 1em -0.125em rgb(10 10 10 / 10%), 0 0 0 1px rgb(10 10 10 / 2%)">
{title != ''&&
<div class="message-header">
<p>{title}</p>
{hasDeleteButton && <button class="delete" id="deleteButton" aria-label="delete"></button>}
</div>
}
<div class="message-body">
{body}
</div>
</article>
)
}
//memo:panelやboxと違いデフォルトでは影が無いので、box-shadowで影を指定
export default message
メッセージボックスは色々な場面で呼び出したいのですが、個別にJSXを設定するのは面倒です。
そこで、JSXでは、上記のように引数を持たせて汎用的なものとすることができます。
それぞれの引数の意味は以下の通りです。
引数 | 機能 | 引数の例 |
---|---|---|
msgClass | Bulmaのメッセージダイアログの種類を変える。 | ‘is-success’(緑色), ‘is-danger’(赤色), ‘is-info’(青色) |
title | メッセージダイアログのヘッダの文字。空文字列を指定すると、ヘッダなしで本文だけになる。 | ‘Result’ |
body | メッセージダイアログの本文。 | ‘Player1 won the game!’ |
hasDeleteButton | trueにするとメッセージダイアログの右上に×ボタンが付く。 | true, false |
例えば、VSモードでの対戦結果を表示するために以下のように呼び出して使うことができます。
const {witdh, height} = this.sys.game.canvas;
const dom = this.add.dom(width/2,height/2,messageJSX('is-info','Result','Player1 won the game!',true));
dom.setScale(1.5);
//デリートボタンにイベントリスナを設定
document.getElementById('deleteButton')!.addEventListener('click', ()=>{
this.scene.start('title');
});
ポイントとして、サイズ調整はCSSで行うのではなくsetScale()
で行った方が綺麗になる傾向があります。
また、今回はDOMを作成した後にデリートボタンにイベントリスナを設定し、デリートボタンを押すとタイトル画面に戻るようにしています。
プレイ画面はこんな感じになります。
フォーム
const VSmodeInput = (
<div class="box">
<div class="field">
<label class="label">Player1's name</label>
<div class="control">
<input class="input" id="player1Name" placeholder="default:Player1"></input>
</div>
</div>
<div class="field">
<label class="label">Player2's name</label>
<div class="control">
<input class="input" id="player2Name" placeholder="default:Player2"></input>
</div>
</div>
<div class="field">
<label class="label">Ends</label>
<div class="select">
<select class="select" id="ends">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</select>
</div>
</div>
<button class="button is-primary is-rounded" id="startButton">
<span class="icon">
<i class= "mdi mdi-play-circle fa-fw"></i>
</span>
<span>Start</span>
</button>
<button class="button is-danger is-light is-rounded" id="backButton">
<span class="icon">
<i class= "mdi mdi-backspace fa-fw"></i>
</span>
<span>Back</span>
</button>
</div>
)
export default VSmodeInput
フォームに入力された項目は、document.getElemetById(id).value
で取得することができます。
取得した項目を、VSモードのシーンをスタートするときの引数であるdata
に含めることで引き渡しています。
ハマったところ
Unityと違い日本語の情報が少なく、一度ハマると大変でした。
細かい点が多いですが、みなさまの参考になれば幸いです。
Viteにおける静的アセットの取り扱い
静的アセットはインポートするかpublicフォルダに置くかしないといけないとのことでした。
これを知らず、上記の入門記事をそのまま移植して失敗しました。
Buttonにdisabled属性を設定した場合の挙動
Buttonにdisabled属性を設定してもボタンが半透明にならず、あたかもクリック可能かのようになってました。
おそらく、Phaserの側でopacityを上書きしてしまっているものと思われます。
disabled属性を指定したいボタンについては、domを生成した後に、dom.setAlpha(0.5)
とすることで半透明にしました。
Scene遷移時の挙動
変数の扱い
Unityではstatic変数かDontDestroyOnLoad()
を使用しなければScene遷移時にオブジェクトが破棄されてしまいますが、Phaserではthis.scene.start()
で別シーンに遷移しただけではSceneのプロパティは破棄されません。
これを知らずにUnityのノリでコーディングし続けた結果、タイトル画面とゲーム画面を行き来したときの挙動がおかしくなり焦りました。
create()が実行される条件
create()
はSceneが初めて開始されたときに実行されるものと思っていましたが、そうではなく呼び出されたときに実行されます。
例えばthis.scene.start()
でタイトル画面→ゲーム画面→タイトル画面と遷移したときにはタイトル画面のcreate()
はそれぞれ1回ずつ呼び出されます。
ビルド時の設定
以下の記事によると、Viteは4k以下の小さな画像をbase64でインライン化してしまうので、vite.config.tsを編集してこれを止めないと画像がリンク切れしてしまいます。
最後に
私自身が初心者であり至らぬ点が多かったかとは思いますが、ご参考になれば幸いです。
Phaserは日本人のユーザーがまだまだ少ないため、この記事を機に一人でも多くの方がPhaserに興味を持ってくださればこれ以上の喜びはありません。
最後に、私はこれからもPhaserでHTML5ゲームを作っていこうと思っており、引き続きゲームや記事を出そうと思っておりますので、よろしくお願いします!
Discussion