matter.js で落ちゲー作った

マスクをたくさん集めよう!スマートフォン専用(画面サイズは iPhone 6/7/8 に最適化)。

技術セット

ビルド: Snowpack with tsc
UI: Preact
状態管理: Redux
物理演算 & 描画: matter.js

最近は Snowpack に加え tsc (TypeScript) を素で使っている。Snowpack は既存の npm を ES modules に対応させてくれるだけなので、npm install 直後に一回だけ使えばよい。あとは tsc --watch するだけ、バンドルはしない。Webpack や Babel をフル活用した環境に比べれば制限はあるが、ビルド環境をシンプルにするためその制限を受け入れている。

Preact は、どうせ web 特化なものしか作らないなら、軽いに越したことはないと選択した。Hooks も使えるので大体困らない。Snowpack は現時点で Preact と React をエイリアス名で解決するような機能はないので、いくつか React エコシステムで使えないものが出てくるが、そこはコピペでなんとかした(具体的には react-redux)。

matter.js は物理演算(物体の落下と衝突判定)に加え、canvas への描画にも使っている。ドキュメントによれば canvas 描画クラスはデバッグ用途らしいが、今回作ったもの程度なら十分だった。

コンポーネント設計・状態管理設計

登場人物が Preact (or React) だけであれば Redux に状態管理を委ねるだけだが、今回は matter.js による物理演算クラス (Engine) と描画クラス (Render) も登場し、それぞれが状態を内包したり DOM と接点を持ったりしている。この点の解消のため次のようにした:

- Render クラスは canvas 要素を描画する GameCanvas コンポーネントに閉じ込める(canvas 要素への参照は useRef を使って Render に渡す)
- Engine クラスはアプリのエントリーポイントで初期化し、各コンポーネントではそのインスタンスを useContext 経由で使う

DOM を吐き出すコンポーネントで Engine インスタンスに触ることは避け(例外として TouchPad コンポーネントがあって、これは画面下部の操作エリア)、Engine を触るのは EffectComponent(以下参照)に極力集約した(GameController コンポーネントや GameRunner コンポーネントが生まれた)。

GameRunner は Engine.run() するだけ。GameController は今のところそれ以外のすべての Engine への働きかけを担っていて、壁やキャラクターを用意したり、衝突イベントを扱ったり、敵・アイテムのスポーンタイマーを設定したりしている。

状態の存在する場所: Engine インスタンスと Redux store インスタンス

Engine インスタンス自体が状態を持つ(物体をいくつ持っているかとか、物体の現在位置・速度とか)ため、状態すべてを Redux store で管理することはできない。しかし、そもそも状態すべてを管理する必要はなくて、ゲームの結果に関わる状態だけ管理できればよい。すなわち残ライフ、得点、次にスポーンする敵・アイテムの数や配置を store にためておけばよい。

この役割分担が意識できたことによって、元々あったゲームの設計まったくわからんという感覚が、一つはパターンを見つけたなという感覚に変わった。ゲームの勝敗判定は reducer でやればいいし、ランダムなスポーンはランダムな値なのだからコンポーネント(内の useEffect)でやればよく、Engine に演算対象を追加するのは副作用だからコンポーネントでやればよい。

今後の作り込み

現状、正直面白いゲームではない。物体のサイズや速度や摩擦といった matter.js 寄りの調整(操作感の向上)から、アイテムの種類や得点計算の方法、ゲームモードといったコンテンツ面での充実がほしい。

すっきりと設計(責任原則)が決められたので、決まった場所に業務ルールを増やしていくだけだが、ここからが大変なんだろうな。

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