1987年Craig W.Reynolds發表一篇名為《鳥群、牧群、魚群:分布式行為模式》的論文,描述了一種非常簡單的、以面向對象思維模擬群體類行為的方法,稱之為 Boids ,Boids 采用了三個核心的規則:
- 排斥性:避免與群體內鄰近個體發生碰撞
- 同向性:趨向與鄰近的個體采用相同的速度方向
- 凝聚向心性:向鄰近個體的平均位置靠近
由此我們采用Unity來實現算法並演示,演示結果:
制作思路
每個boid對象,每幀都有2個關鍵的表:與該boid鄰近的boids的表及與該boid最近的boids的表。根據兩個表求得一些確定該boid位置、方向、速度的因素(例如其他boids的平均速度,其他boids的平均位置,該boid與其他boid的平均距離等),根據所提出的三規則,設置各影響因素的權重比例,最終所有影響因素加和成為確定的、該boid下一幀的方向、位置、速度。
Boid模型的創建與配置
- 創建空對象取名Boid,再創建其空對象子物體,取名Fuselage
- Fuselage->position(0,0,0),Rotation(7.5,0,0),Scale(0.5,0.5,2)
- 創建一個Cube作為Fuselage子物體,移除 Box Collider,並添加拖尾渲染器 TrailRenderer
- Cube->position(0,0,0),Rotation(45,0,45),Scale(1,1,1)
- Component->Effects->TrailRenderer,選擇材質Defualt-Particle(Material),Time值0.5,End Witdth值0.25
- 復制Fuselage創建另一個名為Wing的組件添加到Boid,兩個都屬於Boid的子物體
- 修改主相機位置到頂視圖大范圍
- 將boid設為預制體,boid 模型制作完畢
腳本配置
BoidSpawner.cs
綁定於主相機Boid.cs
綁定於預制體Boid上- 主相機檢視面板中,設定
boidPrefab
變量為預制體 boid
項目地址:Boids
/*-------BoidSpawner.cs-------*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BoidSpawner : MonoBehaviour
{
//BoidSpawner 的單例模式,只允許存在BoidSpawner的一個實例,所以存放在靜態變量S中
static public BoidSpawner S;
//配置參數,調整Boid對象的行為
public int numBoids = 100; //boid 的個數
public GameObject boidPrefab; //boid 在unity中的預制體
public float spawnRadius = 100f; //實例化 boid 的位置范圍
public float spawnVelcoty = 10f; //boid 的速度
public float minVelocity = 0f;
public float maxVelocity = 30f;
public float nearDist = 30f; //判定為附近的 boid 的最小范圍值
public float collisionDist = 5f; //判定為最近的 boid 的最小范圍值(具有碰撞風險)
public float velocityMatchingAmt = 0.01f; //與 附近的boid 的平均速度 乘數(影響新速度)
public float flockCenteringAmt = 0.15f; //與 附近的boid 的平均三維間距 乘數(影響新速度)
public float collisionAvoidanceAmt = -0.5f; //與 最近的boid 的平均三維間距 乘數(影響新速度)
public float mouseAtrractionAmt = 0.01f; //當 鼠標光標距離 過大時,與其間距的 乘數(影響新速度)
public float mouseAvoidanceAmt = 0.75f; //當 鼠標光標距離 過小時,與其間距的 乘數(影響新速度)
public float mouseAvoiddanceDsit = 15f;
public float velocityLerpAmt = 0.25f; //線性插值法計算新速度的 乘數
public bool ______________;
public Vector3 mousePos; //鼠標光標位置
private void Start()
{
//設置單例變量S為BoidSpawner的當前實例
S = this;
//初始化NumBoids(當前為100)個Boids
for (int i = 0; i < numBoids; i++)
Instantiate(boidPrefab);
}
private void LateUpdate()
{
//讀取鼠標光標位置
Vector3 mousePos2d = new Vector3(Input.mousePosition.x, Input.mousePosition.y, this.transform.position.y);
//從世界空間到屏幕空間變換位置
mousePos = this.GetComponent<Camera>().ScreenToWorldPoint(mousePos2d);
}
}
/*-------Boid.cs-------*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boid : MonoBehaviour
{
static public List<Boid> boids; //實例化Boid 的表
public Vector3 velocity; //當前速度
public Vector3 newVelocity; //下一幀中的速度
public Vector3 newPosition; //下一幀中的位置
public List<Boid> neighbors; //附近所有的 Boid 的表
public List<Boid> collisionRisks; //距離過近的所有 Boid 的表(具有碰撞風險,需要處理)
public Boid closest; //最近的 Boid
//初始化Boid
private void Awake()
{
//如果List變量boids未定義,則對其進行定義
if (boids == null)
boids = new List<Boid>();
//向Boids List 中添加Boid
boids.Add(this);
//為當前Boid實例提供一個隨機的位置和速度
//實例化的boid位置在 半徑為 1*spawnRadius 的球形范圍內
Vector3 randPos = Random.insideUnitSphere * BoidSpawner.S.spawnRadius;
//只讓Boid在xz平面上移動,並設定起始坐標
randPos.y = 0;
this.transform.position = randPos;
//Random.onUnitSphere 返回 一個半徑為1的 球體表面的點
velocity = Random.onUnitSphere;
velocity *= BoidSpawner.S.spawnVelcoty;
//初始化兩個List
neighbors = new List<Boid>();
collisionRisks = new List<Boid>();
//讓this.transform成為Boid游戲對象的子對象
this.transform.parent = GameObject.Find("Boids").transform;
//給Boid設置一個隨機的顏色
Color randColor = Color.black;
//設置顏色的顏色要 較深,非透明
while (randColor.r + randColor.g + randColor.b < 1.0f)
randColor = new Color(Random.value, Random.value, Random.value);
//渲染 boid
Renderer[] rends = gameObject.GetComponentsInChildren<Renderer>();
foreach (Renderer r in rends)
r.material.color = randColor;
}
private void Update()
{
//獲取到 當前boid 附近所有的Boids 的表
List<Boid> neighbors = GetNeighbors(this);
//使用當前位置和速度初始化新位置和新速度
newVelocity = velocity;
newPosition = this.transform.position;
//速度匹配
//取得於 當前Boid 的速度接近的 所有鄰近Boid對象 的平均速度
Vector3 neighborVel = GetAverageVelocity(neighbors);
//將 新速度 += 鄰近boid的平均速度*velocityMatchingAmt
newVelocity += neighborVel * BoidSpawner.S.velocityMatchingAmt;
/*
凝聚向心性:使 當前boid 向 鄰近Boid對象 的中心 移動
*/
//取得於 當前Boid 的三位坐標接近的 所有鄰近Boid對象 的平均三位間距
Vector3 neighborCenterOffset = GetAveragePosition(neighbors) - this.transform.position;
//將 新速度 += 鄰近boid的平均間距*flockCenteringAmt
newVelocity += neighborCenterOffset * BoidSpawner.S.flockCenteringAmt;
/*
排斥性:避免撞到 鄰近的Boid
*/
Vector3 dist;
if (collisionRisks.Count > 0) //處理 最近的boid 表
{
//取得 最近的所有boid 的平均位置
Vector3 collisionAveragePos = GetAveragePosition(collisionRisks);
dist = collisionAveragePos - this.transform.position;
//將 新速度 += 與最近boid的平均間距*flockCenteringAmt
newVelocity += dist * BoidSpawner.S.collisionAvoidanceAmt;
}
//跟隨鼠標光標:無論距離多遠都向鼠標光標移動
dist = BoidSpawner.S.mousePos - this.transform.position;
//若距離鼠標光標太遠,則靠近;反之離開(修改新速度)
if (dist.magnitude > BoidSpawner.S.mouseAvoiddanceDsit)
newVelocity += dist * BoidSpawner.S.mouseAtrractionAmt;
else
newVelocity -= dist.normalized * BoidSpawner.S.mouseAvoidanceAmt;
//至此在Update()內 確定了 新速度和新位置,需要在后續LateUpdate()內應用
//一般都是Update()內確定參數,在LateUpdate()內實現移動
}
private void LateUpdate()
{
//使用線性插值法
//基於計算出的新速度 進而修改 當前速度
velocity = (1 - BoidSpawner.S.velocityLerpAmt) * velocity + BoidSpawner.S.velocityLerpAmt * newVelocity;
//確保 速度值 在上下限范圍內(超過范圍就設定為范圍值)
if (velocity.magnitude > BoidSpawner.S.maxVelocity)
velocity = velocity.normalized * BoidSpawner.S.maxVelocity;
if (velocity.magnitude < BoidSpawner.S.minVelocity)
velocity = velocity.normalized * BoidSpawner.S.minVelocity;
//確定新位置(附加新方向),相當於1s移動 velocity 的距離
newPosition = this.transform.position + velocity * Time.deltaTime;
//將所有對象限制在XZ平面
//修改當前boid的方向:從原有位置看向新位置newPosition
this.transform.LookAt(newPosition);
//position移動方式,移動到新位置
this.transform.position = newPosition;
}
//查找那些Boid距離當前Boid距離足夠近,可以被當作附近對象
public List<Boid> GetNeighbors(Boid boi)
{
float closesDist = float.MaxValue; //最小間距,MaxValue 為浮點數的最大值
Vector3 delta; //當前 boid 與其他某個 boid 的三維間距
float dist; //三位間距轉換為的 實數間距
neighbors.Clear(); //清理上次表的數據
collisionRisks.Clear(); //清理上次表的數據
//遍歷目前所有的 boid,依據設定的范圍值篩選出 附近的boid 與 最近的boid 於各自表中
foreach (Boid b in boids)
{
if (b == boi) //跳過自身
continue;
delta = b.transform.position - boi.transform.position; //遍歷到的 b 與當前持有的 boi(都為boid) 的三維間距
dist = delta.magnitude; //實數間距
if (dist < closesDist)
{
closesDist = dist; //更新最小間距
closest = b; //更新最近的 boid 為 b
}
if (dist < BoidSpawner.S.nearDist) //處在附近的 boid 范圍
neighbors.Add(b);
if (dist < BoidSpawner.S.collisionDist) //處在最近的 boid 范圍(有碰撞風險)
collisionRisks.Add(b);
}
if (neighbors.Count == 0) //若沒有其他滿足鄰近范圍的boid,則將自身boid納入附近的boid表中
neighbors.Add(closest);
return (neighbors);
}
//獲取 List<Boid>當中 所有Boid 的平均位置
public Vector3 GetAveragePosition(List<Boid> someBoids)
{
Vector3 sum = Vector3.zero;
foreach (Boid b in someBoids)
sum += b.transform.position;
Vector3 center = sum / someBoids.Count;
return (center);
}
//獲取 List<Boid> 當中 所有Boid 的平均速度
public Vector3 GetAverageVelocity(List<Boid> someBoids)
{
Vector3 sum = Vector3.zero;
foreach (Boid b in someBoids)
sum += b.velocity;
Vector3 avg = sum / someBoids.Count;
return (avg);
}
}
參考
《游戲設計、原型與開發》 - Jeremy Gibson