Unityを使って、Boidバードを飛ばしてみました。
簡単ですが方法を説明します。
Boidsとは、多数の個体からなる群れの集団運動を、各個体に3つのルールを与えてシミュレーションするアルゴリズムです。Craig Reynolds 氏により1986年に提案されました。
個体間の単純なルールによって、複雑な集団運動が生まれることは、統計力学や磁性体の物理に通じるものがあります。
アルゴリズムについて詳しくは、Wikipediaへどうぞ。
https://en.wikipedia.org/wiki/Boids
今回作成したものがこちら。
動機
一昔前では、OpenGLや、java swing、python 3Dとかを使ってガリガリコーディングをする必要がありましたが、今ではUnityがあります。
Unityでは、グラフィック部分はマウスでクリックしていくだけで簡単に実装することができるので、コーディングをするのはアルゴリズム部分だけで済みます。
しかも、無料ライセンスがあります。
そういう訳で、昔から試しに作ってみたかったBoidsを実装してみました。
シーンの作成
鳥
まずは、鳥の個体(Boid)のPrefabを作ります。
- 鳥用に空のGameObjectを作成
- 鳥の画像をSpriteとしてインポート
- GameObjectに鳥のSpriteをアタッチ(Sprite Renderer)
- RigidBody2Dコンポーネントをアタッチ
- Gravity, Angular Dragを0にする。
- Project -> Create-> physics Material 2Dでマテリアルを作成しておく
- Bouncinessを1に。壁に当たったときに弾性衝突をさせるため。
- Circle Collider 2Dをアタッチ
- radiusをspriteのサイズに設定
- Materialに、先程作ったマテリアルをアタッチ
このPrefabには後でスクリプト(Boid.cs)を作成して、アタッチします。
鳥かご
鳥が画面外に飛んでいかないようにするため、壁を作ります。
- 空のGameObjectを作成。
- BoxCollider2Dをアタッチ
- GameObjectを3回Duplicateして、計4つの壁をつくる。
- offsetとsizeを調整して、Cameraの外側を囲う。
マネージャ
鳥のGameObjectの親となり、鳥の発生を制御するManagerというGameObjectを作ります。
- 空のGameObjectを作成します。
このGameObjectに後でスクリプトをアタッチします。
背景
背景がそのままでは寂しい場合は、スプライトを貼っても構いません。
スクリプトの作成
Manager.cs
スタート時にBoidを生成する次のようなC#スクリプトをManager.csという名前で作成し、Manager GameObjectにアタッチします。
using UnityEngine;
using UnityEngine.UI;
public class Manager : MonoBehaviour
{
// number of boids
public int number = 30;
public GameObject boid;
// Use this for initialization
void Start ()
{
// generate boids
// get window edges
Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0, 0));
Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(1, 1));
for (var i = 0; i < number; i++)
{
var position = new Vector3(Random.Range(min.x, max.x), Random.Range(min.y, max.y), 0);
var angle = Random.Range(0, 360.0f);
Instantiate(boid, position, Quaternion.AngleAxis(angle, Vector3.forward), transform);
}
}
}
}
コードの説明をします。
Managerゲームオブジェクトが作成された後、Start()関数が呼ばれます。
この関数内で、numberで指定した数だけのBoidを、ランダムな位置と向きに発生させます。
まず、視野範囲の座標を取得します。ViewportToWorldPoint()関数で、視野範囲の左下(0, 0)と右上(1,1)のワールド座標値を取得します。
この値を最小値、最大値として、Random.range()関数で、ランダムな位置座標(position)を生成します。
またBoidの向きに使用するため、角度(angle)を0から360度のなかからランダムに生成します。
この位置と角度を使用して、Instantiate()関数を使い、boid prefabから boidを作成します。
boidの親はこのManagerとするため、Instantiate()関数の第3引数は、このManagerのtransformを渡します。
これを、number回だけ繰り返します。
スクリプトが作成出来たら、Unityの操作画面に戻り、inspectorの(public GameObject) Boidフィールドに、Boid Prefabをドラッグ&ドロップして、Boidをアタッチしておきます。
Boid.cs
各boidの移動を司る次のようなBoid.csスクリプトを作成します。
using System.Collections.Generic;
using UnityEngine;
public class Boid : MonoBehaviour
{
public float speed;
public float sight;
public float alignment;
public float cohesion;
public float separation;
// Use this for initialization
void Start ()
{
var direction = transform.up;
GetComponent<Rigidbody2D>().velocity = direction * speed;
}
// Update is called once per frame
void Update ()
{
var neighbors = GetNeighbors();
if (neighbors.Count == 0) return;
var direction = cohesion * GetCohesion(neighbors)
+ alignment * GetAlignment(neighbors)
+ separation * GetSeparation(neighbors);
GetComponent<Rigidbody2D>().velocity =
(GetComponent<Rigidbody2D>().velocity.normalized + direction) * speed;
// see forward
transform.up = GetComponent<Rigidbody2D>().velocity.normalized;
}
List<Transform> GetNeighbors()
{
// index of this boid
var index = transform.GetSiblingIndex();
List<Transform> neighbors = new List<Transform>();
for (int i = 0; i < transform.parent.childCount; i++)
{
if (i != index)
{
Transform child = transform.parent.GetChild(i);
if (Vector2.Distance(child.position, transform.position) < sight)
{
neighbors.Add(child);
}
}
}
return neighbors;
}
Vector2 GetAlignment(List<Transform> neighbors)
{
var alignment = new Vector3(0,0,0);
foreach (var boid in neighbors)
{
alignment += boid.up;
}
return alignment.normalized;
}
Vector2 GetCohesion(List<Transform> neighbors)
{
var centerOfMass = new Vector3(0, 0, 0);
foreach (var boid in neighbors)
{
centerOfMass += boid.position;
}
return (centerOfMass / neighbors.Count - transform.position).normalized;
}
Vector2 GetSeparation(List<Transform> neighbors)
{
var distance = new Vector3(0, 0, 0);
foreach (var boid in neighbors)
{
distance += (boid.position - transform.position);
}
return (distance / neighbors.Count * -1).normalized;
}
}
Start()
スタート時に、boidの向いている方向(transform.up)に、指定された速度(speed)で進むようにします。
GetComponent<Rigidbody2D>().velocity にベクトル(Vector2)を代入すると、その方向に進みます。
Update()
この関数はフレームが更新されるごとに呼ばれます。
GetNeighbors()関数で、計算対象とする近隣の鳥を取得します。視野(sight)値の半径内に入る鳥は、計算対象に入れることにします。
近隣に鳥がいなかった場合はなにもしません。
鳥がいた場合には、3つのルール(GetCohesion(), GetAlignment(), GetSeparation())に従って、boidの進む方向を決定します。ルールの具体的な内容については、wikipediaや各関数を参考にしてください。
各ルールから進行方向を決めるには重みを付けます。重みは、各変数(cohesion, alignment, separation)で指定します。
GetComponent<Rigidbody2D>().velocityにベクトルを代入して進行方向を決めるだけでなく、スプライトの向きも進行方向に向けるため、transform.up = GetComponent<Rigidbody2D>().velocity.normalized;とします。
GetNeighbors()
全てのboidは生成時にManagerの子供に設定してあるので、他の鳥を取得するために、自分(transform)の親(parent)のi番目の子ども transform.parent.GetChild(i)と指定します。
parent.childCountで子供の数が取得でき、GetSiblingIndex()で自分の番号を取得できます。
視野(sight)内に入ったBoidのTransformはList型のオブジェクトに入れて、返します。
GetCohesion()
このルールは、近隣のBoidsがなす重心位置に集合するためのものです。
近隣のBoidsの重心を計算し、自分からの方向を返します。
このルールや他のルールでは、返す値はnormalizedしていますが、しない方法もあります。
GetAlignment()
このルールは、近隣のBoidsと進行方向を揃えるためのものです。
近隣のBoidsの向き(transform.up)の平均をとって、返します。
GetSeparation()
このルールは、混雑していない方向へ進行方向を向けるためのものです。
近隣のBoidsとの位置の差の平均をとったのち、ベクトルを逆方向にして返します。
コードの入力が済んだら、Unityの操作画面にもどり、boid.csをBoid prefabにアタッチします。また、inspectorでboid.csの各public変数に適当な値を設定します。
例:
speed = 5 sight = 3 alignment = 1 separation = 1 cohesion = 1
まとめ
以上で作成は終わりで、実行することができます。
alignmentなどのウェイト、sightの視野範囲を調整して、ウェイトが群れの振る舞いにどう影響するか確認してみてください。
視野を広くすると、大きな集団をつくりやすくなります。塊になりすぎる場合には、separationのウェイトを大きめにするとほどよくバラけます。
ルーディ・ラッカーの人工生命研究室on Windowsを読んで、自分も作りたいと思っておりました。
非常わかりやすい説明で、大変参考になりました。有難うございます。
お役に立てて幸いです!