基於cocos2d-x的2D空間中的OBB(Orient Bounding Box)碰撞檢測演算法
引言
最近在與好友聊天的過程中,好友問我如何實現類似這樣的遊戲。它主要想知道,如何檢測旋轉過後的物體與其他物體之間的碰撞。
我們知道,在沒有旋轉的情況下,對於這樣的方塊,比較規則的物體,我們完全可以使用AABB(Axie-Align Bonding Box)來進行交叉檢測,cocos2d-x內建的交叉檢測函式也支援這樣的功能。但是,在cocos2d-x中,並沒有對旋轉過後的物體支援進行檢測。好友說,它發現經過旋轉過後的AABB盒變的比原圖要大,的確是這樣的。在旋轉之後,cocos2d-x內部會重新計算新的AABB盒。而我們知道AABB盒是和座標軸平行的盒,所以它自然而然變大。(如果讀者不知道為什麼會變大了,不必深究,這並不是本文的重點)。
想要解決這樣的問題,我第一個想到的方案就是使用OBB(Orient Bounding Box)碰撞檢測演算法來實現。下面就來像大家講述下,如何在2D空間中實現這樣的演算法,並且在後面給出大家一個使用cocos2d-x來演示的Demo。
OBB包圍盒
OBB,全稱是Oriented Bounding Box,也就是帶有方向的包圍盒。實際上,它和AABB盒一樣,也是一個矩形,只不過它具有任意的方向。對OBB進行結構表示,有很多種方法,我在下面的Demo中是使用矩形的四個頂點來定義OBB的。
好了,我們知道了OBB的具體表現形式之後,我們就需要判斷兩個OBB是否相互碰撞,也就是是否有相互重疊的部分???這裡有兩種不同的方法來進行。
第一種,我們通過判斷OBB包圍盒的四個頂點,是否都在另一個OBB盒的四條邊定義的正半空間內,這樣的方法很簡單,感興趣的同學可以自己去實現下。
另外一種,也是本文將要介紹的方法,稱為Seperating Axie Theorem(分離定理),簡稱為SAT,這是一種一般性的判斷基本幾何體是否分離的演算法。也就是說,對於凸變形,我們都可以使用SAT來判斷兩個凸多邊形是否發生重疊。對於做遊戲開發的我們,很有必要掌握這樣的理論。
SAT理論解釋
對於兩個凸多邊形,如果他們之間沒有發生重疊,那麼就是說,存在一個平面能夠將這兩個物體進行分離。讀者可以看下圖:
上面的灰線條就表示一個可以將這兩個OBB盒分離的平面(讀者可以將這條灰線想象成向螢幕裡面深入的平面)。
在看下圖:
這個圖中的白色線條,它垂直於黑色的平面,我們將這個白色的線條稱之為分離軸(Seperating Axie)。
在有了分離軸之後,我們需要確定這兩個物體是否會發生重疊,也就是說這條潛在的分離軸(注意,這裡的分離軸表示的是可能成為這兩個OBB之間的分離軸,他們之間是否發生分離,我們需要通過潛在的分離軸來判斷是否發生了分離)能夠給我們判斷兩個OBB盒是否分離帶來一些便利。
如何通過這條潛在的分離軸來判斷這兩個OBB盒是否發生了分離了???我們需要將物體在這條分離軸上的投影的起點和終點計算出來,也就是下圖中在白色線條上的紅線和藍色線條的起點和終點計算出來:
看到上圖,也就是我們需要將A,B,C,D這四個點計算出來。當然,讀者需要注意,我這裡說的點是在白色直線這條軸上的點,也就是說,它實際上就是一個值而已,它的參考座標系是這條白線,所以他的值可能是0,-1,10,這樣的一個整數值,而並不是像二維空間中的點(29,9)這樣的。實際上,這幾個點最終的值是多少,我們並不關心,我們只關心的是他們相對之間的位置關係,而這樣的關係只要他們是用同一個參考系進行描述的即可。
如果計算這樣的投影點???我們知道,OBB盒是由四個頂點組成的,所以,我們只要用這四個頂點與這條分離軸進行點積Dot運算,就可以得到這樣的一個投影值,而這個投影值是否是投影線端點的值,我們需要通過判斷,來判斷它是否是最大的或者最小的值就可以確定了。
在計算出了這四個點的值之後,我們只要判斷,紅線和藍線是否發生了重疊即可。
好了,我們知道了如果通過一條潛在的分離軸來判斷這條分離軸是否真實的分離了這兩個OBB盒。那麼,剩下的問題是,一共有多少個這樣的潛在的分離軸?我們如何求得?
對於這樣的問題,是經過專家研究過後得出的結果,至於為什麼是這樣的,讀者可以自己深入的研究這個理論之後,來了解,所以,這裡將直接給出結論:
對於兩個OBB盒來說,他們之間潛在的分離軸就是他們邊的法線,也就是下圖中白色箭頭表示的軸向:
好了,還有問題,就是如何求得這些邊的法線了???這個很簡單,我們假設,下面是某一條邊的兩個頂點:
頂點一:(10,0) 頂點二: (20,-20)
我們用頂點一 減去 頂點二 (或者頂點二 減去 頂點一,都一樣)得到(-10, 20)
然後我們只要將(-10, 20)中的x,y分量位置調反,然後任意一個去相反值就可以了,也就是說有兩個(20,10)或者(-20,-10),隨便選取哪一個做為分離軸都可以,還記得我在前面說過,最終計算出來的值是多少無所謂,只要他們選取的分離軸是同一個,那就能夠通過他們在這條分離軸上的值來判斷他們之間的位置關係,我們需要的僅僅是位置關係而已。
好了,SAT的理論知識就到這裡了。如果你想深入瞭解SAT理論,並且想知道在3D空間中的情況(注意,3D空間中的SAT盒潛在的分離軸要多的多,而且判斷方法也不盡相同),可自行學習相關理論知識。
程式例項
下面就來介紹一個我自己實現的一種簡化的OBB碰撞檢測演算法。
首先來看下標頭檔案:
//------------------------------------------------------------------------------
// declaration : Copyright (c), by XJ , 2014 . All right reserved .
// brief : This file will define the OBB(Oriented bounding box)
// author : XJ
// date : 2014 / 6 / 16
// version : 1.0
//------------------------------------------------------------------------------
#pragma once
#include"XJMath.h"
class Projection
{
public:
Projection(float min, float max);
~Projection();
public:
bool overlap(Projection* proj);
public:
float getMin() const;
float getMax() const ;
private:
float min ;
float max ;
};
class OBB
{
public:
OBB();
~OBB();
public:
void getAxies(VECTOR2 * axie);
Projection getProjection(VECTOR2 axie);
public:
VECTOR2 getVertex(int index) const;
void setVertex(int index, float x, float y);
void setVertex(int index, VECTOR2 v);
public:
bool isCollidWithOBB(OBB* obb);
private:
VECTOR2 vertex[4] ;
};
先來看看OBB的類。這個類中的成員屬性只有一個:
VECTOR2 vertex[4] ;
正如我前面說的,這個成員陣列儲存了OBB盒的四個頂點,我們也僅僅需要這四個頂點而已。
下面的幾個方法依次來解釋下作了哪些工作。
void getAxies()
void OBB::getAxies(VECTOR2* axie)
{
for(int i = 0 ; i < 4 ; i ++)
{
VECTOR2 s ;
Vec2Sub(s,vertex[i],vertex[(i+1)%4]);
Vec2Normalize(s, s);
axie[i].x = -s.y ;
axie[i].y = s.x ;
}
}
這個函式很簡單,只是簡單的根據我們前面討論的結果,計算OBB盒的四條邊的四個分離軸,你可以看到我這裡使用的法向量計算方法是這樣的:
如果邊向量為(x,y)那麼法向量為(-y,x),你也可以設定為(y,-x)。結果沒有什麼變化。
Projection getProjection(VECTOR2 axie)
Projection OBB::getProjection(VECTOR2 axie)
{
float min = 0 ;
Vec2Dot(min,vertex[0], axie);
float max = min ;
for(int i = 1 ; i < 4 ; i ++)
{
float temp = 0 ;
Vec2Dot(temp, vertex[i], axie);
if(temp > max)
max = temp ;
else if(temp < min)
min = temp ;
}// end for
return Projection(min, max);
}
這裡的方法是計算出一個投影線條出來,也就是在前面圖中畫出來的紅線和藍線。投影結構我使用Projection來表示,在上面你可以看到Projection的定義。它很簡單,只是儲存了兩個float形的資料,分別表示OBB盒在分離軸上投影的最小值和最大值。計算最小值和最大值的演算法在上面的getProjection中給出,我們需要根據指定的分離軸axie來計算對應的投影。
vertex的方法
OBB類中,還存在幾個setter和getter的方法,這幾個方法只是簡單的獲取和設定裡面的值而已。沒有什麼好解釋的。
bool isCollidWithOBB(OBB* obb)
bool OBB::isCollidWithOBB(OBB* obb)
{
VECTOR2 * axie1 = new VECTOR2[4];
VECTOR2 * axie2 = new VECTOR2[4];
//Get the seperat axie
getAxies(axie1);
obb->getAxies(axie2);
//Check for overlap for all of the axies
for(int i = 0 ; i < 4 ; i ++)
{
Projection p1 = getProjection(axie1[i]);
Projection p2 = obb->getProjection(axie1[i]);
if(!p1.overlap(&p2))
return false ;
}
for(int i = 0 ; i < 4 ; i ++)
{
Projection p1 = getProjection(axie2[i]);
Projection p2 = obb->getProjection(axie2[i]);
if(!p1.overlap(&p2))
return false ;
}
delete[]axie1 ;
delete[]axie2 ;
return true ;
}
這個方法,就是對傳遞進來的OBB判斷是否與呼叫這個方法的OBB發生了交叉。很簡單,我們只要求出每一個OBB的四個分離軸,然後呼叫每一個OBB的投影方法,計算出在這8個分離軸上的投影,最後呼叫投影的overlap方法,如下所示:
bool Projection::overlap(Projection* proj)
{
if(min > proj->getMax()) return false ;
if(max < proj->getMin()) return false ;
return true ;
}
來判斷投影是否發生了交叉。一旦我們找到了一個分離軸,也就是他們在這個軸上的投影是不相交的,那麼我們就可以確定,這兩個OBB盒是不相交的,就可以提早退出這個函數了。如果對這8個潛在分離軸的判斷都失敗了,也就表示,在這8個軸上都發生了交叉,那麼就可以確定,這兩個OBB盒一定發生了碰撞或者重疊。
上面是這個OBB碰撞檢測演算法的核心內容。下面在給出一個使用這個OBB演算法的例項,我是使用cocos2d-x來製作的Demo。程式碼如下:
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "OBB.h"
class HelloWorld : public cocos2d::CCLayer
{
public:
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();
// there's no 'id' in cpp, so we recommand to return the exactly class pointer
static cocos2d::CCScene* scene();
// a selector callback
void menuCloseCallback(CCObject* pSender);
// implement the "static node()" method manually
CREATE_FUNC(HelloWorld);
//update
void update(float dt);
//draw
void draw();
private:
cocos2d::CCSprite* m_Sprite1 ;
cocos2d::CCSprite* m_Sprite2 ;
float m_vx1 ;
float m_vy1 ;
float m_vx2 ;
float m_vy2 ;
OBB* obb1 ;
OBB* obb2 ;
};
#endif // __HELLOWORLD_SCENE_H__
#include "HelloWorldScene.h"
#include "OBB.h"
using namespace cocos2d;
CCScene* HelloWorld::scene()
{
CCScene * scene = NULL;
do
{
// 'scene' is an autorelease object
scene = CCScene::create();
CC_BREAK_IF(! scene);
// 'layer' is an autorelease object
HelloWorld *layer = HelloWorld::create();
CC_BREAK_IF(! layer);
// add layer as a child to scene
scene->addChild(layer);
} while (0);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
bool bRet = false;
do
{
//////////////////////////////////////////////////////////////////////////
// super init first
//////////////////////////////////////////////////////////////////////////
CC_BREAK_IF(! CCLayer::init());
this->scheduleUpdate();
m_Sprite1 = CCSprite::create("Fog_x4.png");
m_Sprite1->setPosition(ccp(0,160));
m_Sprite1->retain();
this->addChild(m_Sprite1);
m_Sprite1->runAction(CCRepeatForever::create(CCRotateBy::create(1.0/60, 1)));
m_Sprite2 = CCSprite::create("Fog_x4.png");
m_Sprite2->setPosition(ccp(240,160));
m_Sprite2->retain();
this->addChild(m_Sprite2);
m_Sprite2->runAction(CCRepeatForever::create(CCRotateBy::create(1.0/60, 1)));
m_vx1 = 2 ;
m_vy1 = -2 ;
m_vx2 = -2 ;
m_vy2 = 2 ;
obb1 = new OBB();
obb2 = new OBB();
bRet = true;
} while (0);
return bRet;
}
void HelloWorld::menuCloseCallback(CCObject* pSender)
{
// "close" menu item clicked
CCDirector::sharedDirector()->end();
}
void HelloWorld::update(float dt)
{
//Boundarying Check
CCPoint pt1 = m_Sprite1->getPosition();
if(pt1.x < 0 || pt1.x > 480)
m_vx1 = -m_vx1 ;
if(pt1.y < 0|| pt1.y > 320)
m_vy1 = -m_vy1 ;
CCPoint pt2 = m_Sprite2->getPosition();
if(pt2.x < 0 || pt2.x > 480)
m_vx2 = -m_vx2 ;
if(pt2.y < 0|| pt2.y > 320)
m_vy2 = -m_vy2 ;
pt1.x += m_vx1 ;
pt1.y += m_vy1 ;
m_Sprite1->setPosition(pt1);
pt2.x += m_vx2 ;
pt2.y += m_vy2 ;
m_Sprite2->setPosition(pt2);
//Collision Check
CCPoint pt = m_Sprite1->convertToWorldSpace(ccp(0,0));
obb1->setVertex(0, pt.x, pt.y);
pt = m_Sprite1->convertToWorldSpace(ccp(64,0));
obb1->setVertex(1, pt.x, pt.y);
pt = m_Sprite1->convertToWorldSpace(ccp(64,64));
obb1->setVertex(2, pt.x, pt.y);
pt = m_Sprite1->convertToWorldSpace(ccp(0,64));
obb1->setVertex(3, pt.x, pt.y);
pt = m_Sprite2->convertToWorldSpace(ccp(0,0));
obb2->setVertex(0, pt.x, pt.y);
pt = m_Sprite2->convertToWorldSpace(ccp(0,64));
obb2->setVertex(1,pt.x, pt.y);
pt = m_Sprite2->convertToWorldSpace(ccp(64,64));
obb2->setVertex(2, pt.x, pt.y);
pt = m_Sprite2->convertToWorldSpace(ccp(64,0));
obb2->setVertex(3, pt.x, pt.y);
if(obb1->isCollidWithOBB(obb2))
{
VECTOR2 collision = MAKE_VECTOR2(pt1.x - pt2.x, pt1.y - pt2.y);
Vec2Normalize(collision, collision);
Vec2Mul(collision, collision, 2.8);
m_vx1 = collision.x ;
m_vy1 = collision.y ;
m_vx2 = -collision.x ;
m_vy2 = -collision.y ;
}
}
void HelloWorld::draw()
{
//Draw the OBB of 1
ccDrawColor4B(255,0,0,255);
ccDrawLine(ccp(obb1->getVertex(0).x, obb1->getVertex(0).y),
ccp(obb1->getVertex(1).x, obb1->getVertex(1).y));
ccDrawLine(ccp(obb1->getVertex(1).x, obb1->getVertex(1).y),
ccp(obb1->getVertex(2).x, obb1->getVertex(2).y));
ccDrawLine(ccp(obb1->getVertex(2).x, obb1->getVertex(2).y),
ccp(obb1->getVertex(3).x, obb1->getVertex(3).y));
ccDrawLine(ccp(obb1->getVertex(0).x, obb1->getVertex(0).y),
ccp(obb1->getVertex(3).x, obb1->getVertex(3).y));
//Draw the OBB of 2
ccDrawColor4B(0,255,0,255);
ccDrawLine(ccp(obb2->getVertex(0).x, obb2->getVertex(0).y),
ccp(obb2->getVertex(1).x, obb2->getVertex(1).y));
ccDrawLine(ccp(obb2->getVertex(1).x, obb2->getVertex(1).y),
ccp(obb2->getVertex(2).x, obb2->getVertex(2).y));
ccDrawLine(ccp(obb2->getVertex(2).x, obb2->getVertex(2).y),
ccp(obb2->getVertex(3).x, obb2->getVertex(3).y));
ccDrawLine(ccp(obb2->getVertex(0).x, obb2->getVertex(0).y),
ccp(obb2->getVertex(3).x, obb2->getVertex(3).y));
}
需要指出的是,我們需要將在cocos2d-x中進行旋轉後的精靈的座標設定到OBB中。讀者可以發現,我在update方法中,使用了大量的convertToWorldSpace函式。這個函式,可以將旋轉過後的精靈的相對於區域性座標的頂點轉化為世界座標的頂點。通過精靈呼叫convertToWorldSpace,在引數中指定需要轉化的點,而我只需要矩形的四個頂點,他們分別是(0,0),(64,0),(0,64),(64,64)。圖片尺寸是64*64的。通過convertToWorldSpace就可以計算出來了。
在draw方法中,我為了讓Demo看上去更容易看出發生碰撞,我將OBB盒用直線繪製出來,你可以在截圖中發現,每一個Fog周圍都有一個框框,這個就是OBB盒。
程式原始碼即程式可以在下面的連結中下載: