Boids 可以理解为类似鸟群的东西,就是多个个体之间的相互作用。在游戏开发中经常会用到。例如 RTS 游戏,控制一个坦克战队,如何保持行进方向的一致性,以及坦克之间互相有一定有间隔,又不会间隔太大,这里就可以使用 Boids 相关的理论来实现。

在众多这方面的文章中,基本上会涉及到三个方面,跟随分离聚合。跟随,就是说整个群体有一个行进的大方向。分离,则是个体与个体之间有一定的间隔,不至于发生碰撞。而聚合,就是个体不能离群体太远,不能脱离群体。

对于上面提到的三个方面,简单来说,就是一个力的叠加。

跟随

一个鸟群往哪个方向飞,可以假设有一只领头的鸟,其他的鸟跟随这只鸟的方向。知道领头的鸟的方向,知道自己当前的飞行方向,就可以计算出应该向中个方向施加一个力,可以使自己的方向,偏向于领头的鸟的方向。

分离

分离,是要保证个体之间不要离的太近,不要发生碰撞。先考虑两个物体的情况,假设要使物体 A 远离物体 B,只要从 B,向 A 施加一个推力,就可以将 A 推离 B。那如果 A 要同时和 B 与 C 保持距离呢?一样的,只需要从 B 和 C 分别向 A 施加一个推力,这两个的合力,就是 A 远离 B 和 C 的方向。同理,不管 A 要与多少个物体保持距离,只需要从每个物体出发,向 A 的方向施加一个力,就可以将 A 推开。

上面只考虑了 A 远离其他物体的情况,如果每一个物体都要与其他物体保持距离呢?一样的,只需要从每一个其他物体,向自己的方向施加一个力,这个合力,就是自己运动的方向。

聚合

为了保证个体不脱离群体,还需要一个聚合力。就是将个体自身,推向群体中心的力。

分离的力和聚合的力一定程度上抵消,从而达到个体之间即保持了距离,又保证了每一个体不脱离群体。

下面的代码是我的一个小游戏项目中的,其中只用到了分离和聚合。只要懂了原理,就可以根据具体的情况灵活变通,达到自己想要的效果即可。

Boids

using UnityEngine;
using System.Collections.Generic;
using System.Collections;

public class GamePlayFlock : MonoBehaviour
{
    public static List<GamePlayFlock> flockList = new List<GamePlayFlock>();

    public static void StartAllFlock()
    {
        for(int i = 0; i < flockList.Count; ++i)
        {
            flockList[i].StartFlock();
        }
    }

    public float avoidForce;
    public float randomForce;
    public float toCenterForce;
    public float randomFreq;

    public Vector3 centerPos;
    public float borderDist;
    public float avoidDist;

    public Vector2 borderRect;

    private Vector3 randomVelocity;
    private bool started = false;

    public void InitFlock(Vector3 centerPos, float xBorderDist,float yBorderDist, float avoidDist)
    {
        this.centerPos = centerPos;
        this.borderDist = xBorderDist;
        this.borderRect = new Vector2(xBorderDist, yBorderDist);
        this.avoidDist = avoidDist;

        flockList.Add(this);
    }

    private void StartFlock()
    {
        StartCoroutine(UpdateRandomVelocity());
        started = true;
    }

    // 随机方向的力,这个保证了个体不至于完全一样
    private IEnumerator UpdateRandomVelocity()
    {
        while (true)
        {
            randomVelocity = Random.insideUnitSphere * randomForce;
            randomVelocity.z = 0;
            float wait = randomFreq + Random.Range(-randomFreq / 2.0f, randomFreq / 2.0f);
            yield return new WaitForSeconds(wait);
        }
    }

    private void Update()
    {
        if (!started)
        {
            return;
        }

        Vector3 avoidVelocity = Vector3.zero;
        float dist = 0;

        foreach (GamePlayFlock flock in flockList)
        {
            if (flock.transform != transform)
            {
                Vector3 otherPos = flock.transform.position;
                Vector3 dir = transform.position - otherPos;
                 dist = dir.magnitude;
                if (dist < avoidDist)
                {
                    float f = 1.0f - (dist / avoidDist);
                    if (dist > 0)
                    {
                        avoidVelocity += (dir / dist) * f * avoidForce;
                    }
                }
            }
        }

        Vector3 toCenterDir = centerPos - transform.position;
        Vector3 centerVelocity = Vector3.zero;
        float xDist = Mathf.Abs(toCenterDir.x);
        float yDist = Mathf.Abs(toCenterDir.y);

        if(xDist > borderRect.x)
        {
            float f = xDist / borderRect.x - 1.0f;
            centerVelocity.x = (toCenterDir.x / xDist) * f * toCenterForce;
        }

        if(yDist > borderRect.y)
        {
            float f = yDist / borderRect.y - 1.0f;
            centerVelocity.y = (toCenterDir.y / yDist) * f * toCenterForce;
        }

        Vector3 velocity = Vector3.zero;
        velocity += avoidVelocity;
        velocity += randomVelocity;
        velocity += centerVelocity;

        transform.position += velocity * Time.deltaTime;
    }
}