# 群落算法

实现鱼群、鸟群、蜜蜂等群落的模拟。

群落应符合以下规则:

  • 避免与邻近个体发生碰撞
  • 避免与障碍物发生碰撞
  • 趋向与邻近的个体采用相同的速度和方向
  • 向邻近个体的平均位置靠近

# 准备

  1. 个体(Boid):准备好模型并添加基础组件
    img
    img
    img
    img
    img
  2. 吸引子(Attractor):空物体,用于挂载吸引子脚本,它是一个群落的中心
  3. 生成器(Spawner):空物体,用于挂载生成器脚本
  4. 障碍物(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 的数值以达到不同的效果。下表中列出了一些有趣的版本:

DefaultSparse followSmall groupsFormation
velocity30303030
neighborDistance3030830
collDistance410210
velocityMatching0.250.250.250.25
flockCenter0.20.280.2
collAvoidNeighbor24104
collAvoidOtherNeighbor10101010
attractPull2113
attractPush22202
attractPushDistance520201

默认配置的演示(白色方块是障碍物):
gif