FlockDrift は、鳥や魚の群れを再現するシミュレーターです。中心となる指令役はいません。個体が近くの仲間だけを見て動く 3 つの規則から、全体のうねりが勝手に生まれます。Craig Reynolds が 1986 年に発表した Boids の実装です。
#🎯 群れには「群れを作る意思」がない
ムクドリの大群がうねる映像を見ると、誰かが全体を指揮しているように見えます。でも実際には、鳥は近くの数羽しか見ていません。
Boids はそれを 3 つの規則で表します。
- 分離(Separation) — 近すぎる仲間から離れる
- 整列(Alignment) — 近くの仲間と同じ方向に揃える
- 結合(Cohesion) — 近くの仲間の中心へ寄る
全体を見る処理はどこにもありません。 それでも渦を巻き、分裂し、また合流します。作っていて一番驚いたのはここで、規則を書き終えて動かした瞬間にもう群れになっていました。
#🏗️ 3 規則を 1 ループで書く
各個体について、視界内の仲間を集めながら 3 つの合計を同時に作ります。
for (let j = 0; j < boids.length; j++) {
if (i === j) continue
const dx = o.x - b.x, dy = o.y - b.y
const d2 = dx * dx + dy * dy
if (d2 > perc2 || d2 === 0) continue
n++
aliX += o.vx; aliY += o.vy // 整列用: 速度の合計
cohX += o.x; cohY += o.y // 結合用: 位置の合計
if (d2 < sep2) {
sepX -= dx / d2 // 分離用: 近いほど強く反発
sepY -= dy / d2
}
}3 つの規則で別々にループを回さないのがポイントです。視界判定は距離計算が必要で、そこが一番重い。1 回の走査で 3 つぶんの材料を集めれば、計算量が 1/3 になります。
距離は二乗のまま比較しています。Math.sqrt は視界内と分かった相手にしか使いません。個体数 N に対して N² 回まわるループなので、平方根を省くだけで体感が変わります。
#⚖️ 分離だけ「距離で割る」
3 規則のうち、分離だけ扱いが違います。
sepX -= dx / d2距離の二乗で割っているので、近いほど反発が急激に強くなります。整列と結合は視界内なら一律に平均を取るのに対し、分離だけは至近距離で強く効く。
これが無いと個体が同じ点に潰れます。逆に分離を線形にすると、今度は群れがほどけて散ります。衝突回避だけは非線形、というのが Boids の効きどころでした。
#🎚️ 係数の大きさが 3 桁違う
3 つの規則に掛ける係数を見ると、桁がばらばらです。
b.vx += ((aliX / n) - b.vx) * 0.05 * p.alignment // 整列
b.vx += ((cohX / n) - b.x) * 0.0008 * p.cohesion // 結合
b.vx += sepX * 0.9 * p.separation // 分離理由は掛かる値の単位が違うからです。
- 整列は 速度の差(数 px/frame)
- 結合は 位置の差(数百 px)
- 分離は 1/距離 の合計(小さい値)
そのままだと結合だけが極端に強くなります。だから結合には 0.0008、分離には 0.9 と、値域を揃えるための係数が入ります。ユーザーに見せるスライダー(0〜1)とは別に、内部の正規化係数を持つ構造にしました。
この分離があると、スライダーをどう動かしても群れが破綻しません。つまみを触って壊れるシミュレーターは、この正規化をサボっている場合が多いです。
#🦅 捕食者を入れると群れが「割れる」
マウスに追従する捕食者を足しました。個体は捕食者から一定距離内に入ると、反対方向へ強く加速します。
const flee2 = (p.perception * 2.2) * (p.perception * 2.2)
if (d2 < flee2 && d2 > 0) {
const d = Math.sqrt(d2)
b.vx += (dx / d) * 0.9
b.vy += (dy / d) * 0.9
}逃避の視界を通常の2.2 倍にしているのがポイントで、仲間より捕食者を先に見つけないと逃げ遅れます。
これを入れると、群れが避けて割れ、通過後にまた合流する ── 実際の鳥の群れで見る動きが出ます。規則を 1 つ足しただけで、行動のレパートリーが一段増えるのが面白いところでした。
#🐟 プリセットで「何の群れか」を変える
ムクドリ・魚群・蛍・蚊柱の 4 つは、パラメータの組み合わせだけで作り分けています。
| プリセット | 特徴 |
|---|---|
| ムクドリ | 視界が広く、整列が強い。 大きなうねり |
| 魚群 | 分離が強く、密度が高い。 急な方向転換 |
| 蛍 | 速度が遅く、ゆらぎが大きい。 ばらけて漂う |
| 蚊柱 | 結合が非常に強く、狭い範囲に密集 |
同じ 3 規則でも、係数の比率だけで生き物が変わります。
#😓 苦労したところ
画面端の扱い。 端で反転させると不自然に跳ね返り、ラップ(反対側から出す)にすると群れが分断されます。最終的に、端から一定距離(margin 40px)に入ったら中央へ向かう力を弱く足す方式にしました。壁ではなく「戻りたくなる領域」を作る発想です。
個体数と速度の両立。 O(N²) なので数を増やすと落ちます。視界内かどうかの判定を最初に置いて早期 continue し、平方根を避け、1 ループにまとめた ── この 3 つで数百体まで 60fps を保てました。空間分割は入れていません。
「群れているか」の判定基準がない。 物理シミュレーションと違って、正解の数値がありません。エネルギー保存のような検算ができないので、見て判断するしかない題材でした。だからこそプリセットを用意して、「この設定ならこう見える」という基準点を置いています。
#🔭 今後の拡張
- 空間分割(グリッドや四分木)で数千体
- 障害物を置いて避けさせる
- 3 次元化
- 個体ごとに視野角を持たせる(現在は全方位。 実際の鳥は後ろが見えない)
#💡 このサービスから言えること
創発を扱うプログラムは、規則の少なさそのものが価値です。 全体を制御するコードを 1 行も書かずに群れが出る、という事実が面白い。制御を足したくなったら、それは Boids ではない別のものになります。
実装面では、単位の違う量を足し合わせるときの正規化が全部でした。3 規則の係数が 0.0008 から 0.9 まで散らばっているのは行儀が悪く見えますが、そこを揃えたからスライダーで壊れない挙動になっています。
[ ./next_action ]
読んだら、 FlockDrift を実際に動かす。
この開発ログは FlockDrift をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。