Unity的触发器功能很好,但是也有问题。我来讲讲问题以及解决方案。
先上视频给大家看看效果:
自制触发器
首先约定几个特殊名词。
基元游戏物体:Cube、Sphere之类的。
基元碰撞器:BoxCollider、SphereCollider之类的。
基元触发器:把基元碰撞体的IsTrigger选中,就是基元触发器。
高精度网格碰撞器:网格碰撞器不选中Convex,选中Convex会把网格碰撞器凹陷处闭合掉的,这就谈不上高精度。
重叠:也就是触发状态。
用基元触发器来触发一个高精度网格碰撞器:
它只会在基元触发器与高精度网格碰撞器表面有接触时,才会认为是触发状态:
一旦在基元触发器进入到高精度网格碰撞器内部,竟然会判定二者是不触发的,这就很不好:
为了解决这个问题,我苦思冥想,总算想出来一个一定程度上还能行的通的办法,只能说一定程度上行的通,在某些复杂环境下会失效,具体怎么失效,最后再讲。
先讲解决方案的思路,那就是用多道射线来进行内外检测,就是这么朴实无华,我就以图中的Cube和牙齿模型为示例来讲解吧。
把Cube的BoxCollider移除掉,它是无用的东西。牙齿模型的高精度网格碰撞器仍然保留。然后给Cube设置六面点(六个面的中间点)和八顶点(八个角的顶点),这些点用空物体代替就行:
重要的地方来了:
1.首先从遥远处(我这里设置的一百米之外)给立方体的六面点和八顶点分别射出一道射线,总计十四道外围射线。如果十四道外围射线全部在中途被同一游戏物体遮挡住,那就认为立方体在牙齿的内部,设置立方体的触发状态为真。
2.再从立方体的内部检测,六面点和八顶点,那些互相对应的点(对顶点),来回发射射线,也是总计14道射线。为什么要来回发?不能只从A点给B点发,还需要B点给A点发,因为要考虑到万一A点到B点是从网格碰撞器内部到外部,这样会检测不到网格碰撞器的。最终,这14道射线,有任何一道射线被遮挡,那就认为立方体与牙齿表面重叠,设置立方体的触发状态为真。
满足上述两种状况中的任何一种,都认为立方体与牙齿发生了触发状态。
这个博客最重要讲思路,不要局限于是不是只有立方体可以这样做。事实上其他基元游戏物体和网格碰撞器、其他复杂模型和网格碰撞器之间,都可以用这样的思路去做。
或者对于内部检测这块,你可以不用射线,而是继续用Unity本身的触发器,因为内部检测本来就是检测表面重合的,而Unity自带的触发检测是可以做到的。
代码如下:
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
///
/// 立方体与网格碰撞器的重叠检测
///
public class Cube_MeshColliderTrigger : MonoBehaviour
{
Transform selftTransform;
public Transform SelftTransform
{
get
{
if (selftTransform == null)
{
selftTransform = transform;
}
return selftTransform;
}
}
///
/// 六面点
///
public Transform[] sixPoints;
///
/// 八顶点
///
public Transform[] eightPoints;
///
/// 从远至近-六面点射线检测结果
///
bool[] sixPointsHit_FarToNear = new bool[6];
///
/// 从远至近-八顶点射线检测结果
///
bool[] eightPointsHit_FarToNear = new bool[8];
///
/// 从远处射向立方体的14道射线全部被同一物体遮挡
///
[Header("外部射线全部被同一物体遮挡")]
public bool allBlock_FarToNear = false;
///
/// 内部检测-六面点六射线检测结果(本来是六面点三射线,但两个点需要互相重复射,否则就只是一个方向)
///
bool[] sixPointsHit_Inside = new bool[6];
///
/// 内部检测-八顶点八射线检测结果(本来是八顶点四射线,但两个点需要互相重复射,否则就只是一个方向)
///
bool[] eightPointsHit_Inside = new bool[8];
///
/// 内部检测的14道射线至少有一道被遮挡
///
[Header("内部射线至少一道被遮挡")]
public bool atLeastOneBlock_Inside = false;
///
/// 最终重叠结果
///
[Header("最终重叠结果")]
public bool overlap = false;
///
/// 射线起点
///
Vector3 start;
///
/// 射线终点
///
Vector3 end;
///
/// 射线碰撞点数组存储空间
///
RaycastHit[] raycastHitsSpace = new RaycastHit[100];
///
/// 射线碰撞点数组的真实个数
///
int realRaycastHitsCount = 0;
///
/// 距离最近的碰撞点
///
RaycastHit nearestRaycastHit;
///
/// 外部射线在本轮所检测到的游戏物体集合
///
List blocks_FarToNear = new List();
private void Update()
{
CubeTrigger();
}
void CubeTrigger()
{
//每一轮开始前清空此集合
blocks_FarToNear.Clear();
//从遥远的地方向立方体发射6道射线至6个表面的中心点
for (int i = 0; i < sixPoints.Length; i++)
{
start = SelftTransform.position + (sixPoints[i].position - SelftTransform.position).normalized * 100;
end = sixPoints[i].position;
//RaycastNonAlloc是可以检测射程范围内的所有碰撞体的,它不会被挡住
//在这里用这个射线,是为了应对一种特殊情况,那就是立方体完全在牙齿模型内部,理论上14道外部射线都应该射到这个模型上的
//但事实上很有可能发生以下状况:10道射线射到了牙齿模型上,有4道被场景中的其他物体挡住了,这就会导致我们的如下定律失效,因为当前状况是射到了不同的游戏物体
//定律:如果十四道外围射线全部在中途被同一游戏物体遮挡住,那就认为立方体在牙齿的内部,设置立方体的触发状态为真
realRaycastHitsCount = Physics.RaycastNonAlloc(start, end - start, raycastHitsSpace, 100-Vector3.Distance(sixPoints[i].position, SelftTransform.position));
if (realRaycastHitsCount == 0)
{
sixPointsHit_FarToNear[i] = false;
}
else
{
sixPointsHit_FarToNear[i] = true;
}
if (sixPointsHit_FarToNear[i])
{
//查找碰撞点中距离立方体最近的碰撞点,因为是能射穿所有物体的射线,途中可能射穿多个物体,那离立方体最近的碰撞点,才是最准的,其他点有可能是途中的其他物体
//计算第一个碰撞点和立方体的距离,并假设第一个即是最近的点
float minDistance = Vector3.Distance(SelftTransform.position, raycastHitsSpace[0].point);
nearestRaycastHit = raycastHitsSpace[0];
for (int n = 0; n < realRaycastHitsCount; n++)
{
float netxDistance = Vector3.Distance(SelftTransform.position, raycastHitsSpace[n].point);
if (minDistance > netxDistance)
{
minDistance = netxDistance;
nearestRaycastHit = raycastHitsSpace[n];
}
}
Debug.DrawLine(start, nearestRaycastHit.point, Color.magenta, Time.deltaTime);
//判断此集合中是否包含碰撞到的物体
if (blocks_FarToNear.Contains(nearestRaycastHit.transform))
{
//已经包含就什么都不做
}
else
{
//没包含就添加进集合
blocks_FarToNear.Add(nearestRaycastHit.transform);
//print("六面点" + nearestRaycastHit.transform.name);
}
}
else
{
Debug.DrawLine(start, end, Color.yellow, Time.deltaTime);
}
}
//从遥远的地方向立方体发射8道射线至8个顶点
for (int i = 0; i < eightPoints.Length; i++)
{
start = SelftTransform.position + (eightPoints[i].position - SelftTransform.position).normalized * 100;
end = eightPoints[i].position;
realRaycastHitsCount=Physics.RaycastNonAlloc(start, end - start, raycastHitsSpace, 100-Vector3.Distance(eightPoints[i].position, SelftTransform.position));
if (realRaycastHitsCount == 0)
{
eightPointsHit_FarToNear[i] = false;
}
else
{
eightPointsHit_FarToNear[i] = true;
}
if (eightPointsHit_FarToNear[i])
{
//查找碰撞点中距离立方体最近的碰撞点,因为是能射穿所有物体的射线,途中可能射穿多个物体,那离立方体最近的碰撞点,才是最准的,其他点有可能是途中的其他物体
//计算第一个碰撞点和立方体的距离,并假设第一个即是最近的点
float minDistance = Vector3.Distance(SelftTransform.position, raycastHitsSpace[0].point);
nearestRaycastHit = raycastHitsSpace[0];
for (int n = 0; n < realRaycastHitsCount; n++)
{
float netxDistance = Vector3.Distance(SelftTransform.position, raycastHitsSpace[n].point);
if (minDistance > netxDistance)
{
minDistance = netxDistance;
nearestRaycastHit = raycastHitsSpace[n];
}
}
Debug.DrawLine(start, nearestRaycastHit.point, Color.magenta, Time.deltaTime);
//判断此集合中是否包含碰撞到的物体
if (blocks_FarToNear.Contains(nearestRaycastHit.transform))
{
//已经包含就什么都不做
}
else
{
//没包含就添加进集合
blocks_FarToNear.Add(nearestRaycastHit.transform);
//print("八顶点" + nearestRaycastHit.transform.name);
}
}
else
{
Debug.DrawLine(start, end, Color.yellow, Time.deltaTime);
}
}
//计算从远处射向立方体的14道射线是否全部被遮挡
allBlock_FarToNear = true;
foreach (var item in sixPointsHit_FarToNear)
{
if (!item)
{
allBlock_FarToNear = false;
break;
}
}
if (allBlock_FarToNear)
{
foreach (var item in eightPointsHit_FarToNear)
{
if (!item)
{
allBlock_FarToNear = false;
break;
}
}
}
//如果到了这里allBlock_FarToNear仍然为真,说明外围14道射线的确已经被全部遮挡,但还并不能说明立方体就在某个物体内部,因为有可能有一大堆游戏物体把立方体包围住了,需要结合具体碰撞到的游戏物体进行判断
//allBlock_FarToNear为真,blocks_FarToNear集合的元素数量一定是大于0的,但未必大于1。
if (allBlock_FarToNear)
{
//如果数量大于1,那就说明碰到的是多个游戏物体,再结合上面的穿透性射线,就可以得出立方体并不在物体内部。
if (blocks_FarToNear.Count > 1)
{
allBlock_FarToNear = false;
}
//如果数量为1,说明碰到的都是同一物体,那就可以视作在物体内部。
else if (blocks_FarToNear.Count==1)
{
//此句可以不写,我为了对仗好看写的
allBlock_FarToNear = true;
}
}
//内部检测六面点六射线
for (int i = 0; i < sixPoints.Length - 1; i += 2)
{
//射线碰撞点
RaycastHit raycastHit;
//顺序射线检测
start = sixPoints[i].position;
end = sixPoints[i].position + (sixPoints[i + 1].position - sixPoints[i].position);
sixPointsHit_Inside[i] = Physics.Linecast(start, end, out raycastHit);
if (sixPointsHit_Inside[i])
{
Debug.DrawLine(start, raycastHit.point, Color.blue, Time.deltaTime);
}
else
{
Debug.DrawLine(start, end, Color.green, Time.deltaTime);
}
//倒序射线检测
start = sixPoints[i + 1].position;
end = sixPoints[i + 1].position + (sixPoints[i].position - sixPoints[i + 1].position);
sixPointsHit_Inside[i + 1] = Physics.Linecast(start, end, out raycastHit);
if (sixPointsHit_Inside[i + 1])
{
Debug.DrawLine(start, raycastHit.point, Color.blue, Time.deltaTime);
}
else
{
Debug.DrawLine(start, end, Color.green, Time.deltaTime);
}
}
//内部检测八顶点八射线
for (int i = 0; i < eightPoints.Length - 1; i += 2)
{
//射线碰撞点
RaycastHit raycastHit;
//顺序射线检测
start = eightPoints[i].position;
end = eightPoints[i].position + (eightPoints[i + 1].position - eightPoints[i].position);
eightPointsHit_Inside[i] = Physics.Linecast(start, end, out raycastHit);
if (eightPointsHit_Inside[i])
{
Debug.DrawLine(start, raycastHit.point, Color.blue, Time.deltaTime);
}
else
{
Debug.DrawLine(start, end, Color.green, Time.deltaTime);
}
//倒序射线检测
start = eightPoints[i + 1].position;
end = eightPoints[i + 1].position + (eightPoints[i].position - eightPoints[i + 1].position);
eightPointsHit_Inside[i + 1] = Physics.Linecast(start, end, out raycastHit);
if (eightPointsHit_Inside[i + 1])
{
Debug.DrawLine(start, raycastHit.point, Color.blue, Time.deltaTime);
}
else
{
Debug.DrawLine(start, end, Color.green, Time.deltaTime);
}
}
//计算内部检测的14道射线是否至少有一道被遮挡
atLeastOneBlock_Inside = false;
foreach (var item in sixPointsHit_Inside)
{
if (item)
{
atLeastOneBlock_Inside = true;
break;
}
}
if (atLeastOneBlock_Inside == false)
{
foreach (var item in eightPointsHit_Inside)
{
if (item)
{
atLeastOneBlock_Inside = true;
break;
}
}
}
///14道外部射线全部被阻挡或者内部检测有至少一条射线被阻挡,满足其中任一条件,都说明立方体与网格碰撞器重叠
if (allBlock_FarToNear || atLeastOneBlock_Inside)
{
overlap = true;
}
else
{
overlap = false;
}
if (overlap)
{
// print("重叠");
}
else
{
// print("脱离");
}
}
}
总之,一切如上。
最后再来讲讲我这个东西在最根本的原理上有哪些问题,据我观测,是两个地方:
1.外部射线全部被同一物体挡住即视为在物体内部,这一假设,只能适用于你的游戏物体是大石头之类的实心存在。如果你的游戏物体是墙壁很厚的空心房子或者类似的东西,此假设就会把你导向错误的结果。如果你做一个鬼抓人的游戏,想凭借我的办法判断鬼是在房间里还是在墙壁里,那恐怕不行。
2.如果多个高精度网格碰撞器本身就重叠,类似于这样,那么就有可能出错,因为明明立方体在牙齿最内部,但由于其他牙齿插入了这一个牙齿中,穿透射线检测到了最近的点是其他的牙齿,那么就判断错误了:
好了,如果我的博客对你有帮助,请点个赞收藏吧,谢谢!有问题可以留言哦。