「完成したゲームは触れた。でも、結局どう組まれてるの?」。概要だけでは作れない。この連載は、ブラウザで通しクリアできる AI製 2D JRPG「Archipelago Saga」を、実際のコードで・順を追って・自分で再現できる形に開く。第1回は土台だ。npm も webpack も使わず、index.html をダブルクリックすれば動く「無ビルド」のゲーム骨格を、まっさらな状態から組み上げる。プログラミングが得意でなくても追えるよう、専門用語はそのつど噛み砕いて進める。
▶ まず完成版をブラウザで遊ぶ(インストール不要)。これから、その土台を1から作る。全体像は作り方の入口(連載ハブ)から。
目次
この連載で作るもの(全6回の地図)
6地域・49ステージ・9職業・99種類の敵を持つ、ブラウザ完結の CTB(チャージタイム制/FFX風)JRPG。約1万行を、システムごとに分けて再現していく。
- ① 無ビルドの土台と全体設計(この記事)—
index.htmlの読込順・window.G・シーンマネージャ - ② データ駆動設計 — 職業/スキル/敵/装備/ステージを「純データ」に分離する
- ③ CTB 戦闘コア — DOM非依存で書く戦闘ロジック
- ④ ワールドマップ・シーン・セーブ — 進行管理と
localStorage - ⑤ AIアセット量産フロー — 生成→正規化→透過→配置
- ⑥ ヘッドレス検証と静的デプロイ — 「動いている」を自動で証明する
第1回のゴール
この記事を読み終えると、「タイトル画面 → 別の画面」へ切り替わる最小のゲーム土台が、ビルド工程ゼロで手元に出来上がる。以降の回(戦闘・マップ・セーブ)は、すべてこの土台の上に「載せるだけ」で増えていく。まずは骨格を体に入れるのが目的だ。完成形の全体像はこうなっている。

index.html が js/** を順に読み込み、G.game.show('title') で最初のシーンを表示している。file:// でダブルクリックすれば動く。Step 1. なぜ「無ビルド」なのか
まず言葉から。ビルドとは、人間が書いたコードを、ブラウザが動かせる形に「変換・梱包する」作業のこと(webpack や npm run build がやっているのがこれ)。多くの現代的な開発では、この変換工程が必ず挟まる。
このゲームは、その工程をまるごと省く。index.html が css と js のファイルを <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> を書いた順に、上から実行される。だからルールはひとつ。「使われる側」を先に、「使う側」を後に並べる。これを図にするとこうだ。
実際の 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 の決まった場所に置いていくだけ。
各ファイルは、先頭でこの“おまじない”から始める。
// どのファイルも、いちばん上でこの一行を書く
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手順を回すだけだ。
実物はたった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); }; // タイマーを止める=後始末
}
};
もし teardown で clearInterval しないと、画面を離れてもタイマーが回り続け、メモリと動作を少しずつ蝕む。「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に担当を置くだけ。汚染はG1つに閉じる。 - シーンマネージャ:
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(本作のコード・構成・データの権利は作者に帰属します)。
