ブラウザ版 To Hole of Hell 開発後記

To Hole of Hell - RPGアツマール

先日ブラウザ版 To Hole of Hell (以下 THH )をリリースした。ブラウザで遊べるので試しに遊んでみてほしい。

THH は2012年4月に、ゆえっちこと結城えいしくんと二人で作って公開したゲームだ。ツイッターにスコアを投稿する機能があったのでみんな遊んでツイッターに投稿してくれていたのが、2015年くらいまで続いていた。

それから RPGツクールMV が発表になって、ブラウザ向けに気軽にゲームを作れるようになったのを見ながら、THH もブラウザで遊べたらいいな~ということを思っていた。*1

THH は当時すでに完成されたゲームで、試しに組んだ機能が遊んでみたらカッチリとハマって触るところがあまりないという状態でリリースできた。これは幸運なことだったけれど、感覚的にはこれでちゃんと面白いゲームになっているということは言えても、どうして面白いのか自分で説明するのが難しい状態だった。

今回ブラウザ版を作るにあたってロジックの移植をしてテストプレイして挙動をオリジナルに合うよう調整するということをやってみて、そのあたりが言語化できそうだなと思ったので、書いておく。ゲームの仕様みたいなことも包み隠さず書いていくので、攻略の参考にもなるかもしれない。
あとたぶん RPGツクールMV でアクションゲームを作る話とかはそれなりに技術的なバリューがあると思うのでそのあたりも触れておく。

ゲームデザインの話

ゲームプレイにメリハリをつける

移植作業の初期はスクロール速度が遅くなったり早くなったりせず一定のままだったが、これがとにかく退屈だった。
で、開発初期にゆえっちから「ゲームプレイに変化がないからスクロールスピード早くしたり遅くしたりできないか」という提案があってそれを組み込んだが、実装にあたっては、特に深く考えずになんとなく1000フレーム周期で早くなったり遅くなったりするようにした。
900フレームごとに100フレームかけて早くなるのを二回、次の900フレーム後に100フレームかけて元の速度に戻すということをやっている。動かしてみてしっくりきたからこうという感じでこの数字になっていて、明確な根拠はないのだが、これがちょうどいい具合になっていると思う。
実際に遊ぶと24~5階で遅くなって、次に32階付近でまた早くなる、というふうになっていると思う。ゲームのサイクルが16階層周期なので、ちょうどマッチする。

ゲームのサイクルが16階層周期なのは、一回のステージ生成で16階層分作って、以降16階毎にステージを再生成している都合からだ。16の倍数階には敵が出現しない。ループ境界をまたいだときに敵がワープすることがあったのでそれを抑止するための苦肉の策だったのだが、敵がいないことがわかっている階層が周期的にやってくるのは、緊張し続けなくてよくなるので、結果的にはよかったと思う。

なお、16の倍数階層ごとに敵出現率が上がるようになっている。計算上は144階以降はすべての足場に敵か回復アイテムのいずれかが配置される状態になる。ふつうにプレイするとまずそこまで到達できないだろうとは思う。

ゲームプレイと BGM の協調

ブラウザ版では46~7階層で曲が一周して、50階層でゆっくりになるところでイントロが終わるくらいの感じになっている。ここはゲーム体験とシンクロしていてかなりいいんだけど、これは本当に偶然で、まったく意図していなかった。曲が展開して盛り上がりに入っていくところでスクロール速度がゆっくりから加速に切り替わっていくところも、これも本当に偶然だが、プレイしていてかなりしっくりくる。BGMとゲーム体験についてはオリジナルの THH を作った当時にはあんまりよく考えてなかったのだけど、今は自信を持ってこの状態はよくできていると言える。
さすがに次の一周はゲームのサイクルとはシンクロしていないのだが、曲の二周目が展開して盛り上がるあたりから敵の密度が高まってくる。スクロール速度はゆっくりだが、むしろ敵が多くなるタイミングからいきなりスクロール速度が速いよりは適当で、ゆっくりだからかえって「あれ気付いたら敵たくさん湧くようになってない?」という気付きを生むタイミングになっているし、敵が増えて緊張感が増すタイミングで曲が展開して盛り上がるのでグッとくるのではないかと思う。

もちろんブラウザで動作する関係上、曲の読み込みタイミングによっては意図通りにはならないのだが……。

このあたりのことを言語化したかったので、できてよかった。

ランダムだけど完全ランダムでない地形生成

生成といってもあらかじめ組んでおいたパターンを並べているだけである。
だけなのだが、完全にランダムにしようと思うと、

  • 一画面まるごと足場がない
  • 足場の隙間が少なすぎる状態が連続する

こういうことを抑制しないといけない。これをロジックで組むこともできなくはないが、それよりはあらかじめ組んでおいたパターンを並べるほうが制御しやすい。
足場のパターンは14種類で、これが適切かどうかはまだちょっと悩ましい。今は厳しい足場が連続するとかなり厳しく、乱数が効きすぎているなと感じている。足場のパターンごとに出現率に重み付けしたほうがいいんじゃないかと思うが、ちょっと緩めると緩くなりすぎてしまう。

パターンはあらかじめ決まっているので、プレイしているうちになんとなく「この位置に足場が来たら次はここに来やすい」みたいな肌感覚が身についたりするかもしれない。
「目指せ100階」というのはそれも込みの目標階層数で、ちょっと厳しめに設定してある。でも不可能ではないかなというくらい。もちろん祈る場面も少なくはないけど……。

ジャンプの廃止

スクロール速度が変わったりするところはこのゲームのメカニズムの中では複雑な方だと思っているが、基本的にはシンプルにまとめる方向で調整している。
前に公開していたバージョンには回復アイテムもなく、ダメージを受けたら受けっぱなしで、どれだけダメージ受けずに降りられるかみたいなデザインだったが、そのかわりにジャンプできるようになっていた。
で、今作はジャンプできなくするかわりに回復アイテムを配置することにした。
やってるとわかると思うんだけど高速スクロール中はジャンプしてる余裕なんてなく、低速スクロール中はあえて敵をジャンプで乗り越えてまで逆の端から降りたい場面がそんなになかった。もちろん下に足場がないけど敵が前にいて邪魔、みたいなことはたまにあるが、その場面でとっさにジャンプを思いつけるほど、ふだんからジャンプするゲームではないので、下に足場があることを祈ってダイブするほうを選んで、落ちてからそういやジャンプできたなあ、とか思う。だったらいっそジャンプなくていいよね、ってことで、ジャンプを廃止した。

回復アイテムは、一画面あたり最大一つだけ、1/10の確率で足場に配置される。割と出たり出なかったりの確率だ。やむをえず敵に当たっても、わりと回復できる。回復できるが、ダメージを受けると「回復しなきゃ」ってなって焦る。このあたりの緊張感が個人的にはうまく効いてるんじゃないかと思っているけど、人によっては好みがあるかもしれない。
このゲームのコンセプトには合っていると個人的には思っている。

敵に当たらないにこしたことないので、祈りながらダイブするプレイング。
敵に当たっても後で回復すればいいので、安全に足場を選ぶプレイング。

どっちもありだと思う。ジャンプを廃止して左右の移動だけに絞ったことで、どっちも選べるようになった。操作ボタンが少ないほうが、プレイヤーの選択の幅が広がることもある。

といいつつ、今後ジャンプにかわるアクションを追加する予定でいる。ジャンプは気軽に使えないからよくなかったので、今作のゲームデザインにあう形のアクションなら組み込んでもいいだろうという。もちろんそれでゲームバランスは変わってしまうだろうが、今のゲームバランスもそれなりに気に入っているので、クラシックモードとして残すか、何かしら考えている。
どういう形になるにせよ、今後もアップデートを続けてもっと遊べるようにしていくので、楽しみにしていてほしい。

実装の話

RPGツクールMV のプラグインを書いて実装した。TypeScript で書いて JS にトランスパイルして RPGツクールMV に取り込んでいる。
TypeScript でプラグインを書く方法については

www.f-sp.com

など参考になる記事が Web にあるので見てもらえればと思う。
型定義ファイルは以下のものを使うとよいだろう。

github.com

横スクロールアクションの挙動の実装

肝心の実装のポイントは、

  • クラスを継承して独自クラスを作らない
  • クラスにインスタンス変数を追加しすぎない

この二点で、じゃあどうやったかというと、横スクロールアクションの挙動を司るクラスを作って、そのインスタンスGame_Character クラスに保持させるという方針にした。Twitter でかねてより口にしている「無闇に継承せずに委譲とコンポジションを適切に使え」である。

  • SideViewActionCharacter というベタなクラスを作り、Game_Character に持たせる
  • Game_Characterupdate 内で SideViewActionCharacter を更新
  • Game_Character の座標と `SideViewActionCharacter の座標を同期する

ということをやっている。「Game_Characterupdate 内で SideViewActionCharacter を更新」して「Game_Character の座標と SideViewActionCharacter の座標を同期する」ことができれば別に Game_Characterインスタンス変数に SideViewActionCharacter インスタンスを格納する必要はなくて、どっかにテーブルを作って Game_CharacterSideViewActionCharacter を紐づけてもいいのかもしれない。

Game_Character はプリセットの状態でかなり複雑な上、Game_EventGame_Player がそれぞれに機能を拡張している。この状態で横スクロール用の挙動を拡張して実装するのはかなりコストが高いのでやりたくない。既存の動作と競合して意図したとおりに動かなかったりするし、デバッグも困難だからだ。
一から専用のクラスを作って完全にコントロール下に置きたいが、そうなってくると Scene_MapGame_Map 上にその独自クラスを追加することになり、これもまたかなり困難があるし、何よりイベント監視まわりなどの既存の資産をすべて捨てることになる。
既存の動きに乗った上で挙動だけこちらでコントロール可能なクラスに管理させるのはバランスがよいやり方で、実装自体もかなりシンプルになっている。

当たり判定も一から書いているが、実装自体はオリジナルの THH から移植するだけなので難しくないし、元の実装もごくシンプルな AABB ボリュームの交差判定になっている。これくらいならベタで一から書いても大したことはない。*2

このあたりはゆくゆくは汎用的な実装に組み替えてプラグイン化も考えているので、興味のある人は期待せずに待っていてほしい。

ダメージ処理・アイテム取得処理

Game_Character を拡張して横スクロールアクション用のオブジェクトを作った、というわけで、敵やアイテムもイベントとして配置されるようにしてある。
「プレイヤーから触れたとき」をトリガーとしてイベントを記述して、ダメージ処理やアイテム取得処理を組んである。ツクールのイベントコマンドでどうしようもない処理はスクリプトを呼び出してなんとかしている。イベントコマンドのスクリプトGame_Interpreter インスタンスのスコープで解決されるので、ふつうに $gameMap.event(this._eventId) とかやれば実行中の Game_Event インスタンスが取得できるし、ここから SideViewActionCharacter インスタンスにアクセスして任意の処理を行わせればよい。API を整備しておくと便利だろう。
たとえばダメージを受けたら一定時間無敵にする処理は被ダメージイベントをコールする API を用意して Game_Player インスタンスの初期化時にイベントハンドラをセットするというやり方にしてある。

敵の移動ルーチン

Game_Event の移動ルーチンまわりの処理をフックして SideViewActionCharacter を操作しているだけで、難しいことはしていない。座標に関わることはすべて SideViewActionCharacter でやって、ゲーム処理のサイクルはツクール側に任せる、というふうにしてある。これでエディタ上で移動ルートの設定から敵の移動の挙動を記述できる。といっても THH は壁か足場の端にぶつかるまで前進して、ぶつかったら反転する、ということをしているだけなので、凝った挙動はもともと書かなくてよかったりはする。

壁や足場の有無を判定してなにかする部分だけは、移動ルートからスクリプトで呼び出している。
イベントのカスタム移動ルートのスクリプトGame_Interpreter インスタンスのスコープで解決されるので、センサー処理だけスクリプトから呼び出せるように API を整備しておけば、視界にプレイヤーが入ったらどうこうみたいなことも一応制御できるようになる。

動的なマップ生成

ステージ生成用のクラスを作って Scene_Map から呼び出して、Game_MapData_Map にデータを反映する。これだけ。
Game_MapData_Map の構造を把握できてればそんなに難しくないと思う。そのうちに詳細を解説する記事を書きたい気がする。ローグライクゲームなどに応用が効くと思うが、ローグライクはすでにプラグインがあると思うので、それを使ったらいいだろう。

今作のマップ生成はマップパターンをランダムに抽出して縦に連結するだけというシンプルな作りになっていて、マップパターンはエディタ上でそれぞれ一枚のマップとして登録してある。

敵やアイテムの配置パターンはそれぞれのマップ上でイベントとして設定しておいて、マップ生成時には配置情報だけを使うようにしている。
実際の敵やアイテムのイベントはテンプレートイベントを用意して、生成時にクローンしている。テンプレートイベントはマップから読み込んだらどこかのテーブルに格納しておいてマップからは消す。今回みたいな動的にマップを生成するゲーム以外でも、敵や宝箱をランダムに配置したいときには有効な方法だと思う。もちろん既にそういうプラグインはあるので、それを使えばいいだろう。

アンチエイリアスのかからないピクセルフォント

PIXI.extras.BitmapText を使っている。文字とそのグリフの画像ファイル上の位置を記した fnt という形式の xml と グリフを並べた pngPIXI.extras.BitmapText.registerFont に渡してフォントを登録すると、PIXI.extras.BitmapText インスタンスを使ってピクセルフォントを使った文字描画ができるようになる。PIXI.extras.BitmapTextPIXI.Container 派生クラスなので、普通に PIXI.ContaineraddChild すれば画面に乗る。ツクールだと Scene が spriteset というものを持っているのでここに addChild すればピクチャみたいな感じで使える。直接 Scene に addChild すると画面のフェードイン・フェードアウトや色調変更の影響を受けなくなるので注意が必要。

スコア画面はピクチャみたいに使えるプラグインコマンドを生やしてイベントから呼び出して描画している。専用のシーンクラスを作るよりこのほうがイベントの機能を使えて便利なので最近はこういうふうにやってしまうことが多い。

他にも細かいところはいくつかあるものの、実装の話はだいたいこんなところでいいだろう。気になることがあったら気軽に聞いてくれればと思う。

おわりに

もう6年も前に作ったゲームだが、今あらためて遊んでみても「もう一回」となる魅力があって、これが長らく遊べない状況が続いていたのはもったいなかったなと思う。これからは RPGアツマールのサービス終了まではずっと遊べるので、いつでも気が向いたときに遊んでもらえればなと思う。

前述したとおり、今後のアップデートも予定している。アクション追加の他には、たとえば RPGアツマールにはスコアボード機能があるので、各プレイヤーを横断したスコアランキングを作ってみてもいいかなと思っている。あとはよくある実績機能とか。実績向きのゲームシステムだと思うので、たぶん組み込んだら楽しいだろうと思う。他にも思いついたら何かやってみる。もちろん、今すでにシンプルでよく整っているので、あまり足し算しすぎて壊さない範囲で。

*1:実のところ、それより前から Unity に移植する計画などもあったのだが、Unity でオリジナルの挙動を再現するのがむずかしかったので断念した

*2:実はオリジナルの実装には地形の角の接触判定にバグがあって異常な挙動を起こすのだが、面白い動きをするのでそのままに移植した。実際に遊んで体験してみてほしい