Unity項目 - Boids集羣模擬算法

1987年Craig W.Reynolds發表一篇名爲《鳥羣、牧羣、魚羣:分佈式行爲模式》的論文,描述了一種很是簡單的、以面向對象思惟模擬羣體類行爲的方法,稱之爲 Boids ,Boids 採用了三個核心的規則:git

  • 排斥性:避免與羣體內鄰近個體發生碰撞
  • 同向性:趨向與鄰近的個體採用相同的速度方向
  • 凝聚向心性:向鄰近個體的平均位置靠近

由此咱們採用Unity來實現算法並演示,演示結果:
github

製做思路

每一個boid對象,每幀都有2個關鍵的表:與該boid鄰近的boids的表及與該boid最近的boids的表。根據兩個表求得一些肯定該boid位置、方向、速度的因素(例如其餘boids的平均速度,其餘boids的平均位置,該boid與其餘boid的平均距離等),根據所提出的三規則,設置各影響因素的權重比例,最終全部影響因素加和成爲肯定的、該boid下一幀的方向、位置、速度。算法

Boid模型的建立與配置

  1. 建立空對象取名Boid,再建立其空對象子物體,取名Fuselage
    • Fuselage->position(0,0,0),Rotation(7.5,0,0),Scale(0.5,0.5,2)
  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
  3. 複製Fuselage建立另外一個名爲Wing的組件添加到Boid,兩個都屬於Boid的子物體
  4. 修改主相機位置到頂視圖大範圍
  5. 將boid設爲預製體,boid 模型製做完畢

腳本配置

  • BoidSpawner.cs 綁定於主相機
  • Boid.cs 綁定於預製體Boid上
  • 主相機檢視面板中,設定 boidPrefab 變量爲預製體 boid

項目地址:Boidsdom

/*-------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分佈式

相關文章
相關標籤/搜索