1. 程式人生 > >在2D空間中使用四叉樹實現碰撞檢測

在2D空間中使用四叉樹實現碰撞檢測

引言

碰撞檢測是大部分視訊遊戲的關鍵部分。不管在2D還是3D遊戲裡,檢測兩個物體發生碰撞是非常重要的,一個小小的碰撞檢測可以為遊戲加分不少。但是,碰撞檢測一種花費巨大的操作。比如說,現在有一百個物體需要檢測是否發生了碰撞,兩兩物體比較後需要執行操作10000次——這樣的數量太驚人了!有一種方法可以加快過程,即減少檢測數量。兩個物體在螢幕相反的兩邊是絕對不會發生碰撞的,所以沒必要檢測他們之間的碰撞。從這裡就要進入四叉樹了。

什麼是四叉樹?

四叉樹是一種資料結構,被用來將一個2D區域分為更多可管理的範圍。它是二叉樹的擴充套件,但是不像二叉樹每個節點有兩個孩子,它有四個孩子。

四叉樹就是把一塊2d的區域,等分成4份,如下圖:    我們把4塊區域從右上象限開始編號, 逆時針。



接著每份繼續等分4份。


一直到我們不需要再繼續等分為止。
每次插入物件,物件都在其中一個格子裡。 進行碰撞檢測的時候,物件只會和他同在一個格子裡的以及周圍的一些物件進行檢測,所以提高了效率。四叉樹的原理大致就是這樣,和網格法差不多, 簡單說就是分格子,檢測。

四叉樹起始於單節點。物件會被新增到四叉樹的單節點上。


當更多的物件被新增到四叉樹裡時,它們最終會被分為四個子節點。(我是這麼理解的:下面的圖片不是分為四個區域嗎,每個區域就是一個孩子或子節點)然後每個物體根據他在2D空間的位置而被放入這些子節點中的一個裡。任何不能正好在一個節點區域內的物體會被放在父節點。(這點我不是很理解,就這幅圖來說,那根節點的子節點豈不是有五個節點了。)



正如你看到的,每個節點僅包括幾個物體。這樣我們就可以明白前面所說的規則,例如,左上角節點裡的物體是不可能和右下角節點裡的物體碰撞的。所以我們也就沒必要執行消耗很多資源的碰撞檢測演算法來檢驗他們之間是否會發生碰撞。

 使用四叉樹使用四叉樹是非常簡單的。下面的程式碼使用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,insertretrieve
/*
* 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的操作,可能會降低你的遊戲效能。而四叉樹就是一種可以加快碰撞檢測速度的方法,它可以讓你的遊戲高效執行。