← Blog
SimulationAgent-BasedCanvas API

アリの巣シミュレーション — フェロモンで創発する群知能

9 分で読める

このトピックを深掘りする

クリエイティブコーディング

ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法

全12本中 11 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

なぜアリのシミュレーションなのか

アリ1匹は単純なルールで動く。餌を探し、見つけたら巣に運ぶ。それだけだ。しかし数十匹が同じルールで動くと、最短経路の発見や効率的な分業といった「知能」が創発する。この現象はスティグマジー(Stigmergy)と呼ばれ、個体間の直接通信ではなく環境を介した間接的な情報共有で実現される。

AntFarmコンポーネントでは、フェロモンの分泌・蒸発・追従という3つの仕組みだけで、アリの群知能をCanvas API上に再現する。

データ構造 — グリッドとエージェント

世界は2つのTyped Arrayで表現する:

const grid = new Uint8Array(cols * rows)       // セルの種類
const pheromone = new Float32Array(cols * rows) // フェロモン濃度

Uint8Array はセル1つあたり1バイト。空気・土・トンネル・食料・巣の5種類を enum で管理する:

enum CellType {
  Air = 0,
  Dirt = 1,
  Tunnel = 2,
  Food = 3,
  Nest = 4,
  Grass = 5,
}

フェロモン濃度は Float32Array で0.0〜1.0の連続値。Uint8Array だと256段階しか表現できず、減衰カーブが階段状になる。浮動小数点で滑らかな減衰を実現するために Float32Array を選んだ。

アリはオブジェクトの配列で管理する:

enum AntState {
  Wander = 0,   // 餌を探索中
  CarryFood = 1, // 餌を運搬中
}

interface Ant {
  x: number
  y: number
  state: AntState
  dx: number  // 移動方向X
  dy: number  // 移動方向Y
  digCooldown: number
}

アリが持つ状態は「探索中」と「運搬中」の2つだけ。この最小限の状態遷移が、シミュレーション全体の振る舞いを決定する。

フェロモンの3つのメカニズム

1. 分泌(Deposit)

餌を見つけたアリが巣に戻る途中、現在位置にフェロモンを分泌する:

const PHEROMONE_STRENGTH = 0.8

// CarryFood状態のアリが毎ティック実行
const pi = ant.y * cols + ant.x
pheromone[pi] = Math.min(pheromone[pi] + PHEROMONE_STRENGTH, 1)

Math.min で上限1.0にクランプしている。これがないと、複数のアリが同じセルを通るたびに値が際限なく増加する。

2. 蒸発(Evaporation)

毎ティック、全セルのフェロモン濃度を減衰させる:

const PHEROMONE_DECAY = 0.997

for (let i = 0; i < pheromone.length; i++) {
  if (pheromone[i] > 0.001) {
    pheromone[i] *= PHEROMONE_DECAY
  } else {
    pheromone[i] = 0
  }
}

0.997 という減衰率は、約230ティックで半減する計算(0.997^230 ≈ 0.5)。減衰が速すぎると経路が消えてしまい、遅すぎると古い情報が残り続けて非効率な経路にアリが集中する。

閾値 0.001 未満をゼロに切り捨てるのはパフォーマンスのため。微小な値の乗算を毎フレーム数万セル分実行するのは無駄だ。

3. 追従(Following)

探索中のアリはフェロモン濃度が高い方向に引き寄せられる。ただし、AntFarmの現在の実装ではシンプルさを優先して、Wander状態のアリはランダムウォーク+下方バイアスで動く。CarryFood状態のアリだけが巣への方向ベクトルに従う:

// 巣への方向を計算
const tdx = nest.x - ant.x
const tdy = nest.y - ant.y

let moveDx = tdx > 0 ? 1 : tdx < 0 ? -1 : 0
let moveDy = tdy > 0 ? 1 : tdy < 0 ? -1 : 0

// 20%の確率でランダムな方向に逸れる
if (Math.random() < 0.2) {
  const d = randomDir()
  moveDx = d.dx
  moveDy = d.dy
}

この「20%のランダム逸脱」が重要。完全に最短経路を辿ると、全アリが同じルートを通り、新しい経路を発見できない。ノイズが探索の多様性を担保する。

環境のインタラクション — 掘削と障害物

アリは土を掘ってトンネルを作る:

const nx = ant.x + ant.dx
const ny = ant.y + ant.dy

if (inBounds(nx, ny)) {
  const ci = ny * cols + nx
  const cell = grid[ci]

  if (cell === CellType.Dirt && ant.digCooldown <= 0) {
    grid[ci] = CellType.Tunnel
    ant.digCooldown = 3  // 3ティックのクールダウン
  }
}

digCooldown は掘削速度を制限する。これがないと1ティックで何セルも掘り進めてしまい、自然な「じわじわ広がるトンネル」にならない。

初期状態では巣から地表まで1本のトンネルを事前に掘っておく。完全に土で覆われた状態からスタートすると、アリが巣の周辺で行き詰まるためだ:

let cx = nestX
let cy = surfaceY

while (cy < nestY - 2) {
  for (let ox = -1; ox <= 1; ox++) {
    if (inBounds(cx + ox, cy)) {
      const ci = cy * cols + (cx + ox)
      if (grid[ci] === CellType.Dirt) {
        grid[ci] = CellType.Tunnel
      }
    }
  }
  cy++
  // 30%の確率で左右にずれる → 自然な蛇行
  if (Math.random() < 0.3) {
    cx = clamp(cx + (Math.random() < 0.5 ? -1 : 1), 2, cols - 3)
  }
}

ImageDataによるピクセル単位の描画

数千セル+フェロモンオーバーレイ+アリの描画を毎フレーム処理するには、fillRect では遅すぎる。ImageDataを直接操作する:

const imgData = ctx.createImageData(w * dpr, h * dpr)
const data = imgData.data

for (let y = 0; y < rows; y++) {
  for (let x = 0; x < cols; x++) {
    const gi = y * cols + x
    const cell = grid[gi]
    const ph = pheromone[gi]

    let r = 0, g = 0, b = 0
    // セル種別ごとに色を決定...

    // フェロモンオーバーレイ(線形補間)
    if (ph > 0.01 && (cell === CellType.Tunnel || cell === CellType.Air)) {
      const alpha = Math.min(ph, 0.6)
      r = Math.floor(r * (1 - alpha) + 120 * alpha)
      g = Math.floor(g * (1 - alpha) + 180 * alpha)
      b = Math.floor(b * (1 - alpha) + 255 * alpha)
    }

    // セルをスケーリングしてImageDataに書き込み
  }
}

ctx.putImageData(imgData, 0, 0)

フェロモンの可視化はアルファブレンディングの手計算。globalAlpha やレイヤー合成を使わず、RGBを直接線形補間する。GPUへの転送は putImageData の1回だけ。

アリの描画も同じ ImageData 上に円を直接ラスタライズする。arc() でパスを描くよりも高速で、1つのバッファで完結する。

速度制御とフレームレート

シミュレーションの速度を1x/2x/4xで切り替える仕組み:

const loop = () => {
  const ticks = speedRef.current
  for (let i = 0; i < ticks; i++) {
    simulate()
  }
  render()
  rafRef.current = requestAnimationFrame(loop)
}

simulate() を1フレームあたり複数回呼ぶが、render() は1回だけ。描画が最もコストの高い処理なので、シミュレーション回数を増やしても描画負荷は変わらない。

エージェントベースモデルの設計指針

AntFarmの実装から得られる教訓:

  1. 状態は最小限にする: アリの状態は Wander/CarryFood の2つだけ。状態が増えるほどデバッグが困難になる
  2. 環境に情報を持たせる: アリ同士は直接通信しない。フェロモンという「環境の記憶」が間接的な協調を実現する
  3. ノイズを設計に組み込む: 完全な最適化は局所解に陥る。ランダム性が探索空間を広げる
  4. Typed Arrayで空間を表現する: グリッドベースのシミュレーションでは Uint8Array / Float32Array がオブジェクト配列より桁違いに速い

まとめ:フェロモンシミュレーションの道具たち

エージェントベースモデルと群知能の背景理論を深く理解するなら、道具棚の書籍が最適。単純なルールから複雑な振る舞いが生まれるメカニズムは、ゲームAIの基礎であり、自然界のシミュレーション全般に通じる。