操作系統: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
碰撞檢測通常是一種比較昂貴的操作,可能會對游戲的性能造成挑戰。四叉樹是一種加速碰撞檢測過程的途徑,最終使得游戲運行更加流暢。