阅读《Unity Game AI programming 》第6章后,感觉躲避障碍物算法不是很给力。为了研究和学习Unity,自己改良躲避障碍物的算法。当然,代码没有优化,不过没关系,抛砖引玉,记录思想,学习交流。
1.启用物理引擎,使用速度和力解决问题。而不是使用的角度和位置
2.加入沿着障碍物行走,即使障碍物宽度很大或物体向障碍物前进的速度过快,也不会发生穿墙而过现象。
3.为配合沿着障碍物行走,并且显得更自然,引入三个速度与障碍物的交互分区。
public void AvoidObstacles (ref Vector3 dir)
{
RaycastHit hit;
//Only detect layer 8 (Obstacles)
int layerMask = 1 << 8;
//Check that the vehicle hit with the obstacles within it's minimum distance to avoid
//print (transform.forward);
//障碍物交互区
if (Physics.Raycast (transform.position, rb.velocity, out hit, minimumDistToAvoid, layerMask)) {
alongWall = false;
Vector3 hitNormal = hit.normal;
hitNormal.y = 0.0f; //Don't want to move in Y-Space
disToAvoid = hit.distance;
//[5-minimumDistToAvoid]排斥区
if (disToAvoid > 5) {
print ("排斥区");
if (rb.velocity.sqrMagnitude > 100)
rb.AddForce (hitNormal * 10);
}
//[2-5.0]牵引区
else if (disToAvoid > 2f) {
print ("牵引区");
Vector3 pullForce = Vector3.Cross (hitNormal, Vector3.up).normalized;
if (Vector3.Dot (rb.velocity.normalized, pullForce) > 0)
rb.AddForce (pullForce * 5);
else
rb.AddForce (-pullForce * 5);
//return;
}
//[0,2]平行区
else {
print ("平行区");
Vector3 paraSpeed = Vector3.Cross (hitNormal, Vector3.up).normalized;
alongWall = true;
if (Vector3.Dot (rb.velocity.normalized, paraSpeed) > 0)
rb.velocity = paraSpeed * 10;
else
rb.velocity = -paraSpeed * 10;
alongHitNormal = -hitNormal;
//return;
}
}
//目标交互区
else if (Physics.Raycast (transform.position, dir, out hit, Vector3.Distance (transform.position, targetPoint), layerMask)) {
if (alongWall) {
if (!Physics.Raycast (transform.position, alongHitNormal, out hit, 2.0f, layerMask)) {
print ("脱离平行区,开始转向");
rb.AddForce (dir * 10);
}
} else if (hit.distance > minimumDistToAvoid) {
print ("正常");
rb.velocity = dir * 8;
}
} else {
print ("畅通无阻");
rb.velocity = dir * 10;
}
}
这个算法依旧存在缺陷,最大问题是运算量较大,并且细节不够丰富,导致物体移动行为不自然。
当逐渐逼近障碍物时,首先进入排斥区,对移动物施加障碍物平面法线方向的斥力。再次逼近改为施加平行于障碍物平面的引导力。若再次逼近则将移动物体的速度直接更改为平行于障碍物平面的速度。平行于障碍物平面的速度是用向量叉乘以及点乘求得的,Unity向量叉乘遵守左手坐标系,为了让行为更自然需要用点乘纠正方向。
当物体沿着障碍物移动时,则不做任何事情,直到脱离障碍物,开始施加向目标点力。若物体没有沿着墙壁前进,并且通向目标一定范围内没有障碍物。则直接更改速度。
若发现目标点畅通无阻则直接更改速度向其前进。
把此段代码引入到原书项目中,可test效果。有一个问题需要注意一下,由于移动物体时存在体积的,所以单纯以物体position进行射线检测会在拐角处发生碰撞,为了简化问题,开启移动物体isTrigger选项。
下面是优化可读性的代码,类似switch-case状态机,由于运动状态的转换图比较复杂,这里引入一个中央的状态管理器。
public void AvoidObstacles ()
{
StateManager ();
switch (state) {
case AvoidState.ForMax:
UpdateForMax ();
break;
case AvoidState.Normal:
UpdateNormal ();
break;
case AvoidState.Parallel:
UpdateParallel ();
break;
case AvoidState.Pull:
UpdatePull ();
break;
case AvoidState.Push:
UpdatePush ();
break;
}
}
private void StateManager ()
{
RaycastHit hit;
int layerMask = 1 << 8;
if (Physics.Raycast (transform.position, rb.velocity, out hit, minimumDistToAvoid, layerMask)) {
hitNormal = hit.normal;
var avoidDis = hit.distance;
if (avoidDis > 5)
state = AvoidState.Push;
else if (avoidDis > 2)
state = AvoidState.Pull;
else
state = AvoidState.Parallel;
return;
}
if (state == AvoidState.Parallel) {
if (!Physics.Raycast (transform.position, -hitNormal, out hit, 2.0f, layerMask)) {
state = AvoidState.Normal;
}
return;
}
if (Physics.Raycast (transform.position, dir, out hit, Vector3.Distance (transform.position, targetPoint), layerMask)) {
state = AvoidState.Normal;
} else
state = AvoidState.ForMax;
}
private void UpdatePush ()
{
if (rb.velocity.sqrMagnitude > 100)
rb.AddForce (hitNormal * 10);
}
private void UpdatePull ()
{
Vector3 pullForce = Vector3.Cross (hitNormal, Vector3.up).normalized;
if (Vector3.Dot (rb.velocity.normalized, pullForce) > 0)
rb.AddForce (pullForce * 5);
else
rb.AddForce (-pullForce * 5);
}
private void UpdateForMax ()
{
rb.velocity = Vector3.RotateTowards (rb.velocity, dir * 10, 10f * Time.deltaTime, 10f * Time.deltaTime);
}
private void UpdateParallel ()
{
Vector3 paraSpeed = Vector3.Cross (hitNormal, Vector3.up).normalized;
if (Vector3.Dot (rb.velocity.normalized, paraSpeed) > 0)
rb.velocity = paraSpeed * 10;
else
rb.velocity = -paraSpeed * 10;
}
private void UpdateNormal ()
{
rb.velocity = Vector3.RotateTowards (rb.velocity, dir * 8, 10f * Time.deltaTime, 10f * Time.deltaTime);
}