在2D空间中使用四叉树实现碰撞检测

原文链接:https://gamedevelopment.tutsplus.com/tutorials/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space--gamedev-374

许多游戏需要使用碰撞检测算法来决定两个物体什么时候发生碰撞,但是这些算法很消耗CPU,可能会大幅度降低游戏速度。所以在这篇文章中,我们就来学习有关四叉树的知识,以及我们如何使用它们来跳过一些因为太远不可能会碰到的物体,从而加快碰撞检测的速度。

注意:尽管这个教程是用Java来写的,但你可以使用同样的技术和概念在几乎任何游戏开发环境里。

引言:

碰撞检测是大部分视频游戏的关键部分。不管在2D还是3D游戏里,检测两个物体发生碰撞是非常重要的,一个小小的碰撞检测可以为游戏加分不少。

但是,碰撞检测一种花费巨大的操作。比如说,现在有一百个物体需要检测是否发生了碰撞,两两物体比较后需要执行操作10000次——这样的数量太惊人了!

有一种方法可以加快过程,即减少检测数量。两个物体在屏幕相反的两边是绝对不会发生碰撞的,所以没必要检测他们之间的碰撞。从这里就要进入四叉树了。

什么是四叉树?

四叉树是一种数据结构,被用来将一个2D区域分为更多可管理的范围。它是二叉树的扩展,但是不像二叉树每个节点有两个孩子,它有四个孩子。

在下面的图片中,每个图片代表2D区域的可视范围,红色的正方形代表物体。为了更好的表述这篇文章,子节点的顺序会被标示成如下图所示的顺时针方向。


四叉树起始于单节点。对象会被添加到四叉树的单节点上。

当更多的对象被添加到四叉树里时,它们最终会被分为四个子节点。(我是这么理解的:下面的图片不是分为四个区域吗,每个区域就是一个孩子或子节点)然后每个物体根据他在2D空间的位置而被放入这些子节点中的一个里。任何不能正好在一个节点区域内的物体会被放在父节点。


如果有更多的对象被添加进来,那么每个子节点要继续划分(成四个节点)。


正如你看到的,每个节点仅包括几个物体。这样我们就可以明白前面所说的规则,例如,左上角节点里的物体是不可能和右下角节点里的物体碰撞的。所以我们也就没必要运行消耗很多资源的碰撞检测算法来检验他们之间是否会发生碰撞。
点击这里查看一个JavaScript写的例子来立即了解四叉树。

使用四叉树

使用四叉树是非常简单的。下面的代码使用Java写的,但是同样的技术可以用很多其他编程语言来写。我会在每个代码片段后面注释。
首先我们开始创建一个重要的四叉树的类,下面的代码就是Quadtree.java。

public class Quadtree {

  private int MAX_OBJECTS = 10;
  private int MAX_LEVELS = 5;

  private int level;
  private List objects;
  private Rectangle bounds;
  private Quadtree[] nodes;

/*
  * Constructor
  */
  public Quadtree(int pLevel, Rectangle pBounds) {
   level = pLevel;
   objects = new ArrayList();
   bounds = pBounds;
   nodes = new Quadtree[4];
  }
}

这个Quadtree类很直观。MAX_OBJECTS变量表示在节点分裂前一个节点最多可以存储多少个孩子,MAX_LEVELS定义了四叉树的深度。Level变量指的是当前节点(0就表示是每四个节点的父节点),bounds代表一个节点的2D空间的面积,nodes变量存储四个子节点。
在这个例子里,四叉树每个节点的面积都定义成正方形的,当然你的四叉树节点的面积空间可以为任意形状。然后,我们会使用五个四叉树里会用到的方法,分别为:clear,split,getIndex,insert和retrieve。

/*
* Clears the 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函数,是通过递归来清除四叉树所有节点的所有对象。

/*
* Splits the node into 4 subnodes
*/
private void split() {
   int subWidth = (int)(bounds.getWidth() / 2);
   int subHeight = (int)(bounds.getHeight() / 2);
   int x = (int)bounds.getX();
   int y = (int)bounds.getY();

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

Split方法,就是用来将节点分成相等的四份面积,并用新的边界来初始化四个新的子节点。

/*
* Determine which node the object belongs to. -1 means
* object cannot completely fit within a child node and is part
* of the parent node
*/
private int getIndex(Rectangle pRect) {
   int index = -1;
   double verticalMidpoint = bounds.getX() + (bounds.getWidth() / 2);
   double horizontalMidpoint = bounds.getY() + (bounds.getHeight() / 2);

   // Object can completely fit within the top quadrants
   boolean topQuadrant = (pRect.getY() < horizontalMidpoint && pRect.getY() + pRect.getHeight() < horizontalMidpoint);
   // Object can completely fit within the bottom quadrants
   boolean bottomQuadrant = (pRect.getY() > horizontalMidpoint);

   // Object can completely fit within the left quadrants
   if (pRect.getX() < verticalMidpoint && pRect.getX() + pRect.getWidth() < verticalMidpoint) {
      if (topQuadrant) {
        index = 1;
      }
      else if (bottomQuadrant) {
        index = 2;
      }
    }
    // Object can completely fit within the right quadrants
    else if (pRect.getX() > verticalMidpoint) {
     if (topQuadrant) {
       index = 0;
     }
     else if (bottomQuadrant) {
       index = 3;
     }
   }

   return index;
}

getIndex方法是个四叉树的辅助方法,在四叉树里,他决定了一个节点的归属,通过检查节点属于哪个象限。(最上面第一幅图不是顺时针在一个面积里划分了四块面积,上面标示了他们的序号,这个方法就是算在一个父节点里他的子节点的序号)

/*
* Insert the object into the quadtree. If the node
* exceeds the capacity, it will split and add all
* objects to their corresponding nodes.
*/
public void insert(Rectangle pRect) {
   if (nodes[0] != null) {
     int index = getIndex(pRect);

     if (index != -1) {
       nodes[index].insert(pRect);

       return;
     }
   }

   objects.add(pRect);

   if (objects.size() > MAX_OBJECTS && level < MAX_LEVELS) {
     split();

     int i = 0;
     while (i < objects.size()) {
       int index = getIndex(objects.get(i));
       if (index != -1) {
         nodes[index].insert(objects.remove(i));
       }
       else {
         i++;
       }
     }
   }
}

Insert方法,是将节点聚合在一起的方法。方法首先判断是否有父节点,然后将这个子节点插入父节点的某一序号的孩子上。如果没有子节点,或者这个节点的所属面积不属于任何一个子节点的所属面积,那就将它加入父节点。
一旦对象添加上后,要看看这个节点会不会分裂,可以通过检查对象被加入节点后有没有超过一个节点最大容纳对象的数量。分裂起源于节点可以插入任何对象,这个对象只要符合子节点都可以被加入。否则就加入到父节点。

/*
* Return all objects that could collide with the given object
*/
public List retrieve(List returnObjects, Rectangle pRect) {
   int index = getIndex(pRect);
   if (index != -1 && nodes[0] != null) {
     nodes[index].retrieve(returnObjects, pRect);
   }

   returnObjects.addAll(objects);

   return returnObjects;
}

最后一个四叉树的方法就是retrieve方法,他返回了与指定节点可能发生碰撞的所有节点。这个方法成倍的减少碰撞检测数量。

用这个类来进行2D碰撞检测

现在我们有了完整功能的四叉树,是时候使用它来帮助我们减少碰撞检测了。
在一个特定的游戏里,开始创建四叉树,并将屏幕尺寸作为参数传入(Rectangle的构造函数)。

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

在每一帧里,我们都先清除四叉树再用inset方法将对象插入其中。

quad.clear();
for (int i = 0; i < allObjects.size(); i++) {
  quad.insert(allObjects.get(i));
}

所有的对象都插入后,就可以遍历每个对象,得到一个可能会发生碰撞对象的list,然后你就可以在list里的每一个对象间用任何一种碰撞检测的算法检查碰撞,和初始化对象。

List returnObjects = new ArrayList();
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
  }
}

注意:碰撞检测算法超出了这个教程的范围,查看碰撞检测算法

总结:

碰撞检测是一种非常耗CPU的操作,可能会降低你的游戏性能。而四叉树就是一种可以加快碰撞检测速度的方法,它可以让你的游戏高效运行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容