ブラウザ横スクロールアクションゲーム開発記|TypeScript×Canvasで作るネオンランナー
ポートフォリオサイトにブラウザゲームを載せる企画の第2弾。今回はマリオ風の横スクロールアクション「ネオンランナー」を作った。物理エンジンから衝突判定、操作性のチューニングまで、プラットフォーマー開発で学んだことを書く。
なぜ横スクロールアクションを作ったか
1作目のゲームを実装してみて、Canvas描画の基本は掴めた。次は「ゲームらしいゲーム」を作りたかった。横スクロールアクションは、物理演算、衝突判定、レベルデザイン、敵AI、アイテムシステムとゲーム開発の要素が詰まっている。勉強としてこれ以上の題材はない。
テーマはサイト全体と統一してネオン。紫(#a855f7)とシアン(#06b6d4)を基調に、黒背景(#0a0a0a)の上で光るビジュアルを目指した。
技術スタック
- 描画: HTML5 Canvas 2D
- 言語: TypeScript(DOM非依存のゲームエンジン + React UIの分離構成)
- フレームワーク: Next.js 16 + Cloudflare Workers
- 操作: PC向けキーボード入力 + モバイル向けバーチャルパッド
ゲームエンジン部分はDOM操作を一切しない設計にした。Canvas描画とゲームロジックを純粋なTypeScriptで書き、UIのオーバーレイ(スコア表示、ポーズメニューなど)はReactコンポーネントとして分離している。この構成のおかげでテストが書きやすく、ロジックの見通しもいい。
物理エンジンの実装
プラットフォーマーの物理で最も重要なのは「重力」と「衝突応答」の2つ。
重力とジャンプ
毎フレーム、プレイヤーのY速度に重力加速度を加算する。ジャンプは初速度を負方向(上向き)に設定するだけ。シンプルだが、数値のチューニングでゲームの手触りが大きく変わる。
重力が強すぎるとジャンプが低くなってステージの自由度が下がるし、弱すぎるとフワフワして気持ち悪い。何度も調整を繰り返して、マリオに近い「しっかり跳ぶけど着地は速い」感覚に近づけた。
AABB衝突判定
衝突判定にはAABB(Axis-Aligned Bounding Box)を採用した。各オブジェクトを軸に沿った矩形で囲み、矩形同士の重なりを検出する方式。回転を考慮しなくていいので計算が速い。
タイルマップとの衝突解決では、X軸とY軸を分離して処理するのがポイント。両軸を同時に処理すると、壁に沿って滑り落ちるときに引っかかるバグが起きやすい。X方向の移動を処理してから衝突解決、次にY方向の移動を処理して衝突解決、という順序で処理することで安定した挙動になる。
Swept Collisionによる踏みつけ判定
敵の踏みつけ判定には通常のAABBだけでは不十分だった。プレイヤーが高速で落下すると、1フレームで敵をすり抜けることがある。いわゆるトンネリング問題。
これを解決するためにSwept Collisionを導入した。前フレームの位置から現フレームの位置までの軌跡を追跡し、その間に衝突が発生したかを判定する。プレイヤーが敵の上部に接触した場合は「踏みつけ」、側面や下部に接触した場合は「ダメージ」として処理を分岐させている。
操作性の工夫
プラットフォーマーで最も大事なのは操作性。自分がプレイしてストレスを感じない手触りにするために、2つのテクニックを実装した。
ジャンプバッファ(120ms)
ジャンプボタンを地面に着く直前に押してしまうことはよくある。ジャンプバッファは「着地の120ms前までに押されたジャンプ入力を、着地時に自動で発動させる」仕組み。これがないと「ジャンプボタンを押したのに跳ばない」というフラストレーションが頻発する。
コヨーテタイム(80ms)
足場から落ちた直後の80ms間、まだジャンプできるようにする仕組み。名前の由来はワイリー・コヨーテが崖から落ちた後に一瞬空中で静止するアレ。プレイヤーの視覚と操作にはどうしてもラグがあるので、この猶予があるだけで「理不尽に落ちた」と感じることが激減する。
この2つは多くの商用プラットフォーマーでも採用されているテクニックで、実装コストの割に効果が大きい。プラットフォーマーを作るなら絶対に入れるべき。
ハテナブロック(?ブロック)とアイテムシステム
マリオといえば?ブロック。ネオンランナーにも実装した。プレイヤーが下からブロックに頭突きすると、ブロックが反応してアイテムが出現する。ブロックは一度叩くと使用済みになり、見た目も変わる。
シールドアイテム
?ブロックから出てくるのはシールドアイテム。シアンに光るダイヤモンド型のアイテムで、取得するとプレイヤーの周囲にオーラリングが表示される。シールド装備中は敵からの攻撃を1回だけ防御できる。ヒットするとシールドが消滅し、代わりにダメージを受けない。
シールドの配置はレベルデザインと密接に関わっていて、難所の手前にシールドブロックを置くことで「保険をかけてから挑戦する」プレイスタイルが生まれる。
敵AIとバウンスメカニクス
敵は左右にパトロールする基本的なAI。足場の端に到達するか壁にぶつかると反転する。シンプルだが、配置次第で十分な難易度を作れる。
踏みつけで敵を倒すと、プレイヤーは通常ジャンプの2倍の高さでバウンスする。これは単なる演出ではなく、レベルデザインに組み込まれた仕掛け。通常のジャンプでは届かない高所にアイテムやルートを配置して、敵を踏み台にすることで到達できるようにしている。
このバウンスメカニクスは、敵を「障害物」ではなく「ツール」として活用する設計で、プレイヤーに発見の楽しさを提供する。
ネオン演出
ビジュアル面では、Canvas 2Dのshadowプロパティを活用してネオンの発光表現を実現した。テキスト、ブロック、アイテム、プレイヤーの各要素にshadowBlurとshadowColorを設定することで、追加のレイヤーなしでグロウ効果が出せる。
背景には星が流れるパーティクルエフェクトを配置して、疾走感を演出している。パフォーマンスを考慮して、パーティクルの数はデバイスの負荷に応じて調整している。
遊び方
PC操作
- ←→キー: 左右移動
- Space / Enter / ↑キー: ジャンプ
- P / Escキー: ポーズ
モバイル操作
画面下部にバーチャルパッドが表示される。
- ←→ボタン: 左右移動
- JUMPボタン: ジャンプ
ゲームルール
- ライフ3つでスタート
- 敵に当たるとライフが減る(シールドがあれば1回防御)
- 敵を踏みつけるとスコア獲得+高バウンス
- ?ブロックを下から叩くとアイテム出現
- ライフがゼロでゲームオーバー
プラットフォーマー開発のTips
この開発で学んだことをまとめておく。
- 物理の数値調整は体感で決める。数式よりもプレイして「気持ちいいか」で判断した方がいい結果になる。
- XY軸分離の衝突解決は必須。同時処理だと必ずバグる。
- ジャンプバッファとコヨーテタイムは最優先で実装。操作性が劇的に変わる。
- ゲームエンジンとUIを分離する。DOM非依存にしておくとテストもデバッグも楽。
- 敵の配置がレベルデザインの8割。同じ敵でも配置次第で難易度が全く変わる。
まとめ
横スクロールアクションは、実装してみると想像以上に多くの要素が絡み合っている。物理エンジン、衝突判定、操作性チューニング、レベルデザイン、アイテムシステム。どれか一つが欠けてもゲームとして成立しない。
特に操作性は、コードの良し悪しではなく「触ったときの手触り」がすべて。ジャンプバッファとコヨーテタイムという小さな工夫が、プレイ体験を大きく左右することを実感した。
ブラウザだけで動くプラットフォーマーとしては、それなりに遊べるものになったと思う。ぜひ試してみてほしい。
他の記事
ブラウザで遊べるテトリス風パズルをTypeScriptで作った話
HTML5 CanvasとTypeScriptでテトリス風ブロックパズルを実装した。エンジンとUIを分離するSnapshotパターンや、ネオン演出のこだわりを解説する。
ゲーム実況のサムネイル作りで学んだデザインの基本
非デザイナーがゲーム配信のサムネイルを自作し続けて気づいた、視認性・配色・構図の基本ルール。試行錯誤の記録。
配信のコメント欄が動いた瞬間の話。ゼロからイチの体験
ゲーム配信でコメントがゼロの日々を超えて、初めてリアルタイムでコメントが来た瞬間の話。ゼロからイチの体験がモチベーションを変えた。
インフラエンジニアがゲーム配信するギャップ。仕事と趣味の温度差
平日はKubernetesとTerraformを触り、休日はゲーム実況で叫んでいる。仕事と趣味の温度差について書く。