[译]2D空间中使用四叉树Quadtree进行碰撞检测优化

操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Unity2017.2.0f3



原文出处 : Quick Tip: Use Quadtrees to Detect Likely Collisions in 2D Space

许多游戏需要使用碰撞检测算法去判定两个对象是否发生碰撞,但是这些算法通常意味着昂贵操作,拖慢游戏的运行速度。在这篇文章中我们将会学习四叉树 quadtrees,并学习如果通过四叉树跳过那些物理空间距离比较远的对象,最终提高碰撞检测速度。

注:原文中使用Java实现,但是考虑目前多产品是基于Unity3D,故采用C#进行相关实现说明。

IntroDuction



碰撞检测 Collision detection 对于视频游戏来说是非常必要的。无论是2D游戏或是3D游戏中,正确的检测两个物体发生碰撞检测非常重要,否则会出现一切有趣的效果:

然而,碰撞检测是一种非常昂贵的操作。假设有100个对象需要进行碰撞检测时。每两个对象进行比较:100 x 100 = 10000 次,检测的次数实在太多,消耗大量CPU资源。

一种优化途径是减少非必要的碰撞检测的数量。比如屏幕两端的两个物体位于上下两侧,是不可能发生碰撞检测,因此不需要检测它们之间的碰撞。这正是四叉树发挥作用的地方。

What Is a Quadtree?



一个四叉树 quadtree 是一种将2D区域划分为更易于管理的数据结构。他是基于二叉树 binary tree 的扩展,采用四个节点代替两个节点。

在下面的图示中,每个图像是2D空间的一个可视化呈现,红色方块表示对象物体。同时为了更好的说明问题,子节点按照逆时针顺序标记。

一个四叉树开始于单节点(根节点)。此时的根节点还没有进行2D空间的分隔,故添加到四叉树的对象被添加到单节点里。

当更多的对象添加到四叉树时,他最终会进行分裂为四个子节点的形态。每个对象会会根据他们在2D空间中的位置划分到这些子节点中。任何不能完全适合子节点内部边界规则的对象将会被放置在父节点中。

随着对象数量的增加,每个子节点可以继续分裂。

如图所示,每个节点只能包含有限的对象。同时我们了解到,左上角节点中的对象不能与右下角节点中的对象发生碰撞,所以我们不需要在这些节点之间进行昂贵的碰撞检测算法。

Take a look at this JavaScript example 基于Javascript实现的四叉树案例。

Implementing a Quadtree



实现四叉树相对比较简单、容易。下面的代码采用C#编写,注原文基于Java。但是无论啥语言实现理念都是一致的,另外会在每个代码块之后进行注释说明。

我们从创建四叉树的核心类 Quadtree 开始。代码如下所示:

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

public class QuadTree {

    private int MAX_OBJECTS = 1;
    private int MAX_LEVELS = 3;

    private int level;
    private List<SquareOne> objects;
    private Rect bounds;
    private QuadTree[] nodes;

    public QuadTree (int pLevel, Rect pBounds)
    {
        level = pLevel;
        objects = new List<SquareOne>();
        bounds = pBounds;
        nodes = new QuadTree[4];
    }
}

这个类的看上去是比较直观的, MAX_OBJECTS 定义了一个节点所能持有的最大对象数量,如果超过则进行分裂。 MAX_LEVELS 定义了子节点的最大深度。level 是当前节点深度 ( 0 代表最上层节点 ), bounds 代表2D空间的区域面积, 最后 nodes 代表四个子节点的集合。

在这个例子中,四叉树可以容纳的对象是基于矩形形状 Rectangles 的,但是没有任何限制进行自定义。

下面我们实现四叉树的核心五个函数,分别为: Clear Split GetIndex Insert Retrieve

// Clear quadtree
    public void Clear()
    {
        objects.Clear();

        for(int i  = 0; i < nodes.Length; i++)
        {
            if(nodes[i] != null)
            {
                nodes[i].Clear();
                nodes[i] = null;
            }
        }
    }

Clear 函数基于递归的思路清理四叉树每个节点及节点中的对象集合。

// Split the node into 4 subnodes
    private void Split()
    {
        int subWidth = (int)(bounds.width / 2);
        int subHeight = (int)(bounds.height / 2);
        int x = (int)bounds.x;
        int y = (int)bounds.y;

        nodes[0] = new QuadTree(level + 1, new Rect(x + subWidth, y, subWidth, subHeight));
        nodes[1] = new QuadTree(level + 1, new Rect(x, y, subWidth, subHeight));
        nodes[2] = new QuadTree(level + 1, new Rect(x, y + subHeight, subWidth, subHeight));
        nodes[3] = new QuadTree(level + 1, new Rect(x + subWidth, y + subHeight, subWidth, subHeight));
    }

Split 函数用于将当前节点分裂为四个子节点,并对四个节点进行边界裁剪的初始化操作。

private List<int> GetIndexes(Rect pRect)
    {

        List<int> indexes = new List<int>();

        double verticalMidpoint = bounds.x + (bounds.width / 2);
        double horizontalMidpoint = bounds.y + (bounds.height / 2);

        bool topQuadrant = pRect.y >= horizontalMidpoint;
        bool bottomQuadrant = (pRect.y - pRect.height) <= horizontalMidpoint;
        bool topAndBottomQuadrant = pRect.y + pRect.height + 1 >= horizontalMidpoint && pRect.y + 1 <= horizontalMidpoint;

        if(topAndBottomQuadrant)
        {
            topQuadrant = false;
            bottomQuadrant = false;
        }

        // Check if object is in left and right quad
        if(pRect.x + pRect.width + 1 >= verticalMidpoint && pRect.x -1 <= verticalMidpoint)
        {
            if(topQuadrant)
            {
                indexes.Add(2);
                indexes.Add(3);
            }
            else if(bottomQuadrant)
            {
                indexes.Add(0);
                indexes.Add(1);
            }
            else if(topAndBottomQuadrant)
            {
                indexes.Add(0);
                indexes.Add(1);
                indexes.Add(2);
                indexes.Add(3);
            }
        }

        // Check if object is in just right quad
        else if(pRect.x + 1 >= verticalMidpoint)
        {
            if(topQuadrant)
            {
                indexes.Add(3);
            }
            else if(bottomQuadrant)
            {
                indexes.Add(0);
            }
            else if(topAndBottomQuadrant)
            {
                indexes.Add(3);
                indexes.Add(0);
            }
        }
        // Check if object is in just left quad
        else if(pRect.x - pRect.width <= verticalMidpoint)
        {
            if(topQuadrant)
            {
                indexes.Add(2);
            }
            else if(bottomQuadrant)
            {
                indexes.Add(1);
            }
            else if(topAndBottomQuadrant)
            {
                indexes.Add(2);
                indexes.Add(1);
            }
        }
        else
        {
            indexes.Add(-1);
        }

        return indexes;
    }

GetIndex 是四叉树内部的辅助函数。他决定了四叉树中一个对象属于哪个节点,最终将该对象划分到该节点中。

public void Insert(SquareOne sprite)
    {
        SquareOne fSprite = sprite;
        Rect pRect = fSprite.GetTextureRectRelativeToContainer();

        if(nodes[0] != null)
        {
            List<int> indexes = GetIndexes(pRect);
            for(int ii = 0; ii < indexes.Count; ii++)
            {
                int index = indexes[ii];
                if(index != -1)
                {
                    nodes[index].Insert(fSprite);
                    return;
                }
            }

        }

        objects.Add(fSprite);

        if(objects.Count > MAX_OBJECTS && level < MAX_LEVELS)
        {
            if(nodes[0] == null)
            {
                Split();
            }

            int i = 0;
            while(i < objects.Count)
            {
                SquareOne sqaureOne = objects[i];
                Rect oRect = sqaureOne.GetTextureRectRelativeToContainer();
                List<int> indexes = GetIndexes(oRect);
                for(int ii = 0; ii < indexes.Count; ii++)
                {
                    int index = indexes[ii];
                    if (index != -1)
                    {
                        nodes[index].Insert(sqaureOne);
                        objects.Remove(sqaureOne);
                    }
                    else
                    {
                        i++;
                    }
                }
            }
        }
    }

Insert 是每个加入四叉树的对象要执行的函数。该方法首先确定节点是否有子节点,并尝试向子节点添加对象。如果没有子节点或者对象根据边界规则不适合任何子节点的插入操作,则将对象划分到父节点中。

一旦对象添加到某一个节点中,该节点需要进一步判断当前持有的对象数量 是否 超过最大对象持有对象数量,如果是则进行分化。分化节点会导致该节点插入的所有对象重新划分到子节点的操作,如果不满足边界规则,则将对象保留在父节点中。

private List<SquareOne> Retrieve(List<SquareOne> fSpriteList, Rect pRect)
    {
        List<int> indexes = GetIndexes(pRect);
        for(int ii = 0; ii < indexes.Count; ii++)
        {
            int index = indexes[ii];
            if(index != -1 && nodes[0] != null)
            {
                nodes[index].Retrieve(fSpriteList, pRect);
            }    

            fSpriteList.AddRange(objects);
        }

        return fSpriteList;
    }

最后一个 Retrieve 函数,他根据输入的对象返回所有可能发生碰撞的对象集合。该方法将有助于极爱年少碰撞检测对的数量。

Using This for 2D Collision Detection



现在我们已经实现了完整的四叉树,是时候使用他帮助我们减少碰撞检测的数量。

在典型的游戏场景中,我们需要根据传递的 Screen 屏幕边界尺寸来创建合适的四叉树对象。

Quadtree quad = new Quadtree(0, new Rect(0,0,600,600));

在游戏每一帧中,清理四叉树,然后使用 Insert 函数将所有的对象到添加到四叉树中。

当所有的对象添加完毕,你会遍历每个对象,并检索它可能碰撞的对象列表。然后使用碰撞检测算法检查列表中的每个对象与初始对象之间是否真的发生碰撞。

List returnObjects = new List<SqureOne>();
for (int i = 0; i < allObjects.size(); i++) {
  returnObjects.Clear();
  quad.Retrieve(returnObjects, objects.get(i));

  for (int x = 0; x < returnObjects.size(); x++) {
    // Run collision detection algorithm between objects
  }
}

注意:碰撞检测的算法已经超出了本文的讨论范围,这里有一个 文章 进行学习。

Conclusion



碰撞检测通常是一种比较昂贵的操作,可能会对游戏的性能造成挑战。四叉树是一种加速碰撞检测过程的途径,最终使得游戏运行更加流畅。

时间: 2024-11-06 03:52:49

[译]2D空间中使用四叉树Quadtree进行碰撞检测优化的相关文章

2D空间中判断一点是否在三角形内

本来打算做三角形填充多边形,但需要用到耳切法正在看.所以先研究了这个 要注意如果是XY坐标轴的2D空间,要取差乘分量z而不是y. 实现原理是,将三角形ABC三个边(AB,BC,CA)分别与比较点判断差乘,如果这3个差乘结果表示的方向一致,说明就在三角形内. 效果: 代码(Unity3D): using UnityEngine; using System.Collections; using System.Collections.Generic; public class TriangleColl

2D空间中求两圆的交点

出处:https://stackoverflow.com/questions/19916880/sphere-sphere-intersection-c-3d-coordinates-of-collision-points 修改(加入包含和不相交情况的判断): using System.Collections; using System.Collections.Generic; using UnityEngine; public class CircleIntersect : MonoBehav

2D和3D空间中计算两点之间的距离

自己在做游戏的忘记了Unity帮我们提供计算两点之间的距离,在百度搜索了下. 原来有一个公式自己就写了一个方法O(∩_∩)O~,到僵尸到达某一个点之后就向另一个奔跑过去 /// <summary> /// 3维中如何计算两点之间的距离 /// </summary> /// <param name="p1"></param> /// <param name="p2"></param> /// &l

实验8:Problem A: 立体空间中的点(I)

Home Web Board ProblemSet Standing Status Statistics Problem A: 立体空间中的点(I) Problem A: 立体空间中的点(I) Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 1413  Solved: 1104[Submit][Status][Web Board] Description 设计一个平面上的点Point类和3维的点Point_3D类,满足Point_3D类继承自Poin

3D空间中射线与轴向包围盒AABB的交叉检测算法

引言 在上一节中,我讲述了如何实现射线与三角形的交叉检测算法.但是,我们应该知道,在游戏开发中,一个模型有很多的三角形构成,如果要对所有的物体,所有的三角形进行这种检测,就算现在的计算机运算能力,也是无法高效的完成.所以,我们需要通过其他的手段来提早剔除一些不可能发生交叉的物体,这种早退的思想,大量的运用在3D游戏技术中.在本篇文章中,我将像大家讲述如何实现射线与轴向包围盒AABB的交叉检测.如果读者不明白什么是轴向包围盒,请看这篇文章. Ray-AABB交叉检测算法 现如今,有很多的Ray-A

2D空间的Obb碰撞实现

OBB全称Oriented bounding box,方向包围盒算法.其表现效果和Unity的BoxCollider并无二致.由于3D空间的OBB需要多考虑一些情况 这里仅关注2D空间下的OBB. 实现效果: 网上有许多OBB的讲解,其具体步骤也未必一样,我是这么做的 在两个凸多边形中找到一根轴,凸多边形所有在这根轴上的投影点不产生相交,则这两个凸多边形不相交. 这根轴一般取每个边的垂线,逐个投影进行测试. 这里先上一个BOX的版本,如下图: 可以看见在右侧方块的投影轴上,得到了非相交结果 Bo

根据包名,在指定空间中创建对象

输入描述: namespace({a: {test: 1, b: 2}}, 'a.b.c.d') 输出描述: {a: {test: 1, b: {c: {d: {}}}}} function namespace(oNamespace, sPackage) { var packageArr = sPackage.split('.'); var curObj = oNamespace; // 保留对原始对象的引用 for(var i=0, len=packageArr.length; i<len;

【翻译自mos文章】在一个使用uniform size的 本地管理的表空间中建立一个表,为什么会忽略INITIAL 参数?

翻译:Why Does a Table Created in a Locally Managed Tablespace With Uniform Extents Ignore INITIAL? (文档 ID 753662.1) 在一个使用uniform size的 本地管理的表空间中建立一个表,为什么会忽略INITIAL 参数? 适用于: Oracle Database - Enterprise Edition - Version 8.1.5.0 to 11.1.0.7 [Release 8.1

Qt_OpenGL:3D空间中移动图像

Qt_OpenGL:3D空间中移动图像 //.h #ifndef GLWIDGET_H #define GLWIDGET_H #include <QGLWidget> #include <QtOpenGL> class QGLWidget; class QTimer; typedef struct Stars{ public: int r, g, b; GLfloat dist, angle; }Stars; class GLWidget : public QGLWidget {