Unity球形视野+Boids群算法

Unity球型视野研究

射线分布

Unity中进行视野检测,使用射线是一个很方便的方法。问题是如何有效的生成射线?事实证明,学好数学还是很有必要的。先看2D的情况:

for( int i = 0; i < numPoints; i++) {
    float dst = Mathf.Pow( i / ( numPoints - 1f),pow);
    float angle = 2 * Mathf.PI * turnFraction * i;
    float x = dst * Mathf.Cos( angle );
    float y = dst * Mathf.Sin( angle );
    PlotPoint( x, y );
}

以上代码,当turnFraction的值为0.618(黄金分割率),pow=0.5时,可以生成如下的图像:
在这里插入图片描述
是不是很漂亮?怎么弄成3D的呢??下面就是3D的!!

public class Boid : MonoBehaviour
{
    // 方向数组是静态的,只需要初始化一次,所有单元共享
    private static Vector3 [] m_ObstanceRayDirection = null;
    private void Awake()
    {
        // 如果没有初始化过,就初始化方向
        if( m_ObstanceRayDirection == null )
        {
            List<Vector3> dirs = new List<Vector3>();
            // 这里使用120个点;实际是60个,因为要去掉朝后的射线。
            for( int i = 1; i < 120; ++ i )
            {
                float t = i / 119f; // 120 - 1
                float inc = Mathf.Acos(1f - 2f * t);
                float z = Mathf.Cos(inc);
                if (z > 0) // 只关心朝前的方向,忽略朝后的方向
                {
                    float az = 2f * Mathf.PI * 1.618f * i; // 黄金分割率+1
                    float x = Mathf.Sin(inc) * Mathf.Cos(az);
                    float y = Mathf.Sin(inc) * Mathf.Sin(az);
                    dirs.Add(new Vector3(x,y,z).normalized);
                }
            }
            m_ObstanceRayDirection = dirs.ToArray();
        }
    }
    private void Update()
    {
        foreach( var dir in m_ObstanceRayDirection )
        {
            Debug.DrawRay(transform.position, transform.TransformDirection(dir), Color.red, BoidsManager.ObstanceCheckRadius);
        }
    }
}

于是,就是下面的样子了:
在这里插入图片描述

射线检测

有了那些射线,就可以进行障碍物检测了:

private Vector3 CheckObstances()
{
    Vector3 bestDir = transform.forward;
    float maxDis = 0;
    foreach( var dir in m_ObstanceRayDirection )
    {
        Vector3 tdir = eays.TransformDirection(dir);
        if (Physics.Raycast(eays.position, tdir, out RaycastHit hit, BoidsManager.ObstanceCheckRadius, m_ObstanceLayerMask ))
        {
            float dis = hit.distance;
            if( dis > maxDis )
            {
                bestDir = tdir;
                maxDis = dis;
            }
        }
        else
            return tdir;
    }
    return bestDir;
}

这段代码其实挺简单,枚举那些事先初始化好的方向,逐一判断该方向上是否遇到障碍物,如果遇到没有发现障碍的,就返回这个方向,如果所有的方向上都有障碍物,就返回那个距离最长的。这个方法的另一个好处是,因为方向数组在初始化时,是按照中心螺旋向外的,因此,它恰巧可以实现寻找“最小转弯”(代价最小)的那个方向。。

Boids行为

有了上面避障碍的方法,Boid已经可以在世界中自由的飞翔了。只不过,现在看起来它们不具有集群的行为,只是一味的向前跑,遇到障碍就找个代价最小的方向(转弯角度最小)躲避障碍。要想增加集群行为,需要增加一些规则:

  1. 对齐(Alignment) 其实就是计算临近的Boids的平均方向。
  2. 排斥(separation) 就是说Boids之间如果距离太近,就要有互斥的力将他们分开,不能挤作一团。
  3. 目标(Target) 这个是可选的,可以让所有的Boid趋向这个目标移动。
  4. 还可以加入你想到的其他规则。
    代码如下:
private void CalcNeighbors(out Vector3 alignment, out Vector3 separation)
{
    alignment = transform.forward; // 计算平均方向算上自己
    separation = Vector3.zero;
    int alicount = 0;
    int sepcount = 0// 用球体检测周围的Boid
    foreach( var coll in Physics.OverlapSphere(transform.position, BoidIncubator.NeighborRadius, m_BoidLayerMask))
    {
        // 获取Boid组件
        Boid neighbor = coll.GetComponent<Boid>();
        if( neighbor != null )
        {
            // 计算邻居到自己的偏移
            Vector3 offset =  neighbor.transform.position - transform.position;
            
            // 为了更加真实,这里使用点乘来判断目标邻居是不是在自己身后,真实情况是如果在身后那必然看不见它,所以不去管它
            if (Vector3.Dot(transform.forward, offset.normalized) > 0)
            {
                alignment += neighbor.transform.forward;
                ++alicount;
                
                // 如果邻居距离自己过近,则产生斥力
                float dis = offset.magnitude;
                if (dis < BoidIncubator.SeparationRadius)
                {
                    // 斥力是与便宜是相反的,所以是减
                    separation -= offset;
                    ++sepcount;
                }
            }
        }
    }
    if (alicount > 0)
    {
        // 计算平均
        alignment /= alicount;
        if (sepcount > 0)
            separation /= sepcount;
    }
}

这一下就有了第一条和第二条规则
至于目标,那太简单了:

Vector3 targetDir = ( Target - transform.position ).normalized;

然后,把上面这些计算好的方向,都乘以一个系数(为了可以调整权重),然后再全部加起来,就是最终下一帧的方向。当然,为了躲避障碍,应该让障碍躲避规则最优先,如果检测到障碍,那么就不管什么对齐了,先躲了障碍再说。。最后,确定好最终的方向后,为了更平滑的转向,可以采用插值的方法,插值方法可以参考下面两个算法:
方法一:

// 计算出当前的方向和下一帧的方向之间的角度
float angle = Vector3.Angle( transform.forward, nextDir );
// 根据最大转身速度,求得旋转angle角度时需要的时间
float needTime = angle / m_RotationSpeed;
// 计算当前应该设置的角度
transform.forward = Vector3.Slerp(transform.forward, targetDir, delta / needTime)

方法二:

transform.forward += ( targetDir - transform.forward ) * delta;

好了,现在可以有一大群鱼了。。但是这种方法不适合大量的集群。。
大量的集群,应该使用ECS系统。。

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

示申○言舌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值