【ゲーム開発】ブラウザで遊べる和風ホラー脱出ゲーム「丑三つの間」を作成しました。

はじめに
深夜、見知らぬ和室で目を覚ます。時計の針は午前1時57分。丑三つ時の午前2時までに部屋から出られなければ――。
そんな短編ホラー脱出ゲーム「丑三つの間」を作りました。ブラウザだけで遊べて、クリアまでは3分ほどです。
▶ 遊ぶ: https://iwatanabe-7.github.io/ushimitsu-no-ma/
最初のコミットから公開までは、ほぼ2日でした。この記事では、何を作ったのか、どんな順番で肉付けしていったのか、それからゲーム投稿サイト「ふりーむ!」に出すために作り直した話を書きます。
どんなゲームか

舞台は6か所を調べられる和室です。
- 床の間の掛け軸
- 押入れ
- 仏壇
- 畳
- 鏡台
- 出口の襖
押入れの人形から鍵を見つけ、その鍵で仏壇の引き出しを開けると、暗証番号の手がかりが出てきます。掛け軸・畳・仏壇に隠れた3つの数字を集め、出口の襖のダイヤル錠に合わせれば脱出成功です。
手順は短くしました。そのぶん、調べるたびに出る文章と、ときどき流れる環境メッセージで空気を作っています。「行灯の灯りが、わずかに揺れた。」「障子の向こうを、何かが横切った。」といった一文が、ランダムにログへ差し込まれます。怖さの大部分はこういうテキストが担っています。
鏡台は脱出には関係しません。ただ、ここを調べてからクリアすると、エンディングに一文が足されます。寄り道した人だけが見られる小さなご褒美です。
開発の流れ
コミット履歴をさかのぼると、だいたい次の順番で作っていました。
- HTML 1枚で原型を作る:部屋・調べる場所・インベントリ・ダイヤル錠を DOM と CSS で組んで、まず最後まで遊べる形にした
- 怖さを足す:BGM、1分の制限時間、ジャンプスケア(幽霊が画面に飛び出してくる演出)を追加
- 世界観に寄せる:制限時間を「残り◯秒」ではなく「午前1時57分 → 2時00分」と進む時計にした。ジャンプスケアの幽霊も写真に差し替えた
- スマホ対応:viewport の設定、
dvhのフォールバック、タップ領域の拡大、結果パネルへの自動スクロール - canvas で全面的に書き直す:ふりーむ!の規約に合わせるため(後述)
- 描画ずれの修正:アイコンや文字の位置がずれるバグを直した
実装には Claude Code を使っています。「こういう演出を足したい」「スマホで押しにくい」と伝えて、コードを書いてもらい、動かして確かめる。この往復を細かく回しました。仕様を決めるのと遊んで確かめるのは自分、実装の手はAI、という分担です。
「残り時間」を「時計」に変えた
最初の制限時間はただの60秒カウントダウンでした。これを「午前1時57分00秒から進んで、2時00分00秒になったら終わり」という時計表示に変えています。
const CLOCK_START_SEC = 1 * 3600 + 57 * 60; // 午前1時57分00秒
const CLOCK_END_SEC = 2 * 3600; // 午前2時00分00秒(丑三つ時)
仕組みはまったく同じ60秒なのに、「残り60秒」より「丑三つ時が近づいている」のほうがずっと焦ります。残り30秒になると一度ジャンプスケアが入り、時計は警告色に変わります。数字の意味を世界観に合わせるだけで体験が変わる、というのは作ってみて一番の発見でした。
BGM は音声ファイルを使わず Web Audio API で鳴らす
BGM は音源ファイルを持たず、Web Audio API で合成しています。55Hz・82.5Hz・110Hz の低い音を3つ重ねて、ローパスフィルタで角を取ります。そこにごく遅い LFO(0.06Hz)で音量をゆっくり揺らすと、「部屋そのものが低く唸っている」ような音になります。
ジャンプスケアの効果音も同じ作りです。220Hz・233Hz・415Hz のノコギリ波を一瞬だけ重ね、不協和音を鳴らしています。はじめは音が大きすぎて、自分で試していても本当に心臓に悪かったので、音量を下げるコミットを入れました。ホラーの効果音は「怖い」と「不快」の境目が近いので、ここは何度も聴いて調整しました。
ふりーむ!に出そうとして、全部作り直した話
完成したのでふりーむ!に登録しようとしたところ、ブラウザゲームとしての条件を満たしていませんでした。引っかかったのは次の3点です。
- UI が HTML タグで作られていた:ボタンや文章が DOM 要素として
<body>に並んでいた - 外部サーバーと通信していた:Google Fonts を読み込んでいた
- 画像を HTML に埋め込んでいた:幽霊の写真を base64 の data URI で HTML に直接書いていた
2と3はすぐ直せます。Google Fonts をやめて OS 標準の明朝体・ゴシック体を使い、画像は assets/ghost.jpg という別ファイルに切り出しました。
問題は1です。タイトル画面、6つの調査カード、ログパネル、インベントリ、ダイヤル錠、エンディング画面まで、すべてを <canvas> の上に JavaScript で描き直す必要がありました。最終的に index.html は <canvas> と <script> があるだけのファイルになり、代わりに game.js が約1600行になりました。
canvas で UI を作ると、ブラウザがやってくれていたことを全部自分でやることになる
書き直してみて、HTML と CSS がどれだけ多くを肩代わりしてくれていたかを実感しました。
- 当たり判定:ボタンのクリック判定がないので、描画のたびに「この矩形を押したらこの処理」を登録し、クリック座標から探す仕組みを作った
- 折り返し:日本語の文章を枠内で折り返す処理を、文字単位で自前実装した
- 字間:CSS の
letter-spacingがないので、1文字ずつ位置を計算して描いた - スクロール:ログがあふれたときのスクロールも自前
- 高解像度対応:
devicePixelRatioを見て canvas の実サイズを調整しないと、文字がにじむ
画面がリサイズされたら、レイアウトを計算し直して毎フレーム描き直します。宣言的に書けば済んでいた UI を、全部手続き的に組み直す作業でした。
書き直しで入ったバグ2つ
移植直後、見た目のずれが2つ出ました。どちらも原因が面白かったので書いておきます。
① アイコンが右下にずれる
アイコンを描く drawIcon(x, y) は、受け取った座標を「左上の角」として扱っていました。一方、呼び出す側はすべて「アイコンの中心」のつもりで座標を渡していました。その結果、すべてのアイコンがサイズの半分だけ右下にずれていたのです。関数の側を「中心座標を受け取る」に合わせて直しました。
② 中央揃えの文字が左に流れる
字間つきで文字を描く関数は、1文字ずつ「左端の x 座標」を計算して fillText を呼んでいました。ところが ctx.textAlign を自分では設定しておらず、直前の描画で残っていた "center" がそのまま効いていました。その結果、「左端として計算した位置」を基準に1文字ずつさらに中央揃えされ、文字列全体が左へずれていました。
const prevAlign = ctx.textAlign;
ctx.textAlign = "left"; // 位置はすでに1文字ずつの左端として計算済み
for (const ch of text) {
ctx.fillText(ch, cx, y);
cx += ctx.measureText(ch).width + spacing;
}
ctx.textAlign = prevAlign;
canvas のコンテキストは描画状態をずっと持ち続けます。「前に誰かが設定した状態」に知らないうちに依存してしまうのは、canvas でよくある落とし穴だと身をもって知りました。状態を変える関数は、自分で設定して自分で戻す。これを徹底するべきでした。
やってみて思ったこと
小さく完成させてから肉付けする順番はよかった。
最初の1コミットで最後まで遊べる状態にしていたので、そのあとの「怖くする」「スマホ対応」「作り直し」は、どれも動くものを壊さずに進められました。
公開先の条件は最初に確認するべきだった。
DOM で組んだ UI を canvas に移すのは、ほぼ作り直しです。最初から「ふりーむ!」に出すと決めていたなら、はじめから canvas で作ったほうが手戻りはずっと少なかったはずです。これが一番の反省です。
AI と組むと「決めること」に集中できる。
時計の見せ方、効果音の音量、どこで驚かせるか。こうした判断に時間を使えたのは、実装の多くを Claude Code に任せられたからです。一方で、アイコンのずれのような「動くけど見た目がおかしい」問題は、実際に画面を見て気づけるかどうかにかかっていました。確かめる役は人間が手放してはいけないと感じています。
おわりに
「丑三つの間」は3分ほどで遊べるので、よければ夜中に部屋を暗くして遊んでみてください。音が出るので、音量にはご注意を。
▶ https://iwatanabe-7.github.io/ushimitsu-no-ma/
ソースコードは GitHub で公開しています。canvas だけで UI を組む実装例としても、参考になればうれしいです。