Lab
敵もいない。ゲームオーバーもない。ただひたすらゴールを目指して次の面へ進む——暇つぶし全振りの純粋系迷路ゲームを作ることにした。
ゲームの名前はLABYRIA。labyrinth(迷宮)の造語だ。かわいいし謎っぽい。それだけで採用した。
「死なない」はコンセプトだが、設計上の要件でもある。敵がいないからクリアできない、ではなく——岩・氷・鍵・ワープという4種類の障害物があっても、必ずゴールに辿り着ける迷路を自動生成する必要がある。そのための仕組みを作るのが、このゲームの技術的な核心だった。
毎回違う迷路が出てくるように、DFS(深さ優先探索)で自動生成する仕組みを作った。再帰ではなく反復スタックで実装したのでスタックオーバーフローの心配がない。
壁の情報はビットマスクで持つ。各セルに北・南・東・西の4方向をビットフラグとして割り当てて、壁を壊すときは&= ~WN、壁の有無を確認するときは& WN。1マス1整数で4方向の壁情報をすべて持てる。シンプルで速い。
レベルが上がるほど迷路が大きくなる設計にした。最初は5×5のサクサク迷路から始まって、徐々に15×15まで成長する。最初にクリアできる達成感があるから、大きくなっても続けられる。
テーマはレベルに応じて自動で切り替わる。FOREST・OCEAN・CAVE・MAGMA・SPACEの5種類。アクセントカラー・壁色・グロー・パーティクル色をすべてテーマオブジェクトから参照するので、テーマ切替が1行で済む。
障害物を増やすほど、「詰み」が発生するリスクが増える。たとえば岩でゴールへの道を塞いでしまったら、ゲームがクリアできなくなる。
解決策はシンプルで、障害物を置くたびにBFS(幅優先探索)でゴールまで到達可能かチェックする。詰まる配置は即リジェクトして、別の場所に置き直す。この「flood-fill」チェックを全障害物に流用できる汎用設計にしたのがポイントだ。
氷の障害物には別の制約がある。「滑って曲がれない詰み」を防ぐため、直線通路(分岐のないセル)にだけ配置する。壁ビットマスクで「N-Sだけ通路」か「E-Wだけ通路」かを判定して、分岐点への設置を禁止した。
鍵とワープも、同じ「到達可能チェック」を通している。鍵は鍵の場所とゴールの両方に到達できることを確認してから設置する。ワープはワープ先の両端が到達可能な組み合わせだけを使う。
4種類の障害物が全部「詰み防止チェック済み」であることで、「死なない迷路」の保証が成立している。
パーティクルは2層に分けた
レベルクリア時の花火エフェクトが壁の裏に隠れてしまう問題があった。「背景パーティクル(ambient)」と「爆発パーティクル(burst)」を分離して、burstだけプレイヤーより後に描く。描画順をコントロールするだけで花火がちゃんと見える。
押しっぱなしでスイスイ動く
「1歩ずつ押すのが指疲れる」という問題を、ハイブリッド入力で解決した。keydownで即座に1歩動かし、ゲームループで押しっぱなしを検知してオートリピートする。e.repeatフラグで「最初の1回だけ即発火」が簡単に書ける。ゲームパッドも同じ仕組みで対応した。
スタート・ゴール・錠前は絵文字で
スタートは🚩、ゴールは🏁、錠前中は❓。鍵を取ると❓が🏁に変わる演出もCanvasのfillTextで絵文字を描くだけ。シンプルだけどちゃんとドラマチックに見える。
このゲームを作って改めて気づいたことがある。「面白いゲーム」と「ちゃんと遊べるゲーム」は別の問題だ、ということ。
障害物を増やすのは簡単だ。でも「詰まらない」保証がなければ、プレイヤーはゲームを楽しめない。flood-fillチェック・氷の直線通路制限・ワープの到達可能チェック——これらは「面白さ」には直接寄与しない。でも「ちゃんと遊べる」ための土台になっている。
WebAudioAPIで効果音を作った。ライブラリなしでtone()関数だけで全SEを合成する。ゴールした瞬間の音、鍵を取った音、障害物にぶつかった音——それぞれ周波数と長さを変えるだけで違う感触になる。外部依存ゼロで配布が簡単になる、という副産物もある。
「死なない迷路ゲーム LABYRIA」を1日でゼロから完成させた。DFS反復スタックによる迷路自動生成、ビットマスクによる壁管理、flood-fillによる詰まり防止——地味だけど大切な技術を大量に組み合わせた。
5種類のテーマ・4種類の障害物・レベルに応じた動的なサイズ変化で、見た目もゲームプレイも変化に富んだ作品になった。「ちゃんと遊べるゲームを作るための地味な技術」を習得した1日。造語「LABYRIA」、なんか気に入ってる。