首页 > 代码库 > 基于cocos2d-x的2D空间中的OBB(Orient Bounding Box)碰撞检测算法

基于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盒。

             程序源码即程序可以在下面的链接中下载:

            OBB.zip