ゲームを作っていると、たまに鳥や魚などの「群れ」をシミュレーションで再現してみたくなることがあります。このような場合にとても役立つのが「Boids」(ボイド)というアルゴリズムです。
以前ゲームジャムでこのアルゴリズムを教えてもらってからずっと興味があったので、今回UnityでBoidsを使った群れのシミュレーションプログラムを作ってみることにしました。出来上がったプログラムの挙動は次のGIFの通りです。
かなりリアルな群れの動きをしていることがわかります。
Boids(ボイド)とは?
はじめにBoidsについて簡単に説明しておきます。Boidsは単純なプログラムで鳥や魚などの群れの挙動を再現することができるアルゴリズムです。
このアルゴリズムは、群れを構成する各個体に次の3つの動作を与えることで成立します。
- 分離(Separation):他の個体とぶつからないように距離をとる。
- 整列(Alignment):他の個体と同じ方向を向く。
- 集合(Cohesion):群れの中心へ移動する。
基本はこれだけなのでプログラムに落とし込むのはそれほど難しくありません。手軽に扱えてしかもリアルな挙動を再現できるのがBoidsの大きな特徴です。
UnityでBoidsを使って群れをシミュレーションする方法
では早速UnityでBoidsを使って群れを再現する方法を紹介します。やり方は色々あると思いますが、ここでは今回私が採用した方法について説明していきますね。
UnityでBoidsを使うための下準備
UnityでBoidsを使うためには次のようなゲームオブジェクトを用意すると便利です。
- 群れの管理オブジェクト:
群れの個体を生成したり、各個体で利用できる値を計算したりする。 - 個体オブジェクト:
群れに所属する1つ1つの個体を模したオブジェクト。 - リーダー:
群れの中心となるゲームオブジェクト(※任意のゲームオブジェクトでOK)。これが移動させると群れも追従する。
リーダーのゲームオブジェクトはあってもなくてもどちらでも良いのですが、ゲームで使うことを考えるとあった方が群れを制御しやすいので、今回は用意しておくことにしました。
BoidsのC#スクリプト(※抜粋)
次に今回作ったC#スクリプトを掲載しておきます。ただし基礎部分に私が普段使っている自作システムの処理を使ったので、その部分は割愛したプログラムのみを抜粋して掲載します。
群れの管理オブジェクト(BoidsManager.cs)
void Start() { if(boidLeader == null) { boidLeader = transform; } if (spawnAtStart) { SpawnBoids(); } } void Update() { if (!isActive || activeBoids.Count < 2) { return; } aveAlignment = GetAverageAlignment(); aveSeparation = GetAverageSeparation(); } Vector3 GetAverageAlignment() { Vector3 alignment = Vector3.zero; foreach (Boids boid in activeBoids) { alignment += boid.Velocity; } alignment /= activeBoids.Count; return alignment; } Vector3 GetAverageSeparation() { Vector3 separation = Vector3.zero; foreach (Boids boid in activeBoids) { separation += boid.Position; } separation /= activeBoids.Count; return separation; }
主な変数・メソッド
- activeBoids:いまアクティブな個体オブジェクトのリスト
- boidLeader:群れの中心になるオブジェクト
- aveAlignment:群れの各個体の向きの平均値
- aveSeparation:群れの各個体の座標の平均値
- SpawnBoids:群れの個体を生成するメソッド(※割愛)
- GetAverageAlignment:群れの各個体の向きの平均値を求めるメソッド
- GetAverageSeparation:群れの各個体の座標の平均値を求めるメソッド
プログラムの解説
群れの管理オブジェクトでは、群れの各個体を必要な分だけ生成したり各個体で共通で利用する値を計算したりします。
alignmentとseparationは個体ごとに計算しても良いのですが、そうすると群れの個体数が多くなった時に計算量が肥大化して処理がすごく重くなってしまいます。そこでここでは管理オブジェクト側で予め計算しておき、それを各個体で参照して利用する方法を採用しました。
個体オブジェクト(Boids.cs)
void Move() { float deltaTime = Time.deltaTime; float speed; Vector3 direction; accel = (HeadToLeader() * boidsData.LeaderWeight + GetCohesion() * boidsData.CohesionWeight + GetAlignment() * boidsData.AlignmentWeight + GetSeparation() * boidsData.SeparationWeight) / (boidsData.LeaderWeight + boidsData.CohesionWeight + boidsData.AlignmentWeight + boidsData.SeparationWeight); Velocity += accel * Mathf.PerlinNoise(Time.time, Random.value); direction = Velocity.normalized; speed = Velocity.magnitude; Velocity = direction * Mathf.Clamp(speed, boidsData.MinSpeed, boidsData.MaxSpeed); Position += Velocity * deltaTime; if (speed > 0) { thisTransform.rotation = Quaternion.LookRotation(Velocity, Vector3.up); } } Vector3 HeadToLeader() { Vector3 headToLeader = Vector3.zero; if((boidsManager.BoidLeader.position - Position).sqrMagnitude > boidsData.MaxDistanceFromLeader * boidsData.MaxDistanceFromLeader) { headToLeader = boidsManager.BoidLeader.position - Position; } return headToLeader; } Vector3 GetCohesion() { Vector3 origin; Vector3 cohesion; if (boidsManager.BoidLeader != null) { origin = boidsManager.BoidLeader.transform.position; } else { origin = boidsManager.transform.position; } cohesion = origin - Position; cohesion = Vector3.Lerp(cohesion, Velocity, cohesionTurblence); return cohesion; } Vector3 GetAlignment() { Vector3 alignment = boidsManager.AverageAlignment; alignment = Vector3.Lerp(alignment, Velocity, alignmentTurblence); return alignment; } Vector3 GetSeparation() { Vector3 separation = boidsManager.AverageSeparation; separation = Position - separation; separation = Vector3.Lerp(separation, Position, separationTurblence); return separation; }
主な変数・メソッド
- boidsData:個体のデータを格納したScriptableObject
- accel:毎フレームの速度の変位
- Velocity:個体の速度
- Position:transform.positionと同じ
- thisTransform:transformのキャッシュ(※transformに毎回アクセスすると少し重いため)
- ○○Turblence:
○○を無視する割合(0~1のfloat値)。例えばcohesionTurblenceが1の場合、cohesionの影響を完全に無視する。 - Move:毎フレームの移動処理(Updateから呼び出し)
- HeadToLeader:リーダーから離れすぎた時に、リーダーの方に戻る方向のベクトルを得るメソッド
- GetCohesion:リーダーに向かう方向のベクトルを得るメソッド
- GetAlignment:群れ全体と同じ方向を取得するメソッド
- GetSeparation:群れから離れる向きのベクトルを得るメソッド
プログラムの解説
各個体の移動処理を行うクラスです。上で紹介したアルゴリズムを使って毎フレームの移動量を計算し、それをtransformに反映します。
GetAlignmentとGetSeparationはBoidsManagerから値を持ってくるだけなので簡単です。また、GetCohesionもリーダーの座標へ向かうベクトルを計算しているだけです。
少し複雑なのはMoveメソッドで、次のような処理を行っています。
- 他のメソッドの値を加重平均してaccelを算出する。
- accelにパーリンノイズを掛けた値をVelocityに加算する(※パーリンノイズを掛けることで自然な動きになるかな?と思ったのでこの処理を行っています)。
- Velocityを方向と速さに分解し、速さを予め設定した値に制限する(こうしないととんでもない速さになってしまう)。
- Positionに1フレーム当たりのVelocityを加算する。
- もし速さが0より大きければ、個体の向きをVelocityの向きに変更する。
おわりに
以上、UnityでBoidsを使った群れシミュレーションをしてみた…という話でした。
今のところこれをすぐにゲームに使う予定はないのですが、これだけリアルな群れを再現できるとなると何か面白いことができそうな気がしますね。また何かアイデアを考えてみようかなと思います。
いずれにしてもUnityがあればこういったシミュレーションも簡単に行うことができて楽しいです。ゲームに直接関係ない研究もなかなか面白いですし、何か新しいゲームのヒントになる可能性もあるのでぜひ皆さんも色々試してみてください。