都市開発シミュを作る⑤|「面白くない」を目標・施設・イベント・音の4軸で解消【Aurum City制作】

アプリの作り方

都市開発シミュを作る⑤|「面白くない」を目標・施設・イベント・音の4軸で解消【Aurum City制作】

見た目は整った。街は育ち、建物は正しく並ぶ。だが、遊んでみるとこう感じる。「作れるけど、面白くない」。自分で動かして、はっきりそう思った。不満は4つの軸に整理できた。①目標・達成感がない ②打ち手が少ない ③変化・驚きがない ④音がない。第5回は、この4つを1つずつ埋めていく。ポイントは、すでに完成しているシミュレーションを1行も壊さずに、新しいシステムを積み重ねること。それを可能にする設計のコツ(専用の乱数系列と機能フラグ)も開く。

▶ この連載の全記事はバイブコーディング開発実例(一覧)から。「動く」の次にある「面白い」を、どう設計で作るかの回だ。

目次
  1. この連載で作るもの(全6回の地図)
  2. 第5回のゴール
  3. Step 1. ①目標:都市ランクとミッション
  4. Step 2. ②打ち手:サービス施設と幸福度
  5. Step 3. 通勤・交通:道路に「意味」を足す
  6. Step 4. ③変化:ランダムイベントとニュース速報
  7. Step 5. 既存を壊さず積む:専用の乱数系列と機能フラグ
  8. Step 6. ④音:sim を知らない音声ファサード
  9. 設計Q&A:フラグとバランスの疑問
  10. まとめ:第5回の要点

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

  1. ① 土台と3レイヤ設計 — sim / render / ui の分離・グリッド・Canvas描画
  2. ② AIでドット絵タイルを作る — 生成→均一化→アルファ実測→組み込み、道路の自動接続
  3. ③ 街を「動かす」— 需要・成長・経済の決定論シミュレーション(MVP完成)
  4. ④ 4度つまずいて座標系ごと作り直した話 — 真のアイソメグリッドへの全面移行
  5. ⑤「面白くない」を4軸で解消する(この記事)— 目標・施設・イベント・音
  6. ⑥ 静的ビルドをWordPressへ公開する — 検証と配信、そして連載の総括

第5回のゴール

この記事を読み終えると、都市ランクとミッション(目標)、サービス施設と幸福度(打ち手)、通勤・交通、ランダムイベントとニュース速報(変化)、BGMと効果音が、既存のシミュを壊さずに積み上がる。まず「目標」から。人口で街が昇格し、ミッションを達成すると資金がもらえる。

都市ランクバー(町 520/1,500)と昇格バナー『町に昇格!+$1,000』、ミッション達成トースト、右上にアクティブミッション3件のパネル。
①目標:人口で昇格する都市ランク(上部バー)と、達成で資金がもらえるミッション(右)。昇格・達成の瞬間はバナーとトーストで祝う。「達成感」を作る仕掛けだ。

Step 1. ①目標:都市ランクとミッション

まず作ったのが「目標」だ。理由は、ミッションは後から足す新要素を”目標”として取り込める器だから。ランクは人口の閾値で昇格する(降格はしない)。値は実際にプレイして測った数字で凍結する。

// src/sim/goals.ts(純関数・乱数なし)
export const RANKS = [
  { name: '開拓地', popThreshold: 0,    rewardFunds: 0 },
  { name: '村',     popThreshold: 150,  rewardFunds: 500 },
  { name: '町',     popThreshold: 500,  rewardFunds: 1000 },
  { name: '市',     popThreshold: 1500, rewardFunds: 2000 },
  { name: '大都市', popThreshold: 3500, rewardFunds: 4000 },
];
export function rankIndex(population: number): number {
  let i = 0;
  for (let r = 1; r < RANKS.length; r++) if (population >= RANKS[r].popThreshold) i = r;
  return i;
}

ミッション(例:「人口800人を達成」「純収支+$100/tickを達成」)は、World が組み立てたスナップショットを受け取って判定する純関数。goals.tsWorld を知らない(第1回の層分離の作法)。だから「達成条件」だけを、乱数なしで確実にテストできる。ミッションの id はセーブに残るので改名禁止。ここは後の全フェーズで守り続けたルールだ。

Step 2. ②打ち手:サービス施設と幸福度

次は「打ち手」。プレイヤーが街に働きかける手段を増やす。公園(1×1・安い・半径小)/学校(2×1・中)/病院(2×2・高い・半径大)を置くと、そのカバレッジ内の建物の幸福度が上がる。幸福度は高密度化を加速し、税収にボーナスを与える。罰ではなく、ご褒美で誘導する設計だ(「緊張感より達成感」の方針)。

カバレッジは事前計算する。施設の効果範囲(チェビシェフ距離)をマップに焼いておき、毎tickの判定を軽くする。施設もまた独立したレジストリと純関数で、既存のシミュに割り込まない。

消防(赤)と警察(青)のカバレッジ範囲をひし形で重ね表示。下部に流れるニュース速報。ドックに公園・学校・病院・消防・警察・カバーのツール。
②打ち手:施設のカバレッジをオーバーレイ表示(ここは消防=赤/警察=青の範囲)。下部は③変化のニュース速報が流れている。ドックに公園・学校・病院・消防・警察・交通の打ち手が並ぶ。

Step 3. 通勤・交通:道路に「意味」を足す

打ち手の一環として、通勤・交通も入れた。純関数 traffic.ts が2つの層で働く。(1) ゲームルール:住宅から道路を辿って到達できる職(effectiveJobs)だけが住宅需要に効く。橋を1本落として街を分断すると、有効な職が激減し、需要が崩れて経済崩壊のカスケードが起きる。(2) 可視化:住宅→職場のルートを道路に積算し、負荷をヒートマップ(緑→琥珀→赤)で見せる。完全につながった街では、この交通層は既存のバランスに影響しない(=互換)。

Aurum City のゲームプレイ画面(目標・施設カバー)
ゲームプレイ層。右上に都市ランクとミッション、下部にニュース速報が流れる。カバー表示(トグル)でサービス施設の到達範囲が見え、幸福度に効く打ち手が分かる。

Step 4. ③変化:ランダムイベントとニュース速報

「変化・驚き」は、ランダムイベントで作る。好況・慶事・疫病・火災・犯罪。効果は数種類(一時金・需要バイアス・税率倍率・建物損壊)に絞り、カタログを唯一の真実にする。災害はランク2以上かつ建物15以上でのみ発火する(序盤に理不尽が来ない)。

// src/sim/events.ts(カタログ抜粋・id はセーブに残るので改名禁止)
export const EVENTS = [
  { id: 'ev_invest_boom', category: 'boom', headline: '投資ブームが到来',
    weight: 10, minRank: 1, durationTicks: 30, effect: { demandBias: { C: 30, I: 30 }, taxMul: 1.1 } },
  { id: 'ev_fire', category: 'fire', headline: '{area}で火災が発生',
    weight: 5, minRank: 2, minBuildings: 15, durationTicks: 0,
    effect: { destroy: 2, shield: 'fire' } },   // shield=消防カバーが損壊を防ぐ
  // …疫病(shield:hospital)・犯罪(shield:police)・慶事・好況…
];

ここで Step 2 の施設が「打ち手」として効いてくる。火災は消防のカバー外の建物だけを壊し(shield: 'fire')、疫病は病院、犯罪は警察のカバー率でダメージが和らぐ。先に打ち手(施設)を用意してから、変化(イベント)を入れた。順序に意味がある。イベントの見出しは、画面下部を流れるニュース速報(ティッカー)に、カテゴリ色つきで出る。

Step 5. 既存を壊さず積む:専用の乱数系列と機能フラグ

ここが今回いちばんの設計のコツだ。第3回で、成長シミュは決定論(同じ種→同じ結果)で、その正しさをテストで凍結した、と書いた。イベントを足すとき、うっかり同じ乱数系列を使うと、乱数の消費順が変わって成長の結果まで変わってしまう(凍結したテストが全部壊れる)。

そこで、イベント専用の乱数系列を別に持つ。種を seed ^ 0x9e3779b9 でずらし、成長用の this.rng とは絶対に混ぜない。さらに enableEvents という機能フラグを持ち、テストはフラグを off にして、コアのシミュがイベント導入前とビット単位で一致することを機械的に検証する。

// src/sim/world.ts
constructor(grid, seed = 1, funds = STARTING_FUNDS, opts?: { enableEvents?: boolean }) {
  this.rng = mulberry32(seed);                          // 成長用(第3回から不変)
  this.enableEvents = opts?.enableEvents ?? true;
  this.eventRng = mulberry32((seed ^ 0x9e3779b9) >>> 0); // イベント専用の別系列
}
// イベント処理は this.rng を絶対に使わない(eventRng のみ)。
// enableEvents=false ならイベントは一切走らず、コア挙動は導入前と完全一致。

💡 再利用できる設計知見:既存の「凍結した正しさ」を壊さずに新機能を足すには、①乱数系列を分ける ②機能フラグで off にできる ③off のとき旧挙動とビット一致するテストを持つ。この3点セットがあれば、大きなサブシステム(イベント・交通)を後付けしても、土台のシミュは無傷のまま検証できる。目標・施設・イベントを次々に積めたのは、この規律のおかげだ。

Step 6. ④音:sim を知らない音声ファサード

最後は「音」。効果音(配置・撤去・達成・昇格・イベント)は数値からその場で合成(Web Audio)、BGMは生成AIで作った2曲をランクに応じてクロスフェードする。ここでも層を守る。音声は ui 層のファサードで、sim を1行も変えない。App が「操作」や「イベント」を観測して音声メソッドを呼ぶだけだ。

// src/audio/engine.ts
/** UI 層の音声ファサード。sim を知らず、App がイベント/操作を観測して呼ぶ。
 *  未対応/未 unlock/ミュート時は全メソッド無音で正常 return(例外を投げない)。 */
export class AudioEngine {
  /** 最初のユーザー操作で呼ぶ。AudioContext を生成/resume し BGM を開始。二重呼び出しは no-op。 */
  unlock(): void { /* ブラウザの自動再生規制に従い、初回クリック/キーで解錠 */ }
}

全メソッドが「失敗しても無音で正常に返る」のが肝だ。音声ファイルが無くても、ブラウザが未対応でも、ミュートでも、ゲームは一切落ちない。BGMは商用利用可を検証したうえで、生成AI(ACE-Step 1.5・MIT。モデルカードが出力の商用利用を明示しており 2026-07-04 に一次確認済み)で作った。これで不満4軸すべて(目標・打ち手・変化・音)が埋まった。

設計Q&A:フラグとバランスの疑問

Q. enableEvents のような機能フラグは、完成したら消していい?
A. 消さない方がいい。このフラグは「off にすればコアが旧挙動とビット一致する」というテストの土台で、消した瞬間にその検証手段も消える。次の機能を足すときも、また同じ3点セット(別乱数・フラグ・一致テスト)を使うので、フラグは開発の足場として恒久的に残す。実行時のコストはif文1つぶんしかない。

Q. イベントの weight(出やすさ)はどう決める?
A. 体感からの逆算で決める。「好況はたまに来ると嬉しい、災害は忘れた頃に来ると怖い」なら、好況の weight を災害の2倍にして、実際に30分遊んで頻度の体感を確かめる。数字そのものに正解はなく、「遊んでいて理不尽と感じないか」が唯一の判定基準だ。序盤に災害が来ない条件(ランク2以上・建物15以上)を付けたのも、数値でなくプレイ体感からの要請だった。

まとめ:第5回の要点

  • ①目標:人口で昇格する都市ランク+達成で報酬のミッション。純関数・id改名禁止。新要素を取り込む器として最初に作る。
  • ②打ち手:施設(公園/学校/病院)のカバレッジで幸福度→密度化・税収をご褒美で誘導。罰は使わない。
  • 交通:到達できる職だけが需要に効く+渋滞ヒートマップ。分断すると経済崩壊。完全連結なら旧バランスと互換。
  • ③変化:ランダムイベント+ニュース速報。災害は施設カバー外だけを壊す(打ち手→変化の順)。
  • 壊さず積む設計専用乱数系列+機能フラグ+off時ビット一致テスト。凍結した正しさを守りながら拡張。
  • ④音:sim非依存の音声ファサード。全メソッド無音フォールバックで絶対に落ちない。

次回(第6回・最終回)は、この完成した街を世界に出す。バックエンド無しの静的ビルドを作り、WordPressのサイトへ公開する手順、そして「動いている」をどう機械的に証明するか(246個のテスト・型チェック・実機検証)を扱う。あわせて、全パネルをドラッグで動かせるUIなどの仕上げと、この連載の4度の失敗も含めた全工程の総括をする。

AI生成コンテンツの開示:BGMは生成AI(ACE-Step・商用利用可を検証のうえ使用)、効果音は数値合成、建物・地形はこれまでの回の生成アセットです。掲載コードは実際のソースからの抜粋です。© 2026 Cooliris(記事本文と掲載コードの権利は作者に帰属します)。


👉 ▶ この連載の続き(バイブコーディング開発実例)ほかのゲーム開発の実例いろんなアプリの作り方

Claude Cowork がデバイス横断 — 端末を閉じても続くリモート実行Claude Cowork がデバイス横断 — 端末を閉じても続くリモート実行前のページ

Anthropic「Reflect」— AI利用の可視化を自動化の材料にする次のページAnthropic「Reflect」— AI利用の可視化を自動化の材料にする

ピックアップ記事

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

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

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

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

関連記事

  1. 都市開発シミュを作る④|4度つまずいて座標系ごと作り直した話 — 真のアイソメグリッド移行【Aurum City制作】
  2. はじめての“動くアプリ”を1本作る|メモ帳をステップバイステップで【入門4】

    バイブコーディング

    はじめての“動くアプリ”を1本作る|メモ帳をステップバイステップで【入門4】

    コードを1行も書かず、AIに頼んで出てきたHTMLを保存・ダブルクリッ…

  3. ゲーム感覚でIT・AI用語500選を覚える学習アプリの作り方 — XP・13ランク・実績で「続く」を設計する
  4. Grokで回すXメディア生成パイプラインの作り方 — 検索から画像・音声・動画まで自動化する
  5. バイブコーディングのやり方 — AIで「動くアプリ」を最後まで完成させる進め方を、実例7本で全部見せる
  6. Transcription Studio の作り方 — Whisper をローカルで走らせ、エンジンを切り替えて文字起こしする

    アプリの作り方

    Transcription Studio の作り方 — Whisper をローカルで走らせ、エンジン…

    音声/動画をドロップするとローカルの Whisper で文字起こしする…

注目

AIで、ここまで作れる

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

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

PR

ロリポップ!レンタルサーバー(PR)

レンタルサーバ:ロリポップ!(本サイトの稼働環境・PR)

  1. 正解が分かる音声を自分で作って、日本語ASRを測る(そして、その限界)

    アプリの作り方

    正解が分かる音声を自分で作って、日本語ASRを測る(そして、その限界)
  2. WordPress 7.1 Beta 1 公開 — 8月19日の iframe 化前に、自サイトのブロックを1行で棚卸しする

    AI・テック動向

    WordPress 7.1 Beta 1 公開 — 8月19日の iframe …
  3. ゲーム感覚でIT・AI用語500選を覚える学習アプリの作り方 — XP・13ランク・実績で「続く」を設計する

    アプリの作り方

    ゲーム感覚でIT・AI用語500選を覚える学習アプリの作り方 — XP・13ラン…
  4. 背景除去アプリを作る③|「囲まれた背景」と「アイコンの穴」— 相反する2つの難所を実測で解く【背景除去Studio制作】

    アプリの作り方

    背景除去アプリを作る③|「囲まれた背景」と「アイコンの穴」— 相反する2つの難所…
  5. Claude API が会話途中の system メッセージに対応 — キャッシュを壊さず方針を差し替える

    AI・テック動向

    Claude API が会話途中の system メッセージに対応 — キャッシ…
PAGE TOP

TAG CLOUD

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