# 群落算法

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

群落应符合以下规则:

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

# 准备

  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.5f;
        [Tooltip("运动范围")]
        public float radius = 6;
        [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)

  • 控制群落的生成
  • 控制群落的行为
  • 群落中所有个体的父物体
using System.Collections;
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;
        [Tooltip("刷新频率")]
        public float refreshRate = 0.02f;
        private void Start()
        {
            StartCoroutine(InstantiateBoids());
        }
        /// <summary>
        /// 实例化群落
        /// </summary>
        private IEnumerator InstantiateBoids()
        {
            int spawnCount = 0;
            WaitForSeconds wait = new WaitForSeconds(spawnDelay);
            while (spawnCount < numberBoids)
            {
                spawnCount++;
                Instantiate(boidsPrefab, Vector3.zero, Quaternion.identity, this.transform);
                yield return wait;
            }
        }
    }
}

# Neighborhood 脚本

  • 挂载到个体上
  • 在协程中使用球形射线检测,性能比触发器高
  • 用于跟踪所有邻近个体的信息,包括:
    • 邻近个体的平均位置、速度、个体间距
    • 其他邻近物体(障碍物)的平均位置
using System.Collections;
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 Transform tra;
        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.Tra.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.linearVelocity;
                }
                return avg / neighbors.Count;
            }
        }
        /// <summary>
        /// 碰撞距离内的所有个体的平均位置,如果没有则返回 Vector3.zero
        /// </summary>
        public Vector3 AverageCollisionPositionNeighbors
        {
            get
            {
                Vector3 avg = Vector3.zero;
                if (neighbors.Count == 0) return avg;
                // 邻近个体数量
                int nearCount = 0;
                foreach (var neighbor in neighbors)
                {
                    Vector3 pos = neighbor.Tra.position;
                    if (Vector3.Distance(pos, tra.position) <= spawner.collDistance)
                    {
                        avg += pos;
                        nearCount++;
                    }
                }
                return avg / nearCount;
            }
        }
        /// <summary>
        /// 碰撞距离内的所有其他邻近物体的平均位置,如果没有则返回 Vector3.zero
        /// </summary>
        public Vector3 AverageCollisionPositionOthers
        {
            get
            {
                Vector3 avg = Vector3.zero;
                if (neighbors.Count == 0) return avg;
                // 邻近个体数量
                int nearCount = 0;
                foreach (var otherNeighbor in otherNeighbors)
                {
                    Vector3 pos = otherNeighbor.transform.position;
                    if (Vector3.Distance(pos, tra.position) <= spawner.collDistance)
                    {
                        avg += pos;
                        nearCount++;
                    }
                }
                return avg / nearCount;
            }
        }
        private void Awake()
        {
            tra = this.GetComponent<Transform>();
            coll = this.GetComponent<SphereCollider>();
            spawner = tra.parent.GetComponent<Spawner>();
        }
        private void Start()
        {
            neighbors = new List<Boid>();
            otherNeighbors = new List<GameObject>();
            StartCoroutine(UpdateList());
        }
        /// <summary>
        /// 更新邻近个体列表
        /// </summary>
        /// <returns></returns>
        private IEnumerator UpdateList()
        {
            yield return null;
            WaitForSeconds wait = new WaitForSeconds(spawner.refreshRate);
            while (true)
            {
                neighbors.Clear();
                otherNeighbors.Clear();
                RaycastHit[] raycastHits = Physics.SphereCastAll(tra.position, spawner.neighborDistance, tra.forward, spawner.neighborDistance);
                foreach (RaycastHit raycast in raycastHits)
                {
                    if (raycast.transform.TryGetComponent<Boid>(out Boid temp))
                    {
                        neighbors.Add(temp);
                    }
                    else
                    {
                        otherNeighbors.Add(raycast.transform.gameObject);
                    }
                }
                yield return wait;
            }
        }
    }
}

# 个体(Boid)

  • 挂载到个体上
  • 随机初始位置,随机颜色(可选)
  • 利用刚体进行运动
  • 根据 Neighborhood 脚本中的信息计算个体的速度(线性插值)
  • 在协程中实现,性能比 Update 高
using System.Collections;
using UnityEngine;
namespace Boids
{
    /// <summary>
    /// Boids
    /// </summary>
    public class Boid : MonoBehaviour
    {
        /// <summary>
        /// 即使有内置 transform 也需要缓存
        /// 反复调用 this.transform 会有性能损失,因为每次调用都会触发底层的 C++ 到 C# 的转换开销
        /// </summary>
        public Transform Tra { get; private set; }
        public Rigidbody Rig { get; private set; }
        private MeshRenderer mRenderer;
        private TrailRenderer tRenderer;
        private Spawner spawner;
        private Neighborhood neighborhood;
        private void Awake()
        {
            Tra = this.GetComponent<Transform>();
            Rig = this.GetComponent<Rigidbody>();
            mRenderer = this.GetComponentInChildren<MeshRenderer>();
            tRenderer = this.GetComponentInChildren<TrailRenderer>();
            spawner = Tra.parent.GetComponent<Spawner>();
            neighborhood = this.GetComponent<Neighborhood>();
        }
        private void Start()
        {
            Init();
            LookAhead();
            StartCoroutine(UpdateMovement());
        }
        /// <summary>
        /// 初始化
        /// </summary>
        private void Init()
        {
            // 随机初始位置
            Tra.position = Random.insideUnitSphere * spawner.spawnRadius;
            // 随机初始速度
            Rig.linearVelocity = 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 (Material mat in mRenderer.materials)
            {
                mat.color = randomColor_start;
            }
            tRenderer.startColor = randomColor_start;
            tRenderer.endColor = randomColor_end;
        }
        /// <summary>
        /// 更新移动
        /// </summary>
        private IEnumerator UpdateMovement()
        {
            yield return null;
            WaitForSeconds wait = new WaitForSeconds(spawner.refreshRate);
            while (true)
            {
                Move();
                LookAhead();
                yield return wait;
            }
        }
        /// <summary>
        /// 向前看
        /// </summary>
        private void LookAhead()
        {
            Tra.LookAt(Tra.position + Rig.linearVelocity);
        }
        /// <summary>
        /// 移动
        /// </summary>
        private void Move()
        {
            Vector3 velocity = Rig.linearVelocity;
            Vector3 collisionAvoid = CollisionAvoid();
            Vector3 collisionAvoidOthers = CollisionAvoidOthers();
            Vector3 velocityAlign = VelocityMatch();
            Vector3 velocityCenter = VelocityCenter();
            velocity = Vector3.Lerp(velocity, velocityAlign, spawner.velocityMatching * Time.deltaTime);
            velocity = Vector3.Lerp(velocity, velocityCenter, spawner.flockCenter * Time.deltaTime);
            velocity = Vector3.Lerp(velocity, collisionAvoid, spawner.collAvoidNeighbor * Time.deltaTime);
            velocity = Vector3.Lerp(velocity, collisionAvoidOthers, spawner.collAvoidOtherNeighbor * Time.deltaTime);
            Attract(ref velocity);
            velocity = velocity.normalized * spawner.velocity;
            Rig.linearVelocity = velocity;
        }
        /// <summary>
        /// 靠近或远离吸引子
        /// </summary>
        /// <param name="velocity"></param>
        /// <returns></returns>
        private void Attract(ref Vector3 velocity)
        {
            // 指向吸引子的矢量
            Vector3 towardAttractor = spawner.boidsAttractor.transform.position - Tra.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);
            }
        }
        /// <summary>
        /// 个体间避免碰撞
        /// </summary>
        /// <returns></returns>
        private Vector3 CollisionAvoid()
        {
            Vector3 velocityAvoid = Vector3.zero;
            Vector3 averagePosition = neighborhood.AverageCollisionPositionNeighbors;
            if (averagePosition != Vector3.zero)
            {
                velocityAvoid = Tra.position - averagePosition;
                velocityAvoid = velocityAvoid.normalized * spawner.velocity;
            }
            return velocityAvoid;
        }
        /// <summary>
        /// 与其他物体避免碰撞
        /// </summary>
        /// <returns></returns>
        private Vector3 CollisionAvoidOthers()
        {
            Vector3 velocityAvoid = Vector3.zero;
            Vector3 averagePositionOthers = neighborhood.AverageCollisionPositionOthers;
            if (averagePositionOthers != Vector3.zero)
            {
                velocityAvoid = Tra.position - averagePositionOthers;
                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 -= Tra.position;
                velocityCenter = velocityCenter.normalized * spawner.velocity;
            }
            return velocityCenter;
        }
    }
}

# 测试

现在 Boid 看起来像是蜜蜂。你可以更改 Spawner 的数值以达到不同的效果。下表中列出了一些有趣的版本:

DefaultSparse followSmall groupsFormation
velocity30202020
neighborDistance1515415
collDistance410210
velocityMatching0.250.250.250.25
flockCenter0.20.280.2
collAvoidNeighbor24104
collAvoidOtherNeighbor10101010
attractPull2113
attractPush22202
attractPushDistance520201
refreshRate0.020.020.020.02

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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Maikire 微信支付

微信支付

Maikire 支付宝

支付宝

Maikire 贝宝

贝宝