Unity_网格碰撞器-MeshCollider-触发器-Trigger 网格碰撞器(MeshCollider)内部没有触发检测 只在表面有触发检测

Unity的触发器功能很好,但是也有问题。我来讲讲问题以及解决方案。

先上视频给大家看看效果:

自制触发器

首先约定几个特殊名词。

基元游戏物体:Cube、Sphere之类的。

基元碰撞器:BoxCollider、SphereCollider之类的。

基元触发器:把基元碰撞体的IsTrigger选中,就是基元触发器。

高精度网格碰撞器:网格碰撞器不选中Convex,选中Convex会把网格碰撞器凹陷处闭合掉的,这就谈不上高精度。

重叠:也就是触发状态。

用基元触发器来触发一个高精度网格碰撞器:

它只会在基元触发器与高精度网格碰撞器表面有接触时,才会认为是触发状态:

Unity_网格碰撞器-MeshCollider-触发器-Trigger 网格碰撞器(MeshCollider)内部没有触发检测 只在表面有触发检测_第1张图片

一旦在基元触发器进入到高精度网格碰撞器内部,竟然会判定二者是不触发的,这就很不好:

Unity_网格碰撞器-MeshCollider-触发器-Trigger 网格碰撞器(MeshCollider)内部没有触发检测 只在表面有触发检测_第2张图片

为了解决这个问题,我苦思冥想,总算想出来一个一定程度上还能行的通的办法,只能说一定程度上行的通,在某些复杂环境下会失效,具体怎么失效,最后再讲。

先讲解决方案的思路,那就是用多道射线来进行内外检测,就是这么朴实无华,我就以图中的Cube和牙齿模型为示例来讲解吧。

把Cube的BoxCollider移除掉,它是无用的东西。牙齿模型的高精度网格碰撞器仍然保留。然后给Cube设置六面点(六个面的中间点)和八顶点(八个角的顶点),这些点用空物体代替就行:

Unity_网格碰撞器-MeshCollider-触发器-Trigger 网格碰撞器(MeshCollider)内部没有触发检测 只在表面有触发检测_第3张图片

重要的地方来了: 

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.如果多个高精度网格碰撞器本身就重叠,类似于这样,那么就有可能出错,因为明明立方体在牙齿最内部,但由于其他牙齿插入了这一个牙齿中,穿透射线检测到了最近的点是其他的牙齿,那么就判断错误了:

Unity_网格碰撞器-MeshCollider-触发器-Trigger 网格碰撞器(MeshCollider)内部没有触发检测 只在表面有触发检测_第4张图片

好了,如果我的博客对你有帮助,请点个赞收藏吧,谢谢!有问题可以留言哦。

你可能感兴趣的:(Unity3D,unity,游戏引擎,网格碰撞器,触发器,MeshCollider)