射线分布
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已经可以在世界中自由的飞翔了。只不过,现在看起来它们不具有集群的行为,只是一味的向前跑,遇到障碍就找个代价最小的方向(转弯角度最小)躲避障碍。要想增加集群行为,需要增加一些规则:
- 对齐(Alignment) 其实就是计算临近的Boids的平均方向。
- 排斥(separation) 就是说Boids之间如果距离太近,就要有互斥的力将他们分开,不能挤作一团。
- 目标(Target) 这个是可选的,可以让所有的Boid趋向这个目标移动。
- 还可以加入你想到的其他规则。
代码如下:
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系统。。