都市開発シミュを作る②|AIでドット絵タイルを生成+道路オートタイリング【Aurum City制作】

アプリの作り方

都市開発シミュを作る②|AIでドット絵タイルを生成+道路オートタイリング【Aurum City制作】

前回、地図は「色を塗ったブロック」でしかなかった。緑・青・茶色の四角。ここにAIで作ったドット絵タイルを貼ると、同じマップが一気に「絵」に変わる。第2回のテーマは2つ。(1) 画像生成AIで地形タイルを作り、ゲームに組み込む量産フロー。(2) 道路を1本ずつ置くだけで、交差点もカーブも自動でつながる「オートタイリング」。AIにアセットを任せるのがバイブコーディングらしいところだが、任せっぱなしにはしない。採用するかどうかは見た目でなく数値で決める。その線引きも実演する。

▶ この連載の全記事はバイブコーディング開発実例(一覧)から。第1回(土台と3レイヤ設計)を読んでいなくても、この回だけで追えるように書いている。

目次
  1. この連載で作るもの(全6回の地図)
  2. 第2回のゴール
  3. Step 1. AIに地形タイルを生成させる
  4. Step 2. 「均一化」:生成物をそのまま使わない
  5. Step 3. アトラスに載せる(未ロードでもクラッシュさせない)
  6. Step 4. ゾーニングと「排他セル」(住宅・商業・工業)
  7. Step 5. 道路が自動でつながる(オートタイリング)
  8. まとめ:第2回の要点

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

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

第2回のゴール

この記事を読み終えると、草地・水・山のドット絵タイルがAIで生成され、寸法が揃い、地図に貼られている。さらに道路を置けば、隣り合う道路を見て直線・カーブ・T字・十字が自動で選ばれる。住宅(R)・商業(C)・工業(I)のゾーニングと、間違えたときの撤去(ブルドーザー)もそろう。まず、タイル1枚が出来上がるまでの流れを見てほしい。

図1:1枚のタイルが出来るまで(生成は自動、採否は数値で) ① 生成 Seedream(AI) ② 縁を削る 継ぎ目対策 ③ 32×32へ NEAREST縮小 ④ 検証 数値+目視 ⑤ 組込 atlas 生成(①)はAIに任せる。だが「採用してよいか」は④の数値検証で決める。ここが崩さない線引き。

Step 1. AIに地形タイルを生成させる

タイルの生成には、画像生成モデル SeedreamOpenRouter 経由のAPIで呼ぶ。ローカルに重いモデルを置かず、1枚あたり数円で試せるのが利点だ。ここではプロンプトの型を固定する。3種の地形で共通の「スタイル指定」を先に決め、地形名だけを差し替える。こうすると3枚のタイルの画風がそろう。

# tools/asset_gen/generate_terrain.py(抜粋)
STYLE = (
    "top-down orthographic single terrain tile for a 2D city-building game, "
    "16-bit pixel-art, seamless texture filling the whole square frame, "
    "no border, no frame, no text, no shadow, flat even lighting, centered, "
)
PROMPTS = {
    "plain":    STYLE + "lush green grassland / plains",
    "water":    STYLE + "calm blue water surface",
    "mountain": STYLE + "grey rocky mountain terrain",
}

ポイントは、共通スタイルに top-down(真上から)・seamless(敷き詰めても継ぎ目が出ない)・no border, no frame, no shadow(枠や影を描かせない)を明記していること。ゲームのタイルは同じ絵を何百マスも並べるので、影や額縁が付くと繰り返しがバレる。「1枚の絵」ではなく「敷き詰める素材」を作らせる、という指定だ。

Step 2. 「均一化」:生成物をそのまま使わない

生成された画像は、そのままではゲームに使えない。サイズはバラバラだし、モデルはよくタイルの縁をうっすら暗くする(=敷き詰めると格子状の継ぎ目が出る)。そこで、採用前に必ず2つの後処理を通す。(1) 四辺を少し内側に切り落として縁を捨てる。(2) ドット絵がボケないよう NEAREST(最近傍法)で 32×32 px に縮小する。

INSET = 0.14  # 四辺から14%ずつ切り落とす。モデルが描く暗い縁を捨て、
              # 敷き詰めたときの「格子状の継ぎ目」を防ぐため。

def to_tile(raw_path, out_path):
    img = Image.open(raw_path).convert("RGB")
    w, h = img.size
    s = min(w, h)
    cx0, cy0 = (w - s) // 2, (h - s) // 2
    d = int(s * INSET)
    img = img.crop((cx0 + d, cy0 + d, cx0 + s - d, cy0 + s - d))  # 中央を正方形で切り、さらに縁を削る
    img = img.resize((TILE, TILE), Image.NEAREST)                 # ドット絵はNEARESTでくっきり縮小
    img.save(out_path)

INSET = 0.14(四辺14%)という数字の決め方も書いておく。これは理論値ではない。刻み方は機械的で、4%→8%→14%と段階を作り、それぞれのタイルを実際に敷き詰めたプレビューを並べて、格子状の継ぎ目が消える最小の値を採った。削るほど継ぎ目は消えるが、テクスチャの模様も痩せていく。「消える最小で止める」が調整の型だ。使う生成モデルや画風が変われば最適値も変わるので、持ち帰る価値があるのはこの決め方の方だ。

💡 「見た目でOK」と言わない:生成アセットの採否は、サムネイルを眺めて決めない。地形タイルなら「四隅の色が想定どおりか」「継ぎ目が出ないか」を、拡大やヒストグラムで数値として確かめる。特に透過が絡むアセット(後の回で出てくる建物)は、アルファ値を実測しないと「背景が残っている」「端が欠けている」を見逃す。この連載でいちばん高くついた失敗の多くは、ここを目視で済ませたときに起きた。

Step 3. アトラスに載せる(未ロードでもクラッシュさせない)

出来たタイルは tiles.json(タイル名→ファイル名の対応表)にまとめ、TileAtlas が読み込む。ここで守る契約がひとつある。まだ読み込めていないタイルを要求されても、落ちずに null を返す。描画側は null が返ってきたら「フォールバックの色」で塗ればいい。画像の読み込みは非同期なので、この保険が無いと起動直後に一瞬崩れる。

// src/render/atlas.ts(抜粋)
export class TileAtlas implements TileImageSource {
  private images = new Map<string, { img: HTMLImageElement; ready: boolean }>();

  load(): void {
    for (const [key, file] of Object.entries(this.manifest)) {
      const img = this.imageFactory();
      const entry = { img, ready: false };
      img.onload  = () => { entry.ready = true; };   // 読めたら ready
      img.onerror = () => { entry.ready = false; };  // 失敗しても落とさない
      img.src = this.baseUrl + file;
      this.images.set(key, entry);
    }
  }

  imageFor(key: string): CanvasImageSource | null {
    const e = this.images.get(key);
    return e && e.ready ? e.img : null;   // 未ロードは null → 呼ぶ側は色でフォールバック
  }
}

これで地形が「色」から「絵」になった。草地には草のテクスチャ、水には水面、山には岩肌が乗る。

AI生成のドット絵タイルが貼られた地図。緑の草地テクスチャ全面、左上に水、中央に岩肌の山、ゴールドのホバー枠。
色ブロックだった地形が、AI生成のドット絵タイルに置き換わった。草地・水(左上)・山(中央)。前回のフラットな色との差は大きい。

Step 4. ゾーニングと「排他セル」(住宅・商業・工業)

地形の上に、街の要素を置いていく。道路・住宅(R)・商業(C)・工業(I)だ。ここで設計上の判断がひとつ。1つのマスに「道路であり同時に住宅でもある」状態はあり得ないので、それを型で表現不能にする。セルは content という1つの値だけを持ち、'empty' | 'road' | 'R' | 'C' | 'I' のどれか1つになる。新しく何かを置けば、前の内容は上書きされる(排他)。

// src/sim/commands.ts(純関数・テストしやすい)
// 道路・ゾーンは平地にだけ置ける(水・山は建設不可)
function buildable(grid: Grid, x: number, y: number): boolean {
  return grid.inBounds(x, y) && grid.get(x, y).terrain === 'plain';
}

export function placeZone(grid: Grid, x: number, y: number, kind: Zone): boolean {
  if (!buildable(grid, x, y)) return false;   // 置けないなら false(例外にしない)
  grid.get(x, y).content = kind;              // 排他:道路や別ゾーンを上書き
  return true;
}
export function placeRoad(grid: Grid, x: number, y: number): boolean {
  if (!buildable(grid, x, y)) return false;
  grid.get(x, y).content = 'road';
  return true;
}
export function bulldoze(grid: Grid, x: number, y: number): boolean {
  if (!grid.inBounds(x, y)) return false;
  grid.get(x, y).content = 'empty';           // 撤去は地形を変えず、内容だけ空に戻す
  return true;
}

この commands.tsGrid を受け取って書き換えるだけの純関数で、Reactもcanvasも知らない(第1回の sim 層の作法)。だから「平地にしか置けない」「置けないときは false を返す(例外を投げない)」といったルールを、単体テストで細かく守れる。

Step 5. 道路が自動でつながる(オートタイリング)

今回の山場。道路タイルは、直線・カーブ・T字・十字の4種類あればいい。あとは「隣の4マスのうち、どこが道路か」を見て、正しい形を自動で選び、90°単位で回す。これがオートタイリングだ。

まず、上(N)・右(E)・下(S)・左(W)の4方向を 1・2・4・8 のビットに割り当て、隣接する道路を1つの数(0〜15)にまとめる。

// src/sim/autotile.ts
export const N = 1, E = 2, S = 4, W = 8;   // 4方向をビットに

export function roadMask(grid: Grid, x: number, y: number): number {
  const isRoad = (cx, cy) => grid.inBounds(cx, cy) && grid.get(cx, cy).content === 'road';
  let m = 0;
  if (isRoad(x, y - 1)) m |= N;   // 上が道路
  if (isRoad(x + 1, y)) m |= E;   // 右が道路
  if (isRoad(x, y + 1)) m |= S;   // 下が道路
  if (isRoad(x - 1, y)) m |= W;   // 左が道路
  return m;                        // 0〜15 の値になる
}

この 0〜15 の16通りを、「どの基本タイルを・何度回すか」に対応させる表を持つ。4種類の絵 × 90°回転で、16通りすべてを表現できるのがミソだ(=タイルは4枚で足りる)。

// rot 0 の向き:straight=縦, curve=N+E(右上), tee=N+E+S(左が開く), cross=十字
const TABLE = {
  [N | S]:         { base: 'straight', rot: 0 },    // 縦の直線
  [E | W]:         { base: 'straight', rot: 90 },   // 横の直線(縦を90°回す)
  [N | E]:         { base: 'curve', rot: 0 },
  [E | S]:         { base: 'curve', rot: 90 },
  [S | W]:         { base: 'curve', rot: 180 },
  [N | W]:         { base: 'curve', rot: 270 },
  [N | E | S]:     { base: 'tee', rot: 0 },
  [N | E | S | W]: { base: 'cross', rot: 0 },
  // …端(1接続)・孤立(0接続)は直線のスタブで代用…
};
export function roadShape(mask: number) { return TABLE[mask & 0b1111]; }

🔎 なぜ道路だけAIで4枚描かせなかったか:実は、AIに「T字路」「カーブ」を単体で描かせると、道幅や接続位置が毎回ズレてタイル同士がつながらない。そこで採った手が、十字路(cross)だけをAIで生成し、腕を1本ずつ消して直線・カーブ・T字を派生させる方式。こうすれば全4種の道幅が完全に一致し、接続が必ず合う。「AIに全部描かせる」より「1枚だけ生成して、残りは手続きで作る」ほうが確実。この連載で何度も出てくる判断の、最初の1例だ。

回転は ctx.rotate で描画時に適用する。imageSmoothingEnabled = false(第1回)のおかげで、90°回してもピクセルは1つもボケない。結果、道路を1本引くだけで交差点やカーブが勝手に整う。

道路が自動で交差点・カーブ・直線につながり、区画の中に住宅・商業・工業のゾーンタイルが塗られた地図。撤去ツール選択中。
道路を引くと、直線・カーブ・T字・十字が自動で選ばれてつながる。区画の中には住宅(R)・商業(C)・工業(I)を塗った。まだ「空き地」だが、次回ここに建物が育つ。

まとめ:第2回の要点

  • 生成はAI、採否は数値:Seedream(OpenRouter)でタイルを生成し、共通スタイルで画風をそろえる。ただし採用は見た目でなく実測で判断する。
  • 均一化は必須:縁を削って継ぎ目を消し、NEAREST32×32 に。生成物をそのまま使わない。
  • 未ロードは色でフォールバックTileAtlas は読めていないタイルに null を返し、描画側が色で代替。起動直後も落ちない。
  • 排他セルcontent を1値にして「道路かつ住宅」を型で表現不能に。コマンドは純関数でテストしやすく。
  • オートタイリング:4近傍を4bit(0〜15)にして、基本4タイル×90°回転で全16通り。道路は十字1枚から派生させて接続を保証。

次回(第3回)は、いよいよ街を動かす。いま塗った住宅・商業・工業の「空き地」に、需要と道路接続に応じて建物が育つ決定論シミュレーションを作る。同じ操作なら同じ結果になる(=テストできる)ように乱数を種から回し、人口・資金・税収がかみ合う経済までを一気に組む。ここまででMVP(遊べる最小の街づくり)が完成する回だ。

AI生成コンテンツの開示:地形・道路・ゾーンのドット絵タイルは、画像生成AI(Seedream / OpenRouter経由)の出力を後処理・検証して使用しています。出力の商用利用については各サービスの利用条件を確認のうえ使用しています(2026-06-16 確認)。掲載コードは実際のソースからの抜粋です。© 2026 Cooliris(本作のコード・構成・データの権利は作者に帰属します)。


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

Claude Fable 5 が7/1にグローバル再開 — Fable/Mythos/Opus/Sonnet/Haiku を用途で選ぶ完全整理Claude Fable 5 が7/1にグローバル再開 — Fable/Mythos/Opus/Sonnet/Haiku を用途で選ぶ完全整理前のページ

Google Chirp 3: Transcription が GA — 話者分離・多言語ASRと自作Whisperの使い分け次のページGoogle Chirp 3: Transcription が GA — 話者分離・多言語ASRと自作Whisperの使い分け

ピックアップ記事

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

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

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

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

関連記事

  1. バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】

    バイブコーディング

    バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】

    最初に必要な道具はたった1つ。ブラウザのAIチャット(Claude/C…

  2. ゲーム感覚でIT・AI用語500選を覚える学習アプリの作り方 — XP・13ランク・実績で「続く」を設計する
  3. 背景除去アプリを作る⑥|QGraphicsViewズーム・スポイト・QThread非同期・D&D一括処理【背景除去Studio制作】
  4. マイク録音テストを数値化するアプリの作り方 — ノイズフロア測定の落とし穴と、どのオーディオインターフェースでも使える手順
  5. Grokで回すXメディア生成パイプラインの作り方 — 検索から画像・音声・動画まで自動化する
  6. 都市開発シミュを作る⑥|静的ビルドをWordPressへ公開+連載総括【Aurum City制作・完結】

    アプリの作り方

    都市開発シミュを作る⑥|静的ビルドをWordPressへ公開+連載総括【Aurum City制作・完…

    完成した都市開発シミュを、バックエンド無しの静的ビルドにしてWordP…

注目

AIで、ここまで作れる

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

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

PR

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

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

  1. WebSearch とサブエージェントに上限200 — 片方は無言で止まる

    AI・テック動向

    WebSearch とサブエージェントに上限200 — 片方は無言で止まる
  2. MCP 2026-07-28 の Beta SDK — ステートレス化の破壊的変更を本番前に通す

    AI・テック動向

    MCP 2026-07-28 の Beta SDK — ステートレス化の破壊的変…
  3. AnimeGen のライセンスを条文まで読んだ — SDXL の挿絵を動かす前に確認したこと

    AI・テック動向

    AnimeGen のライセンスを条文まで読んだ — SDXL の挿絵を動かす前に…
  4. New Eden Intelligence Hub の作り方 — EVE Online の SSO×ESI API で市場・資産・戦闘を一画面に集約する自分専用ダッシュボード

    アプリの作り方

    New Eden Intelligence Hub の作り方 — EVE Onl…
  5. バイブコーディングとは?プログラミング未経験が最初に知っておきたい全体像【入門1】

    バイブコーディング

    バイブコーディングとは?プログラミング未経験が最初に知っておきたい全体像【入門1…
PAGE TOP

TAG CLOUD

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