# 群落算法
实现鱼群、鸟群、蜜蜂等群落的模拟。
群落应符合以下规则:
- 避免与邻近个体发生碰撞
- 避免与障碍物发生碰撞
- 趋向与邻近的个体采用相同的速度和方向
- 向邻近个体的平均位置靠近
# 准备
- 个体(Boid):准备好模型并添加基础组件
- 吸引子(Attractor):空物体,用于挂载吸引子脚本,它是一个群落的中心
- 生成器(Spawner):空物体,用于挂载生成器脚本
- 障碍物(Obstacle):添加有碰撞器的障碍物,用于测试
# 代码
# 吸引子(Attractor)
- Attractor 是所有 Boid 聚集的对象。没有它,Boid 会飞出屏幕。
- 让它沿着 Sin 函数移动
using UnityEngine; | |
namespace Boids | |
{ | |
/// <summary> | |
/// 吸引子,群体的聚集中心 | |
/// </summary> | |
public class Attractor : MonoBehaviour | |
{ | |
[Tooltip("移动速度")] | |
public float speed = 1; | |
[Tooltip("运动范围")] | |
public float radius = 10; | |
[Header("x,y,z轴向的相位")] | |
public float xPhase = 0.5f; | |
public float yPhase = 0.4f; | |
public float zPhase = 0.1f; | |
private void Update() | |
{ | |
Vector3 tPos = Vector3.zero; | |
Vector3 scale = this.transform.localScale; | |
tPos.x = Mathf.Sin(xPhase * speed * Time.time) * radius * scale.x; | |
tPos.y = Mathf.Sin(yPhase * speed * Time.time) * radius * scale.y; | |
tPos.z = Mathf.Sin(zPhase * speed * Time.time) * radius * scale.z; | |
this.transform.position = tPos; | |
} | |
} | |
} |
# 生成器(Spawner)
- 先创建 Boid 脚本,防止报错
- 控制群落的生成
- 控制群落的行为
using System.Collections.Generic; | |
using UnityEngine; | |
namespace Boids | |
{ | |
/// <summary> | |
/// 群落生成器 | |
/// </summary> | |
public class Spawner : MonoBehaviour | |
{ | |
[Header("控制群落的生成")] | |
[Tooltip("预制件")] | |
public GameObject boidsPrefab; | |
[Tooltip("吸引子")] | |
public Transform boidsAttractor; | |
[Tooltip("群落数量")] | |
public int numberBoids = 100; | |
[Tooltip("生成半径")] | |
public int spawnRadius = 10; | |
[Tooltip("生成间隔")] | |
public float spawnDelay = 0.1f; | |
[Header("控制群落的行为")] | |
[Tooltip("移动速度")] | |
public float velocity = 30; | |
[Tooltip("个体间距")] | |
public float neighborDistance = 30; | |
[Tooltip("碰撞距离")] | |
public float collDistance = 4; | |
[Tooltip("速度匹配")] | |
public float velocityMatching = 0.25f; | |
[Tooltip("中心聚集")] | |
public float flockCenter = 0.2f; | |
[Tooltip("个体间碰撞避免")] | |
public float collAvoidNeighbor = 2; | |
[Tooltip("个体与其他物体间碰撞避免")] | |
public float collAvoidOtherNeighbor = 10; | |
[Tooltip("向内吸引系数")] | |
public float attractPull = 2; | |
[Tooltip("向外推出系数")] | |
public float attractPush = 2; | |
[Tooltip("个体与吸引子之间的距离")] | |
public float attractPushDistance = 5f; | |
/// <summary> | |
/// 群落 | |
/// </summary> | |
private List<Boid> BoidsList; | |
private void Start() | |
{ | |
BoidsList = new List<Boid>(); | |
InstantiateBoids(); | |
} | |
/// <summary> | |
/// 实例化群落 | |
/// </summary> | |
private void InstantiateBoids() | |
{ | |
Boid boid = Instantiate(boidsPrefab, Vector3.zero, Quaternion.identity, this.transform).GetComponent<Boid>(); | |
BoidsList.Add(boid); | |
if (BoidsList.Count < numberBoids) | |
{ | |
Invoke(nameof(InstantiateBoids), spawnDelay); | |
} | |
} | |
} | |
} |
# Neighborhood 脚本
- 挂载到个体上
- 利用触发器检测其他邻近个体
- 用于跟踪所有邻近个体的信息,包括:
- 邻近个体的平均位置、速度、个体间距
- 其他邻近物体(障碍物)的平均位置
using System.Collections.Generic; | |
using UnityEngine; | |
namespace Boids | |
{ | |
/// <summary> | |
/// 跟踪所有邻近的个体的信息,包括平均位置、速度、个体间距 | |
/// </summary> | |
public class Neighborhood : MonoBehaviour | |
{ | |
/// <summary> | |
/// 所有邻近个体 | |
/// </summary> | |
private List<Boid> Neighbors; | |
/// <summary> | |
/// 其他邻近物体 | |
/// </summary> | |
private List<GameObject> OtherNeighbors; | |
private SphereCollider coll; | |
private Spawner spawner; | |
/// <summary> | |
/// 所有邻近个体的平均位置,如果没有则返回 Vector3.zero | |
/// </summary> | |
public Vector3 AveragePositionNeighbors | |
{ | |
get | |
{ | |
Vector3 avg = Vector3.zero; | |
if (Neighbors.Count == 0) return avg; | |
foreach (var neighbor in Neighbors) | |
{ | |
avg += neighbor.transform.position; | |
} | |
return avg / Neighbors.Count; | |
} | |
} | |
/// <summary> | |
/// 所有邻近个体的平均速度,如果没有则返回 Vector3.zero | |
/// </summary> | |
public Vector3 AverageVelocity | |
{ | |
get | |
{ | |
Vector3 avg = Vector3.zero; | |
if (Neighbors.Count == 0) return avg; | |
foreach (var neighbor in Neighbors) | |
{ | |
avg += neighbor.Rig.velocity; | |
} | |
return avg / Neighbors.Count; | |
} | |
} | |
/// <summary> | |
/// 碰撞距离内的所有个体的平均位置,如果没有则返回 Vector3.zero | |
/// </summary> | |
public Vector3 AverageCollisionPositionNeighbors | |
{ | |
get | |
{ | |
Vector3 avg = Vector3.zero; | |
if (Neighbors.Count == 0) return avg; | |
Vector3 vectorNeighbor; // 指向邻近个体的矢量 | |
int nearCount = 0; // 邻近个体数量 | |
foreach (var neighbor in Neighbors) | |
{ | |
vectorNeighbor = neighbor.transform.position - this.transform.position; | |
if (vectorNeighbor.magnitude <= spawner.collDistance) | |
{ | |
avg += neighbor.transform.position; | |
nearCount++; | |
} | |
} | |
return avg / nearCount; | |
} | |
} | |
/// <summary> | |
/// 碰撞距离内的所有其他邻近物体的平均位置,如果没有则返回 Vector3.zero | |
/// </summary> | |
public Vector3 AverageCollisionPositionOthers | |
{ | |
get | |
{ | |
Vector3 avg = Vector3.zero; | |
if (Neighbors.Count == 0) return avg; | |
Vector3 vectorNeighbor; // 指向邻近个体的矢量 | |
int nearCount = 0; // 邻近个体数量 | |
foreach (var otherNeighbor in OtherNeighbors) | |
{ | |
vectorNeighbor = otherNeighbor.transform.position - this.transform.position; | |
if (vectorNeighbor.magnitude <= spawner.collDistance) | |
{ | |
avg += otherNeighbor.transform.position; | |
nearCount++; | |
} | |
} | |
return avg / nearCount; | |
} | |
} | |
private void Awake() | |
{ | |
coll = this.GetComponent<SphereCollider>(); | |
spawner = this.transform.parent.GetComponent<Spawner>(); | |
Neighbors = new List<Boid>(); | |
OtherNeighbors = new List<GameObject>(); | |
} | |
private void Start() | |
{ | |
Init(); | |
} | |
/// <summary> | |
/// 初始化 | |
/// </summary> | |
private void Init() | |
{ | |
// 个体间距 | |
coll.radius = spawner.neighborDistance / 2; | |
} | |
private void Update() | |
{ | |
ResetRadius(); | |
} | |
/// <summary> | |
/// 重置间距 | |
/// </summary> | |
private void ResetRadius() | |
{ | |
if (coll.radius != spawner.neighborDistance / 2) | |
{ | |
coll.radius = spawner.neighborDistance / 2; | |
} | |
} | |
private void OnTriggerEnter(Collider other) | |
{ | |
if (other.TryGetComponent<Boid>(out Boid temp)) | |
{ | |
if (!Neighbors.Contains(temp)) | |
{ | |
Neighbors.Add(temp); | |
} | |
} | |
else if (!other.TryGetComponent<Attractor>(out Attractor attractor)) | |
{ | |
if (!OtherNeighbors.Contains(other.gameObject)) | |
{ | |
OtherNeighbors.Add(other.gameObject); | |
} | |
} | |
} | |
private void OnTriggerExit(Collider other) | |
{ | |
if (other.TryGetComponent<Boid>(out Boid temp)) | |
{ | |
if (Neighbors.Contains(temp)) | |
{ | |
Neighbors.Remove(temp); | |
} | |
} | |
else | |
{ | |
if (OtherNeighbors.Contains(other.gameObject)) | |
{ | |
OtherNeighbors.Remove(other.gameObject); | |
} | |
} | |
} | |
} | |
} |
# 个体(Boid)
- 随机初始位置,随机颜色(可选)
- 利用刚体进行运动
- 根据 Neighborhood 脚本中的信息计算个体的速度(线性插值)
using UnityEngine; | |
namespace Boids | |
{ | |
/// <summary> | |
/// Boids | |
/// </summary> | |
public class Boid : MonoBehaviour | |
{ | |
public Rigidbody Rig { get; private set; } | |
private Renderer[] renderers; | |
private TrailRenderer tRenderer; | |
private Spawner spawner; | |
private Neighborhood neighborhood; | |
private void Awake() | |
{ | |
Rig = this.GetComponent<Rigidbody>(); | |
renderers = this.gameObject.GetComponentsInChildren<Renderer>(); | |
tRenderer = this.GetComponentInChildren<TrailRenderer>(); | |
spawner = this.transform.parent.GetComponent<Spawner>(); | |
neighborhood = this.GetComponent<Neighborhood>(); | |
} | |
private void Start() | |
{ | |
Init(); | |
LookAhead(); | |
} | |
private void Update() | |
{ | |
Move(); | |
LookAhead(); | |
} | |
/// <summary> | |
/// 初始化 | |
/// </summary> | |
private void Init() | |
{ | |
// 随机初始位置 | |
this.transform.position = Random.insideUnitSphere * spawner.spawnRadius; | |
// 随机初始速度 | |
Rig.velocity = Random.onUnitSphere * spawner.velocity; | |
// 随机颜色,并保证不暗淡 | |
Color randomColor_start; | |
Color randomColor_end; | |
do | |
{ | |
randomColor_start = new Color(Random.value, Random.value, Random.value); | |
randomColor_end = new Color(Random.value, Random.value, Random.value); | |
} | |
while (randomColor_start.r + randomColor_start.g + randomColor_start.b < 1 || | |
randomColor_end.r + randomColor_end.g + randomColor_end.b < 1 | |
); | |
foreach (Renderer r in renderers) | |
{ | |
r.material.color = randomColor_start; | |
} | |
tRenderer.startColor = randomColor_start; | |
tRenderer.endColor = randomColor_end; | |
} | |
/// <summary> | |
/// 向前看 | |
/// </summary> | |
private void LookAhead() | |
{ | |
this.transform.LookAt(this.transform.position + Rig.velocity); | |
} | |
/// <summary> | |
/// 移动 | |
/// </summary> | |
private void Move() | |
{ | |
Vector3 velocity = MoveCorrection(); | |
velocity = JudgeAttract(velocity); | |
Rig.velocity = velocity; | |
} | |
/// <summary> | |
/// 移动方向校正 | |
/// </summary> | |
/// <returns></returns> | |
private Vector3 MoveCorrection() | |
{ | |
Vector3 velocity = Rig.velocity; | |
Vector3 collisionAvoid = CollisionAvoid(); | |
Vector3 collisionAvoidOthers = CollisionAvoidOthers(); | |
Vector3 velocityAlign = VelocityMatch(); | |
Vector3 velocityCenter = VelocityCenter(); | |
if (velocityAlign != Vector3.zero) | |
{ | |
velocity = Vector3.Lerp(velocity, velocityAlign, spawner.velocityMatching * Time.deltaTime); | |
} | |
if (velocityCenter != Vector3.zero) | |
{ | |
velocity = Vector3.Lerp(velocity, velocityCenter, spawner.flockCenter * Time.deltaTime); | |
} | |
if (collisionAvoid != Vector3.zero) | |
{ | |
velocity = Vector3.Lerp(velocity, collisionAvoid, spawner.collAvoidNeighbor * Time.deltaTime); | |
} | |
if (collisionAvoidOthers != Vector3.zero) | |
{ | |
velocity = Vector3.Lerp(velocity, collisionAvoidOthers, spawner.collAvoidOtherNeighbor * Time.deltaTime); | |
} | |
return velocity; | |
} | |
/// <summary> | |
/// 判断是否朝向吸引子移动 | |
/// </summary> | |
/// <param name="velocity"></param> | |
/// <returns></returns> | |
private Vector3 JudgeAttract(Vector3 velocity) | |
{ | |
// 指向吸引子的矢量 | |
Vector3 towardAttractor = spawner.boidsAttractor.transform.position - this.transform.position; | |
//true: 远离吸引子 | |
bool isTowardAttractor = towardAttractor.magnitude > spawner.attractPushDistance; | |
// 指向吸引子的矢量,长度为速度的大小 | |
Vector3 attractVelocity = towardAttractor.normalized * spawner.velocity; | |
// 分情况求方向 | |
if (isTowardAttractor) | |
{ | |
velocity = Vector3.Lerp(velocity, attractVelocity, spawner.attractPull * Time.deltaTime); | |
} | |
else | |
{ | |
velocity = Vector3.Lerp(velocity, -attractVelocity, spawner.attractPush * Time.deltaTime); | |
} | |
// 取生成器中的速度大小 | |
velocity = velocity.normalized * spawner.velocity; | |
return velocity; | |
} | |
/// <summary> | |
/// 个体间避免碰撞 | |
/// </summary> | |
/// <returns></returns> | |
private Vector3 CollisionAvoid() | |
{ | |
Vector3 velocityAvoid = Vector3.zero; | |
if (neighborhood.AverageCollisionPositionNeighbors != Vector3.zero) | |
{ | |
velocityAvoid = this.transform.position - neighborhood.AverageCollisionPositionNeighbors; | |
velocityAvoid = velocityAvoid.normalized * spawner.velocity; | |
} | |
return velocityAvoid; | |
} | |
/// <summary> | |
/// 与其他物体避免碰撞 | |
/// </summary> | |
/// <returns></returns> | |
private Vector3 CollisionAvoidOthers() | |
{ | |
Vector3 velocityAvoid = Vector3.zero; | |
if (neighborhood.AverageCollisionPositionOthers != Vector3.zero) | |
{ | |
velocityAvoid = this.transform.position - neighborhood.AverageCollisionPositionOthers; | |
velocityAvoid = velocityAvoid.normalized * spawner.velocity; | |
} | |
return velocityAvoid; | |
} | |
/// <summary> | |
/// 速度匹配 | |
/// </summary> | |
/// <returns></returns> | |
private Vector3 VelocityMatch() | |
{ | |
Vector3 velocityAlign = neighborhood.AverageVelocity; | |
if (velocityAlign != Vector3.zero) | |
{ | |
velocityAlign = velocityAlign.normalized * spawner.velocity; | |
} | |
return velocityAlign; | |
} | |
/// <summary> | |
/// 中心聚集 | |
/// </summary> | |
/// <returns></returns> | |
private Vector3 VelocityCenter() | |
{ | |
Vector3 velocityCenter = neighborhood.AveragePositionNeighbors; | |
if (velocityCenter != Vector3.zero) | |
{ | |
velocityCenter -= this.transform.position; | |
velocityCenter = velocityCenter.normalized * spawner.velocity; | |
} | |
return velocityCenter; | |
} | |
} | |
} |
# 测试
现在 Boid 看起来像是蜜蜂。你可以更改 Spawner 的数值以达到不同的效果。下表中列出了一些有趣的版本:
Default | Sparse follow | Small groups | Formation | |
---|---|---|---|---|
velocity | 30 | 30 | 30 | 30 |
neighborDistance | 30 | 30 | 8 | 30 |
collDistance | 4 | 10 | 2 | 10 |
velocityMatching | 0.25 | 0.25 | 0.25 | 0.25 |
flockCenter | 0.2 | 0.2 | 8 | 0.2 |
collAvoidNeighbor | 2 | 4 | 10 | 4 |
collAvoidOtherNeighbor | 10 | 10 | 10 | 10 |
attractPull | 2 | 1 | 1 | 3 |
attractPush | 2 | 2 | 20 | 2 |
attractPushDistance | 5 | 20 | 20 | 1 |
默认配置的演示(白色方块是障碍物):