Unity 四叉树QuadTree应用之碰撞检测

目录

1.简介

2.树的结构

3.构造原则

4.重要接口

5.示例

6.压测

7.更新


1.简介

最近在研究四叉树的二维空间碰撞检测,随笔记录一下

四叉树顾名思义,就是每科树都包含4个分支,每个分支可以看做一个区域象限,用来存放一些数据的空间索引

这里只讨论四叉树在二维空间的碰撞检测,Unity的实现方式

2.树的结构

    // 区域象限定义
    public enum QuadrantType
    {
        /// <summary>
        /// 左上
        /// </summary>
        LT = 0,
        /// <summary>
        /// 右上
        /// </summary>
        RT = 1,
        /// <summary>
        /// 右下
        /// </summary>
        RB = 2,
        /// <summary>
        /// 左下
        /// </summary>
        LB = 3,
    }

    public enum QuadrantBitType
    {
        /// <summary>
        /// 左上
        /// </summary>
        LT = 1 << 0,
        /// <summary>
        /// 右上
        /// </summary>
        RT = 1 << 1,
        /// <summary>
        /// 右下
        /// </summary>
        RB = 1 << 2,
        /// <summary>
        /// 左下
        /// </summary>
        LB = 1 << 3,
    }

    public interface IRect
    {
        /// <summary>
        /// 矩形的中心坐标x
        /// </summary>
        float x { get; set; }
        /// <summary>
        /// 矩形的中心坐标y
        /// </summary>
        float y { get; set; }
        /// <summary>
        /// 矩形的宽
        /// </summary>
        float width { get; set; }
        /// <summary>
        /// 矩形的高
        /// </summary>
        float height { get; set; }
    }

    public interface IMark
    {
        /// <summary>
        /// 对象待比较标记(1:待比较)
        /// </summary>
        int mark { get; set; }
    }

    public class QTreeComparer<T> : IComparer<QTree<T>> where T : IRect
    {
        public int Compare(QTree<T> x, QTree<T> y)
        {
            if (x.depth > y.depth)
                return 1;
            else if (x.depth == y.depth)
                return 0;
            else
                return -1;
        }
    }

    // 四叉树结构
    public class QTree<T> : IRect where T : IRect
    {
        public float x { get; set; }
        public float y { get; set; }
        public float width { get; set; }
        public float height { get; set; }

        public int depth;              // 树的深度
        public int childCount;         // 对象数量
        public bool isLeaf;            // 是否叶子节点
        public List<T> childList;      // 对象引用
        public QTree<T>[] childNodes;  // 子节点数组(4个)

        public QTree()
        {
            Init();
        }

        public QTree(int depth)
        {
            Init();
            this.depth = depth;
        }

        private void Init()
        {
            childList = new List<T>();
            isLeaf = true;
            childCount = 0;
        }

        public void InitRect(float x, float y, float width, float height)
        {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }

        public void Clear()
        {
            if (isLeaf)
            {
                childList.Clear();
                childList = null;
            }
            else
            {
                for (int i = 0; i < childNodes.Length; ++i)
                {
                    childNodes[i].Clear();
                    childNodes[i] = null;
                }
                childNodes = null;
            }
        }
    }

3.构造原则

1.默认构造的QTree树节点都是叶子节点

2.对象只会放入叶子节点

3.每个叶子节点可以存放的对象上限是 MAXCHILDCOUNT,下同(在深度没有达到最大值的情况下,否则不限

4.当叶子节点存放的对象数量超过 MAXCHILDCOUNT,则分离生成子树(4个区域象限),并把自身存储的所有对象存放到子树中,然后标记为非叶子节点,清除自身所有保存的对象引用

5.根据对象的坐标及宽高插入QTree树中(边界对象可能会加入多个QTree树中

4.重要接口


        /// <summary>
        /// 插入四叉树
        /// </summary>
        public void InsertQTree<T>(QTree<T> node, T t) where T : IRect, IMark
        {
            if (node.isLeaf)
            {
                if (node.depth < maxDepth && node.childCount + 1 > maxChildCount)
                {
                    // 分裂树(象限)
                    SplitQTree(node);
                    InsertQTree(node, t);
                }
                else
                {
                    node.childList.Add(t);
                    node.childCount++;
                }
            }
            else
            {
                int indexs = GetTargetQuadrantIndex<T>(node, t);
                if (indexs > 0)
                {
                    int indexArea = indexs & (int)QuadrantBitType.LT;
                    if (indexArea == (int)QuadrantBitType.LT)
                        InsertQTree<T>(node.childNodes[(int)QuadrantType.LT], t);
                    indexArea = indexs & (int)QuadrantBitType.RT;
                    if (indexArea == (int)QuadrantBitType.RT)
                        InsertQTree<T>(node.childNodes[(int)QuadrantType.RT], t);
                    indexArea = indexs & (int)QuadrantBitType.RB;
                    if (indexArea == (int)QuadrantBitType.RB)
                        InsertQTree<T>(node.childNodes[(int)QuadrantType.RB], t);
                    indexArea = indexs & (int)QuadrantBitType.LB;
                    if (indexArea == (int)QuadrantBitType.LB)
                        InsertQTree<T>(node.childNodes[(int)QuadrantType.LB], t);
                }
            }
        }

        /// <summary>
        /// 分离树(象限)
        /// </summary>
        private void SplitQTree<T>(QTree<T> node) where T : IRect, IMark
        {
            node.isLeaf = false;
            float width = node.width * 0.5f;
            float height = node.height * 0.5f;
            node.childNodes = new QTree<T>[4];
            // 左上
            node.childNodes[(int)QuadrantType.LT] = CreateChildNode<T>(node.x - width * 0.5f, node.y + height * 0.5f, width, height, node.depth + 1);
            // 右上
            node.childNodes[(int)QuadrantType.RT] = CreateChildNode<T>(node.x + width * 0.5f, node.y + height * 0.5f, width, height, node.depth + 1);
            // 右下
            node.childNodes[(int)QuadrantType.RB] = CreateChildNode<T>(node.x + width * 0.5f, node.y - height * 0.5f, width, height, node.depth + 1);
            // 左下
            node.childNodes[(int)QuadrantType.LB] = CreateChildNode<T>(node.x - width * 0.5f, node.y - height * 0.5f, width, height, node.depth + 1);

            for(int i = node.childCount - 1; i >= 0 ; --i)
            {
                InsertQTree<T>(node, node.childList[i]);
                node.childList.RemoveAt(i);
                node.childCount--;
            }
        }

        /// <summary>
        /// 创建子树(叶子节点)
        /// </summary>
        private QTree<T> CreateChildNode<T>(float x, float y, float width, float height, int depth) where T : IRect, IMark
        {
            QTree<T> node = new QTree<T>();
            node.depth = depth;
            node.InitRect(x, y, width, height);
            return node;
        }

        /// <summary>
        /// 查询树,并返回包含所有树节点的列表
        /// </summary>
        public void QueryQTreeRetrunList<T>(QTree<T> node, ref List<QTree<T>> qTreeList) where T : IRect, IMark
        {
            qTreeList.Add(node);
            if (!node.isLeaf)
            {
                for(int i = (int)QuadrantType.LT ; i <= (int)QuadrantType.LB ; ++i)
                {
                    QueryQTreeRetrunList<T>(node.childNodes[i], ref qTreeList);
                }
            }
        }

        /// <summary>
        /// 查询树,并返回包含所有树节点的列表(按depth升序排列)
        /// </summary>
        public void QueryQTreeReturnRiseList<T>(QTree<T> node, ref List<QTree<T>> qTreeList) where T : IRect, IMark
        {
            QueryQTreeRetrunList<T>(node, ref qTreeList);
            qTreeList.Sort(new QTreeComparer<T>());
        }

        /// <summary>
        /// 获取目标所在的象限索引列表
        /// </summary>
        public int GetTargetQuadrantIndex<T>(QTree<T> node, T target) where T : IRect, IMark
        {
            float halfWidth = node.width * 0.5f;
            float halfHeight = node.height * 0.5f;
            float min_x = target.x - target.width * 0.5f;
            float min_y = target.y - target.height * 0.5f;
            float max_x = target.x + target.width * 0.5f;
            float max_y = target.y + target.height * 0.5f;

            // 不在当前节点范围内返回null
            if (min_x > node.x + halfWidth || max_x < node.x - halfWidth
                || min_y > node.y + halfHeight || max_y < node.y - halfHeight)
                return 0;
            
            int indexs = 0;
            bool isLeft = min_x <= node.x ? true : false;
            bool isRight = max_x > node.x ? true : false;
            bool isBottom = min_y <= node.y ? true : false;
            bool isTop = max_y > node.y ? true : false;

            if (isLeft)
            {
                // 左上
                if (isTop)
                    indexs = indexs | (int)QuadrantBitType.LT;
                // 左下
                if (isBottom)
                    indexs = indexs | (int)QuadrantBitType.LB;
            }
            if (isRight)
            {
                // 右上
                if (isTop)
                    indexs = indexs | (int)QuadrantBitType.RT;
                // 右下
                if (isBottom)
                    indexs = indexs | (int)QuadrantBitType.RB;
            }

            return indexs;
        }

        /// <summary>
        /// 返回目标周围的可能碰撞对象列表
        /// </summary>
        public void FindTargetAroundObjs<T>(QTree<T> node, T target) where T : IRect, IMark
        {
            if (node.isLeaf)
            {
                for (int i = 0; i < node.childList.Count; i++)
                {
                    node.childList[i].mark = 1;
                }
            }
            else
            {
                int indexs = GetTargetQuadrantIndex<T>(node, target);
                if (indexs > 0)
                {
                    int indexArea = indexs & (int)QuadrantBitType.LT;
                    if (indexArea == (int)QuadrantBitType.LT)
                        FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.LT], target);
                    indexArea = indexs & (int)QuadrantBitType.RT;
                    if (indexArea == (int)QuadrantBitType.RT)
                        FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.RT], target);
                    indexArea = indexs & (int)QuadrantBitType.RB;
                    if (indexArea == (int)QuadrantBitType.RB)
                        FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.RB], target);
                    indexArea = indexs & (int)QuadrantBitType.LB;
                    if (indexArea == (int)QuadrantBitType.LB)
                        FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.LB], target);
                }
            }
        }

5.示例

如上图:黄色方块为跟随鼠标移动的目标对象,绿色方块为待与黄色方块做碰撞计算的对象,红色方块为已发生碰撞的对象

6.压测

下图为最大深度4、根节点深度1,10000个对象的插入、查询测试:

如上图:10000个对象的四叉树插入平均耗时14ms,查询计算对象700多个大概5ms,300多个大概1.5ms

7.更新

优化了如下两个接口,移除了List造成的GC

1.优化了GetTargetQuadrantIndex接口,移除了原有返回的List<int>,改为int,位枚举保存

2.新增了IMark接口:用来标记待比较对象,FindTargetAroundObjs后,在调用段通过for循环找出所有的待比较对象

调用段示例:

public class Element : IRect, IMark
{
    public int id { get; set; }
    public ColorType color { get; set; }
    public bool isMoving { get; private set; }
    public float from_x { get; private set; }
    public float from_y { get; private set; }
    public float to_x { get; private set; }
    public float to_y { get; private set; }
    public float duration { get; private set; }

    public float x { get; set; }
    public float y { get; set; }
    public float width { get; set; }
    public float height { get; set; }
    public int mark { get; set; }

    private float factorSpeed;
    private float factor;

    public void Init(int id, float x, float y, float width, float height)
    {
        this.id = id;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = 0;
    }

    public void Move(float from_x, float from_y, float to_x, float to_y, float duration)
    {
        if (duration <= 0)
            return;

        //Debug.Log($"id = {id}, duration = {duration}, from_x = {from_x}, from_y = {from_y}, to_x = {to_x}, to_y = {to_y}");

        this.from_x = from_x;
        this.from_y = from_y;
        this.to_x = to_x;
        this.to_y = to_y;
        this.duration = duration;
        isMoving = true;
        factorSpeed = 1f / duration;
        factor = 0;
        x = from_x;
        y = from_y;
    }

    public void Update(float deltaTime)
    {
        factor += deltaTime * factorSpeed;
        if (factor >= 1f)
        {
            isMoving = false;
            x = to_x;
            y = to_y;
        }
        else
        {
            x = to_x * factor + from_x * (1f - factor);
            y = to_y * factor + from_y * (1f - factor);
        }
    }
}    

    private void FindTargetAroundObjs()
    {
        QTreeManager.Insatnce.FindTargetAroundObjs<Element>(root, mouseUIElement.M_Element);
        List<Element> objs = new List<Element>();
        Element element = null;
        for (int i = 0 ; i < mElementList.Count ; i++)
        {
            if (mElementList[i].mark == 1)
            {
                element = mElementList[i];
                objs.Add(element);
            }
        }
        if (objs != null && objs.Count > 0)
        {
            for (int i = 0; i < mElementList.Count; ++i)
            {
                mElementList[i].color = mElementList[i].mark == 1 ? ColorType.Green : ColorType.Default;
                mElementList[i].mark = 0;
            }
            CalculateCollision(mouseUIElement.M_Element, objs);
            for (int i = 0; i < mUIElementList.Count; ++i)
            {
                mUIElementList[i].RefreshColor();
            }
        }
        else
            ResetElementColor();
        mouseUIElement.SetVisable(CheckoutIsCollision(mouseUIElement.M_Element, rootElement));
    }

详见源码:

四叉树源码下载

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值