基于Unity利用四叉树算法实现二维碰撞检测

前言

在游戏制作过程中会经常遇到碰撞检测,假设在二维平面上有n个物体,那么检测每个物体的碰撞都要检测n-1次,如果要检测所有物体的碰撞,那么需要计算n*(n-1)/2次,时间复杂度为n的平方,四叉树算法可以将物体分到不同的区域,从而减少计算次数。关于四叉树 的概念可以参考文章四叉树与八叉树

四叉的实现

下面是用C#代码实现的四叉树算法,需要注意的是构造函数的pBounds参数,不能直接使用RectTransform.rect,这里使用的rect的参数x和y需要特殊处理,具体可以看末尾函数public static Rect GetRect(RectTransform rectTransform)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class QuadTree
{
    //节点内允许的最大对象
    private int MAX_OBJECTS = 1;
    //最大层级
    private int MAX_LEVELS = 3;
    //当前层级
    private int level;
    //当前层级内的对象
    public List<RectTransform> rectTrans;
    //rect范围
    private Rect bounds;
    //子节点
    private List<QuadTree> childs;

    public QuadTree(Rect pBounds, int pLevel = 0, int maxObjs = 1, int maxLevel = 3)
    {
        level = pLevel;
        rectTrans = new List<RectTransform>();
        bounds = pBounds;
        childs = new List<QuadTree>();
        MAX_OBJECTS = maxObjs;
        MAX_LEVELS = maxLevel;
    }
    /// <summary>
    /// 清理四叉树
    /// </summary>
    public void Clear()
    {
        rectTrans.Clear();
        for (int i = 0; i < childs.Count; i++)
        {
            childs[i].Clear();
        }
    }
    /// <summary>
    /// 分割四叉树
    /// </summary>
    public void Split()
    {
        float halfWidth = bounds.width / 2;
        float halfHeight = bounds.height / 2;
        float x = bounds.x;
        float y = bounds.y;
        childs.Add(new QuadTree(new Rect(x, y + halfHeight, halfWidth, halfHeight), level + 1, MAX_OBJECTS, MAX_LEVELS));
        childs.Add(new QuadTree(new Rect(x + halfWidth, y + halfHeight, halfWidth, halfHeight), level + 1, MAX_OBJECTS, MAX_LEVELS));
        childs.Add(new QuadTree(new Rect(x, y, halfWidth, halfHeight), level + 1, MAX_OBJECTS, MAX_LEVELS));
        childs.Add(new QuadTree(new Rect(x + halfWidth, y, halfWidth, halfHeight), level + 1, MAX_OBJECTS, MAX_LEVELS));
        //对象下沉
        for (int i = 0; i < rectTrans.Count; i++)
        {
            Insert(rectTrans[i]);
        }
        rectTrans.Clear();
    }
    /// <summary>
    /// 寻找对象所在节点列表
    /// </summary>
    /// <param name="rect">对象的rect</param>
    /// <returns></returns>
    public List<QuadTree> GetIndexes(Rect rect)
    {
        List<QuadTree> ret = new List<QuadTree>();
        FindQuad(rect, ret);
        return ret;
    }

    /// <summary>
    /// 插入对象
    /// </summary>
    /// <param name="go"></param>
    public void Insert(RectTransform go)
    {
        Rect rect = GetRect(go);
        List<QuadTree> tempList = GetIndexes(rect);
        for (int i = 0; i < tempList.Count; i++)
        {
            QuadTree quad = tempList[i];
            quad.rectTrans.Add(go);
            //判断对象是否可以分割
            if (quad.rectTrans.Count > MAX_OBJECTS && quad.level < MAX_LEVELS)
            {
                quad.Split();
            }
        }
    }

    /// <summary>
    /// 判断两个矩形是否重合
    /// </summary>
    /// <param name="rect1"></param>
    /// <param name="rect2"></param>
    /// <returns></returns>
    public static bool RectCollision(Rect rect1, Rect rect2)
    {
        float minx = Mathf.Max(rect1.x, rect2.x);
        float miny = Mathf.Max(rect1.y, rect2.y);
        float maxx = Mathf.Min(rect1.x + rect1.width, rect2.x + rect2.width);
        float maxy = Mathf.Min(rect1.y + rect1.height, rect2.y + rect2.height);
        if (minx > maxx || miny > maxy) return false;
        return true;
    }

    /// <summary>
    /// 寻找Rect所在的节点列表
    /// </summary>
    /// <param name="point"></param>
    /// <returns></returns>
    public void FindQuad(Rect rect,List<QuadTree> quadTrees)
    {
        if (bounds.Overlaps(rect))
        {
            if (childs.Count == 0)
            {
                quadTrees.Add(this);
            }
            else
            {
                for (int i = 0; i < childs.Count; i++)
                {
                    childs[i].FindQuad(rect, quadTrees);
                }
            }
        }
    }

    /// <summary>
    /// 获取与对象go相交的对象
    /// </summary>
    /// <param name="go"></param>
    /// <returns></returns>
    public List<RectTransform> GetCollisions(RectTransform go)
    {
        HashSet<RectTransform> set = new HashSet<RectTransform>();
        Rect rect = GetRect(go);
        var tempList = GetIndexes(rect);
        for (int i = 0; i < tempList.Count; i++)
        {
            for (int j = 0; j < tempList[i].rectTrans.Count; j++)
            {
                if (tempList[i].rectTrans[j] != go && !set.Contains(tempList[i].rectTrans[j]))
                {
                    Rect rect1 = GetRect(tempList[i].rectTrans[j]);
                    if (rect1.Overlaps(rect))
                    {
                        set.Add(tempList[i].rectTrans[j]);
                    }
                }
            }
        }
        return new List<RectTransform>(set);
    }

    /// <summary>
    /// 获取所有相交的对象
    /// </summary>
    /// <returns></returns>
    public Dictionary<RectTransform, HashSet<RectTransform>> GetAllCollisions()
    {
        Dictionary<RectTransform, HashSet<RectTransform>> ret = new Dictionary<RectTransform, HashSet<RectTransform>>();
        List<QuadTree> leafs = new List<QuadTree>();
        GetAllLeaf(leafs);
        for (int i = 0; i < leafs.Count; i++)
        {
            for (int j = 0; j < leafs[i].rectTrans.Count; j++)
            {
                for (int k = j + 1; k < leafs[i].rectTrans.Count; k++)
                {
                    if (ret.ContainsKey(leafs[i].rectTrans[j]) && ret[leafs[i].rectTrans[j]].Contains(leafs[i].rectTrans[k])) continue;
                    if (ret.ContainsKey(leafs[i].rectTrans[k]) && ret[leafs[i].rectTrans[k]].Contains(leafs[i].rectTrans[j])) continue;
                    Rect rect1 = GetRect(leafs[i].rectTrans[j]);
                    Rect rect2 = GetRect(leafs[i].rectTrans[k]);
                    if (rect1.Overlaps(rect2))
                    {
                        if (!ret.ContainsKey(leafs[i].rectTrans[j]))
                        {
                            ret.Add(leafs[i].rectTrans[j], new HashSet<RectTransform>());
                        }
                        ret[leafs[i].rectTrans[j]].Add(leafs[i].rectTrans[k]);
                    }
                }
            }
        }
        return ret;
    }
    /// <summary>
    /// 获取所有叶子节点
    /// </summary>
    /// <param name="ret"></param>
    public void GetAllLeaf(List<QuadTree> ret)
    {
        if (childs.Count == 0)
        {
            if (rectTrans.Count > 1)
                ret.Add(this);
        }
        else
        {
            for (int i = 0; i < childs.Count; i++)
            {
                childs[i].GetAllLeaf(ret);
            }
        }
    }
    /// <summary>
    /// 获取对象的rect
    /// </summary>
    /// <param name="rectTransform"></param>
    /// <returns></returns>
    public static Rect GetRect(RectTransform rectTransform)
    {
        Rect rect = rectTransform.rect;
        return new Rect(rectTransform.position.x + rect.x, rectTransform.position.y + rect.y, rect.width, rect.height);
    }
}

测试

写好四叉树算法后,需要在实际场景进行测试
测试环境:
Unity 2020.3
Win10
CPU:i5-10400F

创建一个需要碰撞检测的预制体

在这里插入图片描述
这里创建一个Image,附加一个让其自由移动的脚本。

public class RandomMove : MonoBehaviour
{
    float stopTime;
    float moveTime;
    float vel_x, vel_y, vel_z;//速度
    /// <summary>
    /// 最大、最小飞行界限
    /// </summary>
    public float maxPos_x = 500;
    public float maxPos_y = 300;
    public float minPos_x = -500;
    public float minPos_y = -300;
    int curr_frame;
    int total_frame;
    float timeCounter1;
    float timeCounter2;
    // int max_Flys = 128;
    // Use this for initialization
    void Start()
    {
        Change();

    }

    // Update is called once per frame
    void Update()
    {
        timeCounter1 += Time.deltaTime;
        if (timeCounter1 < moveTime)
        {
            transform.Translate(vel_x, vel_y, 0, Space.Self);
        }
        else
        {
            timeCounter2 += Time.deltaTime;
            if (timeCounter2 > stopTime)
            {
                Change();
                timeCounter1 = 0;
                timeCounter2 = 0;
            }
        }
        Check();
    }
    void Change()
    {
        stopTime = Random.Range(1, 5);
        moveTime = Random.Range(1, 20);
        vel_x = Random.Range(-10, 10) * 0.01f;
        vel_y = Random.Range(-10, 10) * 0.01f;
    }
    void Check()
    {
        //如果到达预设的界限位置值,调换速度方向并让它当前的坐标位置等于这个临界边的位置值
        if (transform.localPosition.x > maxPos_x)
        {
            vel_x = -vel_x;
            transform.localPosition = new Vector3(maxPos_x, transform.localPosition.y, 0);
        }
        if (transform.localPosition.x < minPos_x)
        {
            vel_x = -vel_x;
            transform.localPosition = new Vector3(minPos_x, transform.localPosition.y, 0);
        }
        if (transform.localPosition.y > maxPos_y)
        {
            vel_y = -vel_y;
            transform.localPosition = new Vector3(transform.localPosition.x, maxPos_y, 0);
        }
        if (transform.localPosition.y < minPos_y)
        {
            vel_y = -vel_y;
            transform.localPosition = new Vector3(transform.localPosition.x, minPos_y, 0);
        }
    }
}

在场景中加载200个碰撞体,并检测碰撞

为了实时表现碰撞,在两个碰撞的对象之间画一条红线

public class TestQuad : MonoBehaviour
{
    public GameObject Image;
    QuadTree quadTree;
    List<RectTransform> rectTransforms;
    List<LineRenderer> lineRenderers;
    public int cNums = 200;
    private void Start()
    {
        
        rectTransforms = new List<RectTransform>();
        lineRenderers = new List<LineRenderer>();
        Rect sc = Screen.safeArea;
        quadTree = new QuadTree(sc,0,2,5);
        
        for(int i = 0; i < cNums; i++)
        {
            GameObject go = Instantiate(Image);
            RectTransform rectT = go.GetComponent<RectTransform>();
            //随机生成位置
            float x = Random.Range(0,sc.width);
            float y = Random.Range(0,sc.height);
            rectT.position = new Vector3(x, y, 0);
            go.transform.SetParent(transform);
            var move = go.GetComponent<RandomMove>();
            move.maxPos_x = sc.width / 2;
            move.maxPos_y = sc.height / 2;
            move.minPos_x = -move.maxPos_x;
            move.minPos_y = -move.maxPos_y;
            rectTransforms.Add(rectT);
        }
    }
    private void Update()
    {
        //清理树
        quadTree.Clear();
        //插入节点
        for (int i = 0; i < rectTransforms.Count; i++)
        {
            quadTree.Insert(rectTransforms[i]);
        }
        //获取所有交集
        var temps = quadTree.GetAllCollisions();
        List<Vector3> point = new List<Vector3>();
        int linet = 0;
        //画线
        foreach (var kv in temps)
        {
            if (linet == lineRenderers.Count)
            {
                GameObject go = new GameObject("line_"+linet);
                lineRenderers.Add(go.AddComponent<LineRenderer>());
            }
            LineRenderer lineRenderer = lineRenderers[linet++];
            lineRenderer.gameObject.SetActive(true);
            lineRenderer.positionCount = 0;
            foreach (var v in kv.Value)
            {
                //显示线条(z为-1,才能显示出来)
                lineRenderer.SetPosition(lineRenderer.positionCount++, kv.Key.position + new Vector3(0,0,-1));
                lineRenderer.SetPosition(lineRenderer.positionCount++, v.position + new Vector3(0,0,-1));
            }
        }

        while (linet < lineRenderers.Count)
        {
            lineRenderers[linet++].gameObject.SetActive(false);
        }
    }
}

运行结果如下
请添加图片描述

与暴力法的性能对比

现将Update内的代码,修改如下

int linet = 0;
for (int i = 0; i < rectTransforms.Count; i++)
{
    if (linet == lineRenderers.Count)
    {
        GameObject go = new GameObject("line_" + linet);
        lineRenderers.Add(go.AddComponent<LineRenderer>());
    }
    LineRenderer lineRenderer = lineRenderers[linet++];
    lineRenderer.gameObject.SetActive(true);
    lineRenderer.positionCount = 0;
    for (int j = i + 1; j < rectTransforms.Count; j++)
    {
        Rect rect1 = QuadTree.GetRect(rectTransforms[i]);
        Rect rect2 = QuadTree.GetRect(rectTransforms[j]);
        if (rect1.Overlaps(rect2))
        {
            lineRenderer.SetPosition(lineRenderer.positionCount++, rectTransforms[i].position + new Vector3(0, 0, -1));
            lineRenderer.SetPosition(lineRenderer.positionCount++, rectTransforms[j].position + new Vector3(0, 0, -1));
        }
    }
}

while (linet < lineRenderers.Count)
{
    lineRenderers[linet++].gameObject.SetActive(false);
}

运行结果如下
请添加图片描述

可以看到使用四叉树,运行帧数在260以上,而暴力法帧数在90左右,效果明显。

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值