JavaScriptでブラウザゲームを作る①|ビルド不要(file://)で動くシーン制の土台と全体設計【Archipelago Saga制作】

アプリの作り方

JavaScriptでブラウザゲームを作る①|ビルド不要(file://)で動くシーン制の土台と全体設計【Archipelago Saga制作】

「完成したゲームは触れた。でも、結局どう組まれてるの?」。概要だけでは作れない。この連載は、ブラウザで通しクリアできる AI製 2D JRPG「Archipelago Saga」を、実際のコードで・順を追って・自分で再現できる形に開く。第1回は土台だ。npmwebpack も使わず、index.html をダブルクリックすれば動く「無ビルド」のゲーム骨格を、まっさらな状態から組み上げる。プログラミングが得意でなくても追えるよう、専門用語はそのつど噛み砕いて進める。

▶ まず完成版をブラウザで遊ぶ(インストール不要)。これから、その土台を1から作る。全体像は作り方の入口(連載ハブ)から。

目次
  1. この連載で作るもの(全6回の地図)
  2. 第1回のゴール
  3. Step 1. なぜ「無ビルド」なのか
  4. Step 2. ディレクトリ構成
  5. Step 3. index.html = 読込順が設計図
  6. Step 4. window.G:みんなの「集合場所」を1つだけ作る
  7. Step 5. シーンマネージャ:main.js(司令塔)
  8. Step 6. シーンの「形」(守るべき1つの約束)
  9. Step 7. 最小で動く土台を、写経して動かす
    1. ① index.html
    2. ② js/data/state.js(G に“持ち物”を置く・最小版)
    3. ③ js/main.js(司令塔・最小版)
    4. ④ 2つのシーン
  10. まとめ:第1回の要点

この連載で作るもの(全6回の地図)

6地域・49ステージ・9職業・99種類の敵を持つ、ブラウザ完結の CTB(チャージタイム制/FFX風)JRPG。約1万行を、システムごとに分けて再現していく。

  1. ① 無ビルドの土台と全体設計(この記事)— index.html の読込順・window.G・シーンマネージャ
  2. ② データ駆動設計 — 職業/スキル/敵/装備/ステージを「純データ」に分離する
  3. ③ CTB 戦闘コア — DOM非依存で書く戦闘ロジック
  4. ④ ワールドマップ・シーン・セーブ — 進行管理と localStorage
  5. ⑤ AIアセット量産フロー — 生成→正規化→透過→配置
  6. ⑥ ヘッドレス検証と静的デプロイ — 「動いている」を自動で証明する

第1回のゴール

この記事を読み終えると、「タイトル画面 → 別の画面」へ切り替わる最小のゲーム土台が、ビルド工程ゼロで手元に出来上がる。以降の回(戦闘・マップ・セーブ)は、すべてこの土台の上に「載せるだけ」で増えていく。まずは骨格を体に入れるのが目的だ。完成形の全体像はこうなっている。

図0:全体像 — 1つの index.html が、すべてを束ねる index.html css/js を読み込むだけ window.G(唯一のグローバル=みんなの集合場所) data/純データ 敵・職業・ステージ… engine/仕組み 戦闘・状態・保存 scenes/画面 タイトル・戦闘…

Archipelago Saga のタイトル画面
完成版のタイトル画面。index.htmljs/** を順に読み込み、G.game.show('title') で最初のシーンを表示している。file:// でダブルクリックすれば動く。

Step 1. なぜ「無ビルド」なのか

まず言葉から。ビルドとは、人間が書いたコードを、ブラウザが動かせる形に「変換・梱包する」作業のこと(webpacknpm run build がやっているのがこれ)。多くの現代的な開発では、この変換工程が必ず挟まる。

このゲームは、その工程をまるごと省くindex.htmlcssjs のファイルを <script> タグで順番に読み込むだけ。これを「クラシックスクリプト」構成と呼ぶ。地味だが、個人で作る小〜中規模のゲームでは、これが一番事故らない。理由は3つだ。

  • file:// で動くindex.html をダブルクリックするだけで即起動。サーバーもビルドの待ち時間も要らない。書いて、保存して、再読み込み。それだけ。
  • 配布もホスティングも「置くだけ」:ファイル一式をサーバーにアップすれば公開完了。ビルド成果物や依存解決でつまずく事故が起きない。
  • 壊れる箇所が少ない:トランスパイルやバンドル設定という「自分が書いたわけじゃない部分」で詰まらない。エラーは全部、自分のコードの中にある。

代償は「ファイルを読む順番を、自分で管理する」こと。だが、その順番こそが、このゲームの設計図そのものになる。次の Step で見ていこう。

Step 2. ディレクトリ構成

フォルダは「役割」で分ける。大きく ロジック(js)見た目(css)素材(assets)の3系統だ。

game/
├── index.html          # css/js を読み込むだけ。ロジックは一切書かない
├── css/                # 見た目: base / map / battle / town / menu …
├── js/
│   ├── data/           # 中身(純データ): classes, enemies, skills, stages …
│   ├── engine/         # 仕組み: rng, storage, state, battle-core, audio …
│   ├── scenes/         # 画面: title, worldmap, battle, result, shop …
│   └── main.js         # 司令塔+起動(必ず最後に読む)
└── assets_ai/          # 素材: 立ち絵・背景・アイコン・BGM・ボイス

覚えてほしいのは js/ の3分割だけでいい。data=中身engine=仕組みscenes=画面。この「中身と仕組みを分ける」発想が、後からコンテンツを“データを書き足すだけ”で増やせる秘訣になる(第2回で深掘り)。

Step 3. index.html = 読込順が設計図

無ビルドの世界では、<script> を書いた順に、上から実行される。だからルールはひとつ。「使われる側」を先に、「使う側」を後に並べる。これを図にするとこうだ。

図1:読み込む順番が、そのまま依存の階層になる ① js/engine/rng.js 乱数など — 何にも依存しない基盤 ② js/data/* 純データ:classes / enemies / skills / stages … ③ js/engine/* storage→state→battle-core→audio(②を使う) ④ js/scenes/* title / worldmap / battle …(②③を使う) ⑤ js/main.js シーンマネージャ+起動(必ず最後) ▼ 下の段ほど多くに依存する。だから「依存される側(上)」から順に読む。

実際の index.html の末尾は、この5段がそのまま並んでいる(抜粋)。

<!-- ① 乱数など、何にも依存しない基盤 -->
<script src="js/engine/rng.js"></script>

<!-- ② 純データ(ただの定義の集まり。何にも依存しない) -->
<script src="js/data/classes.js"></script>
<script src="js/data/skills.js"></script>
<script src="js/data/enemies.js"></script>
<script src="js/data/stages.js"></script>

<!-- ③ エンジン(上のデータを使う:保存→状態→戦闘コア→音) -->
<script src="js/engine/storage.js"></script>
<script src="js/engine/state.js"></script>
<script src="js/engine/battle-core.js"></script>
<script src="js/engine/audio.js"></script>

<!-- ④ 各画面(エンジンとデータを使う) -->
<script src="js/scenes/title.js"></script>
<script src="js/scenes/worldmap.js"></script>
<script src="js/scenes/battle.js"></script>

<!-- ⑤ 最後に:司令塔+起動 -->
<script src="js/main.js"></script>

なぜこの順か、具体例で噛み砕こう。js/scenes/battle.js(戦闘画面)は、戦う相手を G.ENEMIES から読む。その G.ENEMIES を用意するのは js/data/enemies.js だ。だから敵データ(②)を先に、戦闘画面(④)を後に読まないと、画面が開いた瞬間に「相手がいない」状態になる。

💡 つまずきポイント:順番を間違えると Uncaught ReferenceError: G.ENEMIES is undefined のような実行時エラーがコンソール(F12)に出る。これは「読む順番がおかしい」というサインなので、該当ファイルを正しい段へ移すだけで直る。原因が一目で分かるのも、無ビルドの利点だ。

Step 4. window.G:みんなの「集合場所」を1つだけ作る

ビルドを使わないと、ファイル同士でデータを受け渡す“正式な仕組み(モジュール)”が無い。そこでこのゲームは、たった1つのグローバルオブジェクト window.G を「全員の集合場所」にする。各ファイルは、自分の担当を G の決まった場所に置いていくだけ。

図2:すべては window.G にぶら下がる window.G G.CLASSES / G.ENEMIES 純データ G.state 状態・進行・パーティ G.scenes 全画面の登録簿 G.audio BGM / 効果音 G.storage セーブ(localStorage) G.game 司令塔:show()

各ファイルは、先頭でこの“おまじない”から始める。

// どのファイルも、いちばん上でこの一行を書く
window.G = window.G || {};

window.G = window.G || {} を噛み砕くと、「G がすでにあるなら、それをそのまま使う。まだ無いなら、空っぽの箱 {} を新しく作る」という意味だ(|| は「左がカラ/未定義なら右を使う」という記号)。これがあるおかげで、どのファイルが先に実行されても G を壊さない。結果、各ファイルは安心して「G の決まった場所に、自分の担当を置く」だけでよくなる。

// data/classes.js は「職業」を G に置く
window.G = window.G || {};
G.CLASSES = { warrior: { name: "戦士", hp: 120 }, mage: { name: "魔法使い", hp: 70 } };

// data/enemies.js は「敵」を G に置く(別ファイル)
window.G = window.G || {};
G.ENEMIES = { slime: { name: "スライム", hp: 28 } };

// scenes/battle.js は、置かれた G.ENEMIES を“読む”だけ(別ファイル)
window.G = window.G || {};
var enemy = G.ENEMIES.slime;   // ← 受け渡しは G 経由。import は要らない

ポイントは2つ。(1) グローバル変数を撒き散らさず、汚染を G ただ1つに閉じ込められる。(2) ファイル同士が直接つながらず(疎結合)、G という共有の掲示板を介して協調する。だから後からファイルを足しても、既存に影響しにくい。

Step 5. シーンマネージャ:main.js(司令塔)

ゲームは「画面(シーン)の切り替え」の連続だ。タイトル→編成→マップ→戦闘→リザルト…。この切り替えを一手に仕切るのが main.js。やることはシンプルで、G.game.show("名前") が呼ばれるたびに、次の3手順を回すだけだ。

図3:G.game.show() が画面を切り替える3手順 G.game.show(“battle”) を呼ぶ ① 前の画面を片付ける teardown() ② 画面を空にする app.innerHTML = “” ③ 新しい画面を描く scene.mount(app) battle 画面が表示される

実物はたった30行。これがゲーム全体の心臓だ。全文を載せ、すぐ下で1ブロックずつ解説する。

/* シーンマネージャ+起動。各シーンは G.scenes[name] = { mount(app,params) -> teardown } で登録する */
window.G = window.G || {};
G.scenes = G.scenes || {};
(function () {
  var app = document.getElementById("app");   // 全画面を描く“差し替え先”の箱
  var teardown = null;                          // いま出ている画面の「後始末」関数

  G.game = {
    show: function (name, params) {
      var scene = G.scenes[name];
      if (!scene) { console.error("unknown scene:", name); return; }            // 無い画面名なら中止
      if (typeof teardown === "function") { try { teardown(); } catch (e) {} }   // ① 前の画面を片付ける
      app.innerHTML = "";                          // ② 画面をまっさらに
      app.scrollTop = 0;
      teardown = scene.mount(app, params || {}) || null;   // ③ 新しい画面を描く(戻り値=次の後始末)
      G.game.current = name;
      if (G.audio) G.audio.sceneBgm(name);         // 画面ごとのBGMへ切り替え
    }
  };

  // 起動:セーブを初期化してから、最初の画面(タイトル)を表示
  document.addEventListener("DOMContentLoaded", async function () {
    try {
      await G.storage.init();
      var slots = await G.storage.listSlots();
      G.state._hasAny = G.storage.ALL.some(function (n) { return !!slots[n]; });
    } catch (e) { console.warn("storage init failed", e); }
    G.game.show("title");   // ← ここでゲームが始まる
  });
})();

分解するとこうだ。

  • var app = document.getElementById("app"):HTMLにある <div id="app"> が、全画面の“差し替え先”。画面が変わるたび、この中身を丸ごと入れ替える。
  • teardown:いま出ている画面の「後始末関数」を覚えておく変数。タイマーやイベントを張る画面は、これで掃除する(次の Step)。
  • G.game.show(name):これが司令塔の本体。図3の3手順(①前の画面の teardown() を呼ぶ → ②#app を空にする → ③新しい画面の mount() を呼ぶ)を順に実行する。mount() の戻り値(次の後始末関数)を teardown に保存しておくのがミソ。
  • DOMContentLoaded:HTMLの読み込みが終わったら起動。セーブの仕組みを初期化してから、最初の画面 "title" を表示する。

1つの司令塔・1つの差し替え先(#appに集約するから、画面が10個に増えても20個に増えても、切り替えの仕組みはこの30行のまま変わらない。

Step 6. シーンの「形」(守るべき1つの約束)

すべての画面は、同じ“形”で書く。これだけ守ればいい。
G.scenes.名前 = { mount: function(app, params){ … 描画 …; return 後始末関数 または null } }

実際のタイトル画面(抜粋)はこう。

// scenes/title.js(抜粋)
window.G = window.G || {}; G.scenes = G.scenes || {};
G.scenes.title = {
  mount: function (app) {
    var wrap = document.createElement("div");
    wrap.innerHTML =
      '<img class="title-logo-img" src="../assets_ai/ui_title/title_logo.png" alt="Archipelago Saga">' +
      '<button class="btn" id="btnNew">▶ はじめる</button>';
    app.appendChild(wrap);

    // ボタンを押したら、司令塔に「次は編成画面」と頼む
    wrap.querySelector("#btnNew").onclick = function () {
      if (G.audio) G.audio.playSe("menu_click");
      G.game.show("party");           // ← 画面遷移は必ず show() 経由
    };

    return null;   // 後始末が要らない画面は null を返す
  }
};

大事なルールは、画面遷移は必ず G.game.show() を通すこと。シーン同士が直接呼び合わない。だから各画面は完全に独立し、1つずつ作って差し込める。

では teardown(後始末)はいつ要るのか。タイマーや、画面の外に張ったイベントを使うときだ。例えば「1秒ごとに時刻を更新する画面」を作るとこうなる。

// 後始末が必要な画面の例:setInterval を張る
G.scenes.clock = {
  mount: function (app) {
    app.innerHTML = '<h1 id="t"></h1>';
    var t = app.querySelector("#t");
    var id = setInterval(function () { t.textContent = new Date().toLocaleTimeString(); }, 1000);

    // ← この関数を返すと、次の画面に移る瞬間 main.js が呼んでくれる
    return function teardown() { clearInterval(id); };   // タイマーを止める=後始末
  }
};

もし teardownclearInterval しないと、画面を離れてもタイマーが回り続け、メモリと動作を少しずつ蝕む。「mount で張ったものは、teardown で必ず外す」。これがシーンを安全に切り替え続けるための、たった1つのお作法だ。

Step 7. 最小で動く土台を、写経して動かす

ここまでを、今すぐ動く最小形にまとめよう。必要なのは 4ファイルだけ。これが第2回以降すべての出発点になる。

index.html

<!doctype html>
<html lang="ja"><head><meta charset="utf-8"><title>My JRPG</title></head>
<body>
  <div id="app"></div>        <!-- 全画面の差し替え先 -->

  <!-- データ → 画面 → 最後に司令塔 の順で読む(=図1の縮小版) -->
  <script src="js/data/state.js"></script>
  <script src="js/scenes/title.js"></script>
  <script src="js/scenes/play.js"></script>
  <script src="js/main.js"></script>
</body></html>

js/data/state.js(G に“持ち物”を置く・最小版)

window.G = window.G || {};
G.state = { hp: 100, gold: 0 };   // ゲームの“今の状態”。これも G にぶら下げるだけ

js/main.js(司令塔・最小版)

window.G = window.G || {}; G.scenes = G.scenes || {};
(function () {
  var app = document.getElementById("app");
  var teardown = null;
  G.game = {
    show: function (name, params) {
      var scene = G.scenes[name];
      if (!scene) { console.error("unknown scene:", name); return; }
      if (typeof teardown === "function") { try { teardown(); } catch (e) {} }
      app.innerHTML = "";
      teardown = scene.mount(app, params || {}) || null;
      G.game.current = name;
    }
  };
  document.addEventListener("DOMContentLoaded", function () { G.game.show("title"); });
})();

④ 2つのシーン

// js/scenes/title.js
window.G = window.G || {}; G.scenes = G.scenes || {};
G.scenes.title = {
  mount: function (app) {
    app.innerHTML = '<h1>My JRPG</h1><button id="go">▶ はじめる</button>';
    app.querySelector("#go").onclick = function () { G.game.show("play"); };
    return null;
  }
};

// js/scenes/play.js
window.G = window.G || {}; G.scenes = G.scenes || {};
G.scenes.play = {
  mount: function (app) {
    // G.state を“読む”:別ファイルで置いた持ち物が、ここで使える
    app.innerHTML =
      '<h1>冒険スタート!</h1>' +
      '<p>HP: ' + G.state.hp + ' / Gold: ' + G.state.gold + '</p>' +
      '<button id="get">💰 10ゴールド拾う</button> ' +
      '<button id="back">← タイトルへ</button>';
    app.querySelector("#get").onclick = function () {
      G.state.gold += 10;          // 状態を書き換えて…
      G.game.show("play");         // …同じ画面を描き直す(再表示で反映)
    };
    app.querySelector("#back").onclick = function () { G.game.show("title"); };
    return null;
  }
};

この4ファイルを構成どおり置いて、index.html をダブルクリックする。ビルドなしで「タイトル⇄プレイ」が切り替わり、ボタンでゴールドが増える。たった今あなたは、window.G(集合場所)・読込順(設計図)・シーン契約・司令塔・状態の受け渡し、というこのゲームの背骨を全部動かしたことになる。

🔎 動かないとき:F12 でコンソールを開く。G.state is undefined なら state.js の読込が play.js より後になっている(=順番ミス)。unknown scene なら show() に渡した名前と G.scenes.◯◯ の名前が食い違っている。エラー文がそのまま原因を教えてくれる。

まとめ:第1回の要点

  • 無ビルドindex.html<script> を順に読むだけ。file:// で動き、配布は置くだけ。
  • 読込順=設計図:rng →(純データ)→ engine → scenes → main の5段。下ほど依存が多い。
  • 単一グローバル window.G:全員の集合場所。各ファイルは G に担当を置くだけ。汚染は G 1つに閉じる。
  • シーンマネージャG.game.show() が teardown→clear→mount を回す。画面は同じ契約で1つずつ足せる。
  • 後始末(teardown):mount で張ったタイマー/イベントは、teardown で必ず外す。

次回(第2回)は、この G に載せる「純データ」設計へ。職業・スキル・敵・装備・ステージを“ただのデータ”に切り出し、Object.assign の拡張ファイル方式で「コンテンツ追加=データを書き足すだけ」にする仕組みを、実データのスキーマつきで開く。Step 7 で置いた G.state が、本物ではどう育つのかも見ていく。

AI生成コンテンツの開示:本作の BGM・効果音・キャラクター画像・ボイスは、すべて生成AIの出力物です(BGM=ACE-Step 1.5〈MIT・出力の商用利用可を配布元モデルカードで確認済み〉 / ボイス=Irodori-TTS 系の合成設計音声・実在人物の複製ではありません / 効果音=数値合成 / 画像=画像生成AI〈各サービスの利用条件で出力の商用利用可を確認のうえ使用。透過処理は BiRefNet〈MIT〉を rembg 経由で使用〉)。第三者の著作物は同梱していません。© 2026 Cooliris(本作のコード・構成・データの権利は作者に帰属します)。


👉 ▶ 完成版を遊ぶ連載ハブ(全体像)開発実例:Archipelago Sagaほかのアプリの作り方

作ったアプリを公開する|ローカルから人に使ってもらうまでの最短ルート【入門7】作ったアプリを公開する|ローカルから人に使ってもらうまでの最短ルート【入門7】前のページ

JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】次のページJavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

ピックアップ記事

  1. 高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyIns…

  2. Claude Mission Control の作り方 — Tauri+Pyth…

  3. New Eden Intelligence Hub の作り方 — EVE Onl…

  4. 競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド…

関連記事

  1. 背景除去アプリを作る⑤|5スライダー後処理+消去/復元ブラシ+20段Undoで「自動9割・手で1割」を仕上げる【背景除去Studio制作】

    アプリの作り方

    背景除去アプリを作る⑤|5スライダー後処理+消去/復元ブラシ+20段Undoで「自動9割・手で1割」…

    完全自動を狙うより、自動+微修正の方が実用品質に早く届く。しきい値・ソ…

  2. large-v3-turbo は large-v3 の代わりになるか。日本語で測って、速度の測り方を2回間違えた記録
  3. ゲーム感覚でIT・AI用語500選を覚える学習アプリの作り方 — XP・13ランク・実績で「続く」を設計する
  4. JavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipela…

    FFX風のCTB(チャージタイム制)戦闘を、画面(DOM)から完全に切…

  5. 都市開発シミュを作る④|4度つまずいて座標系ごと作り直した話 — 真のアイソメグリッド移行【Aurum City制作】
  6. JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増や…

    ゲームのボリュームは「コード」でなく「データ」で増やす。職業・スキル・…

注目

AIで、ここまで作れる

AIで作った2D RPGを、ブラウザでそのまま遊べます。その「作り方=最後まで完成させる進め方」も実例つきで公開中。

▶ ゲームを遊ぶやり方を読む

PR

お名前.com 独自ドメイン取得(PR)

独自ドメイン:お名前.com(本サイトで使用・PR)

  1. WordPress 7.0.2 の緊急修正を読む — batch API の添字ズレが RCE に化けた仕組みと、自作プラグインの点検

    AI・テック動向

    WordPress 7.0.2 の緊急修正を読む — batch API の添字…
  2. アーティファクトがライブになった代わりに、配れなくなった — MCP コネクタ対応の実際

    AI・テック動向

    アーティファクトがライブになった代わりに、配れなくなった — MCP コネクタ対…
  3. JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“デ…
  4. アプリの作り方

    Claude Codeのトークン消費を減らす実践ガイド — 公式ドキュメントで裏…
  5. 競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド

    アプリの作り方

    競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド…
PAGE TOP

TAG CLOUD

ドラッグで回転・クリックでそのタグの記事一覧へ