**写在前面**:这次所写的内容是最近在做项目的时候解决的一个问题,从分析问题到中间各种尝试再到最后完完全全解决这个问题,花费了我一番功夫。故特意在此记录下来以便后顾。
问题描述:我们在打篮球或者踢足球时常常会遇到这样一种情况,就是当一个队员持球想传给另一个队员时,另一名队员被人防守/阻挡住了,通常情况下我们的策略都是让被防守的队员跑动到一个新的位置再传球。那么在程序中要怎么模拟并实现这一情况呢?这就是我所要解决的问题。
下面进入正题,在unity中关于这个问题可以用这样一副图来表示:
1.利用射线碰撞检测实现“防守/阻挡”判断:在模拟上述情况时我们要做的第一件事就是先确定什么情况下己方球员算是被防守/阻挡住了,从图上来看只要两个己方球员的连线上有别的障碍物就可以认为是被阻挡住了。对于这种情况我第一反应是使用射线进行碰撞检测,在两个球员之间做若干条射线,若射线碰撞到了两个球员以外的物体,就可以认为是有障碍物存在于两者之间,从而达到目的。这里就要使用到unity提供的一套射线碰撞检测API:Raycast()系列函数。我是用的是RaycastAll (ray : Ray, distance : float , layerMask : int ),其中ray为射线,我们可以在两个球员身上取相应的点(还记得上一篇提到的avator骨骼节点么)做射线,距离就是两点之间的距离,同时为了减少运算量,我们还可以把球员,你想要检测的障碍物等设置为一个统一的layer,RaycastAll函数最后一个参数就可以控制unity只检测这一个layer的碰撞,从而减少运算量(PS:如果你设置layer全部为第10层,则layerMask需要设置为 1 << 10,unity通过检测该参数每一位是否为1来确定层级)。我们首先对这个函数进行一个小小的封装:
RaycastHit[] CalaculateRayCast(Vector3 origin, Vector3 direction)
{
Ray ray = new Ray(origin, direction);
RaycastHit[] RH = Physics.RaycastAll(ray, direction.magnitude, 1 << LayerMask.NameToLayer("Collider"));
return RH;
}
关于返回值RH,这其实是一个碰撞的信息数组,你可以通过它获取与射线发生碰撞的碰撞器(RH.colloder)但是如果该射线与多个碰撞器碰撞,unity将不保证返回的碰撞器的顺序,需要自己一个个去检测。那么接下来就是在两个球员身上取点了,我们首先提供一个目标球员的Transform接口,为拿球的队员提供一个owner接口
//Owner owner
//Transform passTarget
public bool CheckTargetPass()
{
string TargetName = passTarget.gameObject.name;
Vector3 direction = passTarget.position - owner.transform.position;
direction.y = 0;
//add some check points
List<HumanBodyBones> HBB = new List<HumanBodyBones>();
HBB.Add(HumanBodyBones.Chest);
HBB.Add(HumanBodyBones.LeftShoulder);
HBB.Add(HumanBodyBones.RightShoulder);
foreach (HumanBodyBones hbb in HBB)
{
foreach (RaycastHit RCH in CalaculateRayCast(owner.PA.GetBoneTransform(hbb).position, direction))
{
if (RCH.collider.gameObject.name != TargetName && RCH.collider.gameObject.name != owner.name && RCH.collider.gameObject.tag != owner.tag)
{
Debug.Log("collider:" + RCH.collider.gameObject.name);
return true;
}
}
}
return false;
}
这里我首先计算出direction向量,即利用passTarget.position - owner.transform.position得到。然后可以取拿球队员身上的胸口(chest),左手(lefthand),右手(righthand)三个点作为原点发射射线并进行阻挡检测,并对拿到的碰撞信息数组RH进行遍历,如果发现有其他的碰撞器存在,则确认有阻挡存在。至此,该问题的第一部分算是基本解决了。接下来就是下一个问题,既然我们已经知道被阻挡住了,那么被挡住的队员那个方向移动呢?
2.利用二维法向量计算新的移动方向:对于防守队员来说,摆脱防守最简单的方法就是沿垂直于两者连线的方向移动,如图:
这里我们又需要注意一点,垂直的方向有两个,选哪一个呢?这就可以根据障碍物的位置来选择了,我们先从目标点到拿球队员做向量V1,再从原目标点做一个到障碍物的向量V2,两个向量的y分量取0,再叉乘,得到一个y方向的向量,通过y方向的正负就可以判断v1到v2是顺时针还是逆时针从而得到新目标点的方向,至于那条垂直与v1的向量,则可以利用点乘的知识解决,这里不再赘述。
//Owner owner
//Transform passTarget
Vector3 CalculateNewPos()
{
Vector3 v1 = owner.PA.GetBoneTransform(HumanBodyBones.Chest).position - passTarget.position;
v1.y = 0;
RaycastHit hit;
Physics.Raycast(passTarget.position, owner.PA.GetBoneTransform(HumanBodyBones.Chest).position-passTarget.position,out hit);
Vector3 v2 = hit.transform.position - passTarget.position;
v2.y = 0;
Vector3 v3 = Vector3.Cross(v1, v2);
if (v3.y > 0)
{
Vector3 direction = passTarget.position - owner.transform.position;
Vector3 newPosition = passTarget.position +
new Vector3(direction.z * Mathf.Tan(15 / Mathf.Rad2Deg), 0, -direction.x * Mathf.Tan(15 / Mathf.Rad2Deg));
passTarget.transform.position = newPosition;
return newPosition;
}
else
{
Vector3 direction = passTarget.position - owner.transform.position;
Vector3 newPosition = passTarget.position +
new Vector3(-direction.z * Mathf.Tan(15 / Mathf.Rad2Deg), 0, direction.x * Mathf.Tan(15 / Mathf.Rad2Deg));
passTarget.transform.position = newPosition;
return newPosition;
}
}
上面代码里我以v1为边做了一个15度的直角三角形,三角形里剩下的那个顶点即为新的目标点。至此,问题基本解决。