1. 程式人生 > >2D空間中使用Quadtree四叉樹進行碰撞檢測優化

2D空間中使用Quadtree四叉樹進行碰撞檢測優化

很多遊戲中都需要使用碰撞檢測演算法檢測兩個物體的碰撞,但通常這些碰撞檢測演算法都很耗時很容易拖慢遊戲的速度。這裡我們學習一下使用四叉樹來對碰撞檢測進行優化,優化的根本是碰撞檢測時跳過那些明顯離得很遠的物體,加快檢測速度。

【注:這裡演算法的實現使用的Java語言,但演算法和語言沒有關係,理解了其中的原理可以應用到各種碰撞檢測場景中。】

介紹

碰撞檢測是多數遊戲的關鍵部分,不管是2d遊戲還是3d遊戲中,檢測兩個物體的碰撞都是很重要的,否則會出現很奇怪的現象。

碰撞檢測是一個很耗費資源的操作。假設有100個物體要進行互相的碰撞檢測,兩兩物體的位置比較要進行:100x100 = 10000次,檢測的次數實在太多。

碰撞檢測優化的一個辦法就是想辦法減少檢測的次數,例如在螢幕對角的兩個物體那麼遠是不可能碰撞的所以也就沒必要去判斷他們是否碰撞了,四叉樹的優化就是基於這一點。

關於四叉樹

四叉樹是二叉樹的一個擴充套件也是一個數據結構,只不過四叉樹是有四個子節點。四叉樹將2d區域分成多個部分來進行劃分操作。

在接下來的示例圖片中,每張圖片代表一個遊戲的2d空間,紅色的方塊代表物體。同時本文中每個區域的子區域(子節點)按照逆時針進行標記如下:

https://cdn.tutsplus.com/gamedev/authors/legacy/Steven%20Lambert/2012/08/30/image1.png

一個四叉樹的開始是一個單一節點(根節點),根節點對應原本還沒有分割的2d空間,物體可以新增到2d空間,也就是新增到根節點上。

https://cdn.tutsplus.com/gamedev/authors/legacy/Steven%20Lambert/2012/08/30/image2.png

當更多的物體新增到四叉樹以後,根節點物體數量太多了就會分裂出四個子節點,將多數物體分給子節點中(處於子節點邊界的物體無法加入任何一個子節點就還是留給父節點)。

https://cdn.tutsplus.com/gamedev/authors/legacy/Steven%20Lambert/2012/08/30/image3.png

同樣隨著物體數量增加每一個子節點可以繼續分裂出自己的四個子節點。

https://cdn.tutsplus.com/gamedev/authors/legacy/Steven%20Lambert/2012/08/30/image4.png

可以看到每個節點只包含少量的物體(不能分給子節點的那些物體)。因此我們知道,在左上節點中的物體是不可能和左下節點中的物體產生碰撞的,因此就不需要在他們之間進行碰撞檢測。

四叉樹的實現

四叉樹的實現很簡單,這裡的實現程式碼使用的Java語言,但這個技術可以應用到任何語言當中。

【譯者注】:這裡的Java實現中座標系原點位於左上角,物體的錨點也位於左上角。

首先是Quadtree的核心類:

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; // 四個子節點 /* * 建構函式 */ public Quadtree(int pLevel, Rectangle pBounds) { level = pLevel; objects = new ArrayList(); bounds = pBounds; nodes = new Quadtree[4]; } }

Quadtree類的結構很清楚:

  • MAX_OBJECTS:定義的是一個區域節點在被劃分之前能夠擁有節點的最大數量;
  • MAX_LEVELS:定義的是子節點的最大深度;
  • level:指的是當前節點的深度,對於自身來說level為0;
  • bounds:指的是當前節點所佔的2d空間;
  • nodes:四個子節點。

在這個例子中,四叉樹中要碰撞檢測的物體都是些小矩形,實際可能會有任意形狀的物體,這個和四叉樹檢測優化演算法本身無關,只是不同行的物體檢測要採用不同的檢測方法(圓形檢測、矩形檢測等),一般也都會將不規則物體簡化為規則形狀。

然後要實現四叉樹的五個方法:clear, split, getIndex, insertretrieve

clear方法遞迴清除所有節點所擁有的物體:

/*
 * 清空四叉樹
 */
 public void clear() {
   objects.clear();

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

split函式將當前節點平均分成四個子節點,並用計算好的新節點資料初始化四個子節點:

/*
 * 將一個節點分成四個子節點(實際是新增四個子節點)
 */
 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));
 }

getIndex函式判斷物體屬於父節點還是子節點,以及屬於哪一個子節點:

/*
 * 用於判斷物體屬於哪個子節點
 * -1指的是當前節點可能在子節點之間的邊界上不屬於四個子節點而還是屬於父節點
 */

 private int getIndex(Rectangle pRect) {
   int index = -1;
   // 中線
   double verticalMidpoint = bounds.getX() + (bounds.getWidth() / 2);
   double horizontalMidpoint = bounds.getY() + (bounds.getHeight() / 2);

   // 物體完全位於上面兩個節點所在區域
   boolean topQuadrant = (pRect.getY() < horizontalMidpoint && pRect.getY() + pRect.getHeight() < horizontalMidpoint);
   // 物體完全位於下面兩個節點所在區域
   boolean bottomQuadrant = (pRect.getY() > horizontalMidpoint);

   // 物體完全位於左面兩個節點所在區域
   if (pRect.getX() < verticalMidpoint && pRect.getX() + pRect.getWidth() < verticalMidpoint) {
      if (topQuadrant) {
        index = 1; // 處於左上節點 
      }
      else if (bottomQuadrant) {
        index = 2; // 處於左下節點
      }
    }
    // 物體完全位於右面兩個節點所在區域
    else if (pRect.getX() > verticalMidpoint) {
     if (topQuadrant) {
       index = 0; // 處於右上節點
     }
     else if (bottomQuadrant) {
       index = 3; // 處於右下節點
     }
   }

   return index;
 }

insert函式往四叉樹中新增物體,如果物體可以分給子節點則分給子節點,否則就留給父節點了,父節點物體超出容量後如果沒分裂的話就分裂從而將物體分給子節點:

/*
 * 將物體插入四叉樹
 * 如果當前節點的物體個數超出容量了就將該節點分裂成四個從而讓多數節點分給子節點
 */
 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) {
      if (nodes[0] == null) { 
         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++;
       }
     }
   }
 }

最後一個是retrieve函式,這個函式返回所有可能和指定物體碰撞的物體,也就是待檢測物體的篩選,這也是優化碰撞檢測的關鍵:

/*
 * 返回所有可能和指定物體碰撞的物體
 */
 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;
 }

應用於2d碰撞檢測

實現了四叉樹資料結構之後我們就可以用來優化碰撞檢測減少檢測次數了。

典型的,遊戲開始要建立四叉樹並指定遊戲場景的邊界尺寸:

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

在每一幀都要使用clear函式清空四叉樹然後使用insert函式將所有物體重新新增到四叉樹中:

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

所有物體都插入以後,要遍歷所有的物體並使用retrieve函式篩選出所有可能碰撞的物體進行碰撞檢測。

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++) {
    // 使用合適的碰撞檢測演算法和每一個可能碰撞的物體進行碰撞檢測...
  }
}

關於碰撞檢測演算法文章(之後也會再翻譯):碰撞檢測

結論

總之碰撞檢測是一個很耗費資源的操作,通過四叉樹結構減少檢測的次數可以大大優化碰撞檢測效率,幫助減輕碰撞檢測對遊戲的拖延。