box2d 碰撞检测_(译)如何使用box2d来做碰撞检测(且仅用来做碰撞检测)
免責申明(必讀!):本博客提供的所有教程的翻譯原稿均來自于互聯網,僅供學習交流之用,切勿進行商業傳播。同時,轉載時不要移除本申明。如產生任何糾紛,均與本博客所有人、發表該翻譯稿之人無任何關系。謝謝合作!
程序截圖:
當你使用cocos2d來制作一個游戲的時候,有時,你可能想使用cocos2d的action來移動游戲中的對象,而不是直接使用Box2d物理引擎來做。然而,這并不是說你不能使用box2d提供的強大的碰撞檢測功能!
這個教程的目的,就是一步一步地向你展示如何僅使用box2d來做碰撞檢測---沒有物理效果。我們將創建一個簡單的demo,里面有一輛車在屏幕上奔馳,當它撞到一只貓后就會放聲大笑。是的,我明白,這樣做太殘忍了。
在這篇教程中,我們會引入一些新的、有趣的概念,比如使用sprite sheets,利用Zwoptex工具來制作它。還會涉及box2d的debug drawing和VertexHelper這個工具。
這個簡單假設你已經閱讀過前面的一系列的cocos2d和box2d的教程了(如果沒有,最好把前面的教程先看一遍),或者你有相關經驗也可以。
還有,在我忘記之前--特別要感謝Kim在評論中建議我寫一篇這樣的教程。
Sprites and Sprite Sheets
在我們開始之前,需要簡單地介紹一下sprites和spritesheets。
目前為止,當我們使用cocos2d的時候,我們都是直接使用CCSprite類,然后傳遞一個精靈圖片給它。但是,根據cocos2d最佳實踐,如果使用spritesheet來做的話,那樣會極大地提高效率。
上圖就是一個spritesheet的樣例,這個圖在cocos2d的樣例代碼中可以找到。簡言之,spritesheet就是一張大的圖片,它能夠被裁成許多小的子圖片。為了指定spritesheet中的每個子圖片的位置,你需要為每個圖片指定一個坐標,通常是一個矩形。比如,下面的代碼展示了,如何把spritesheet中前四張圖片摳出來:
CCSpriteSheet*sheet=[CCSpriteSheet
spriteSheetWithFile:@"grossini_dance_atlas.jpg"capacity:1];
CCSprite*sprite1=[CCSprite spriteWithTexture:sheet.texture rect:
CGRectMake(85*0,121*1,85,121)];
CCSprite*sprite2=[CCSprite spriteWithTexture:sheet.texture rect:
CGRectMake(85*1,121*1,85,121)];
CCSprite*sprite3=[CCSprite spriteWithTexture:sheet.texture rect:
CGRectMake(85*2,121*1,85,121)];
CCSprite*sprite4=[CCSprite spriteWithTexture:sheet.texture rect:
CGRectMake(85*3,121*1,85,121)];
你可以會說,寫一大堆硬編碼的坐標太麻煩了,太煩人了!幸運的是,Robert Payne已經寫好了一個非常方便地web工具,叫做Zwoptex,它可以創建精靈表單(spritesheet),同時會導出每個子圖片的坐標,這樣你在cocos2d里面使用這些子圖片就會非常方便了。
制作精靈表單
在我們開始之前,你需要一張圖片。你可以下載我老婆制作的車和貓的圖片,或者使用你自己的。接下來,打開瀏覽器,加載Zwoptex主頁。你將會看到下面的屏幕:
一旦加載完畢后,點擊Zwoptex的File菜單,然后點Import Images。選擇你剛剛下載的車和貓的圖片,然后點click,這時,你的圖片應該出現會互相重疊在一起。拖動其中一張,這樣會看得更清楚一些。
注意,這些圖片已經被自動地把圖片周圍的空白部分給去掉了。這并不是我們想要的(后面你會知道為什么),因此,用鼠標把這兩張圖片都框住,然后選擇菜單Modify\Untrim Selected Images。
現在,我們的圖片看起來非常好了。點擊Arrange\Complex by Height(no spacing),然后它們會排列地更加整齊。
最后,讓我們把畫布(canvas)的大小調整為合適的大小。點擊Modify\Canvas Width,并把它設置為128px。同樣的,點擊Modify\Canvas Height,然后把它設置為64px。你的屏幕最后看起來應該是下面這個樣子。
最后,是時候導出sprite sheet和相應的坐標了!點擊File\Export Texture,然后保存sprite sheet為“sprites.jpg”。然后點擊File\Export Coordinates“并且保存為”sprites.plist“。注意,這里必須把spritesheet和坐標文件的名字取成相同的,因為,spritesheet為假設它的坐標文件為相應名字的plist文件。
接下來,打開sprites.plist。你會看到Zwopte已經自動地幫你把原圖中每一個子圖片的坐標計算出來了,并且存儲成了一個plist文件。我們接下來就可以使用這些坐標,而不用手工去輸入它們了!
從Sprite sheet中添加我們的精靈
好,是時候寫一些代碼了!
打開Xcode并創建一個新的工程,選擇 cocos2d-0.99.1 Box2d Application template,取名為Box2DCollision。然后把自帶的樣例代碼全部刪除,你可能從彈球的教程中找到具體的做法。
當然,我們要在HelloWorldScene.mm的頂部加入下面一行代碼:
#definePTM_RATIO 32.0
如果你不明白我在這里講的是些什么,你可能需要看看彈球的教程來獲取更多的信息。
接下來,讓我們把sprite sheet和坐標plist文件都加入到工程中去。把sprites.jpg和sprites.plist文件都拖到Resouces文件夾中,同時確保 ?“Copy items into destination group’s folder (if needed)”被復選中。
然后,在HelloWorldScene.h文件的HelloWorld類中,添加下面的成員變量:
CCSpriteSheet*_spriteSheet;
現在,讓我們修改HelloWorldScene.mm中的init方法來加載我們的spritesheet和plist文件。具體修改如下:
-(id)init {if((self=[super init])) {//Create our sprite sheet and frame cache_spriteSheet=[[CCSpriteSheet spriteSheetWithFile:@"sprites.jpg"capacity:2] retain];
[[CCSpriteFrameCache sharedSpriteFrameCache]
addSpriteFramesWithFile:@"sprites.plist"];
[self addChild:_spriteSheet];
[self spawnCar];
[self schedule:@selector(secondUpdate:) interval:1.0];
}returnself;
}
第一件事情就是創建一個spritesheet對象,它是一個可以用來高效地繪制它的所有的CCSprite結點的對象。很明顯,這些CCSprite必須共享相同的紋理(texture)。當我們把車子和貓加入到場景中的時候,我們需要將它們當作spritesheet的孩子加進去。
然后,我們使用CCSpriteFrameCache類來加載坐標屬性列表文件。這個函數會自動地查找一個與之同名的圖片(即sprites.jpg).這也就是前面說的,為什么要把”sprites.jpg“和"sprites.plist”取成相同名字的原因。
在這之后,我們調用一個函數在場景中顯示一輛車。同時,還設置一個計時器,每隔一秒調用一次secondUpdate函數。
接下來,讓我們實現spawnCar方法。我們的做法是讓車子永遠地在屏幕中間做路徑為三角形的運動。在init函數的上面添加下面函數代碼:
-(void)spawnCar {
CCSprite*car=[CCSprite spriteWithSpriteFrameName:@"car.jpg"];
car.position=ccp(100,100);
car.tag=2;
[car runAction:[CCRepeatForever actionWithAction:
[CCSequence actions:
[CCMoveTo actionWithDuration:1.0position:ccp(300,100)],
[CCMoveTo actionWithDuration:1.0position:ccp(200,200)],
[CCMoveTo actionWithDuration:1.0position:ccp(100,100)],
nil]]];
[_spriteSheet addChild:car];
}
注意,這里創建sprite的時候,不是使用spriteWithFile,而是使用spriteWithSpriteFrameName。這里指的是使用spritesheet紋理中代表Car的圖片來創建精靈。
還有一點需要注意,我們不是把Car作為HelloWorld層的函數添加進去,而是把Car作為Spritesheet的孫子添加進去的。
這個函數的后面部分你應該比較熟悉了。因此,讓我們添加一些貓吧!在上面的spawnCar方法后面添加下面的方法:
-(void)spawnCat {
CGSize winSize=[CCDirector sharedDirector].winSize;
CCSprite*cat=[CCSprite spriteWithSpriteFrameName:@"cat.jpg"];intminY=cat.contentSize.height/2;intmaxY=winSize.height-(cat.contentSize.height/2);intrangeY=maxY-minY;intactualY=arc4random()%rangeY;intstartX=winSize.width+(cat.contentSize.width/2);intendX=-(cat.contentSize.width/2);
CGPoint startPos=ccp(startX, actualY);
CGPoint endPos=ccp(endX, actualY);
cat.position=startPos;
cat.tag=1;
[cat runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:1.0position:endPos],
[CCCallFuncN actionWithTarget:self
selector:@selector(spriteDone:)],
nil]];
[_spriteSheet addChild:cat];
}-(void)spriteDone:(id)sender {
CCSprite*sprite=(CCSprite*)sender;
[_spriteSheet removeChild:sprite cleanup:YES];
}-(void)secondUpdate:(ccTime)dt {
[self spawnCat];
}
你應該對上面的代碼線路熟悉了,如果不熟悉,建議看相關的教程后再繼續。編譯并運行代碼,如果一切ok,你將會看到一輛車在屏幕上來回動,同時有一只貓從右至左穿過屏幕。接下來,我們將添加一些碰撞檢測的代碼。
為這些精靈創建Box2d的body
接下來的一步就是為每個精靈創建一個body,這樣box2d就能知道它們的位置了,這樣的話,當碰撞發生的時候,我們就可以被告知了。下面所做的事情和之前的教程做法差不多。
然后,這一次,我們不是更新box2d的body,然后再更新sprite。這里,我們是先更新sprite(使用action或者別的),然后再更新box2d的body。
因此,讓我們首先創建world。打開HelloWorldScene.h,并在文件頂部添加下面的代碼:
#import"Box2D.h"
然后在HelloWorld類中添加下面的成員變量:
b2World*_world;
然后在HeloWorldScene.mm的init方法中加入下列代碼:
b2Vec2 gravity=b2Vec2(0.0f,0.0f);booldoSleep=false;
_world=newb2World(gravity, doSleep);
注意,這里有兩件事情非常重要!首先,我們把重力向量設置成(0,0)。因為我們并不想讓這些對象自動運動,而是人為控制其運動。其次,我們告訴box2d不能讓這些對象sleep。這一點非常重要,因為我們是人為地移動對象,所以必須設置。
然后,在spawnCat方法上面添加下列代碼:
-(void)addBoxBodyForSprite:(CCSprite*)sprite {
b2BodyDef spriteBodyDef;
spriteBodyDef.type=b2_dynamicBody;
spriteBodyDef.position.Set(sprite.position.x/PTM_RATIO,
sprite.position.y/PTM_RATIO);
spriteBodyDef.userData=sprite;
b2Body*spriteBody=_world->CreateBody(&spriteBodyDef);
b2PolygonShape spriteShape;
spriteShape.SetAsBox(sprite.contentSize.width/PTM_RATIO/2,
sprite.contentSize.height/PTM_RATIO/2);
b2FixtureDef spriteShapeDef;
spriteShapeDef.shape=&spriteShape;
spriteShapeDef.density=10.0;
spriteShapeDef.isSensor=true;
spriteBody->CreateFixture(&spriteShapeDef);
}
這段代碼對你來說應該比較熟悉了---它和我們在breakout教程中是一樣的。然后,這里有一點區別,我們把shape定義的isSensor成員設置成了true。
根據Box參考手冊,如果你想讓對象之間有碰撞檢測但是又不想讓它們有碰撞反應,那么你就需要把isSensor設置成true。這正是我們想要的效果!
接下來,在spawnCat的最后一行addChild之前添加下列代碼:
[self addBoxBodyForSprite:cat];
同樣的,你需要在spawnCar的相應位置添加下列代碼:
[self addBoxBodyForSprite:car];
當我們的sprites被銷毀的時候,我們需要銷毀Box2d的body。因此,把你的spriteDone方法改寫成下面的形式:
-(void)spriteDone:(id)sender {
CCSprite*sprite=(CCSprite*)sender;
b2Body*spriteBody=NULL;for(b2Body*b=_world->GetBodyList(); b; b=b->GetNext()) {if(b->GetUserData()!=NULL) {
CCSprite*curSprite=(CCSprite*)b->GetUserData();if(sprite==curSprite) {
spriteBody=b;break;
}
}
}if(spriteBody!=NULL) {
_world->DestroyBody(spriteBody);
}
[_spriteSheet removeChild:sprite cleanup:YES];
}
現在,最重要的一部分,我們需要根據sprite的位置改變來更新box2d的body。因此,在init方法里面添加下面一行代碼:
[self schedule:@selector(tick:)];
然后,把tick方法寫成下面的形式:
-(void)tick:(ccTime)dt {
_world->Step(dt,10,10);for(b2Body*b=_world->GetBodyList(); b; b=b->GetNext()) {if(b->GetUserData()!=NULL) {
CCSprite*sprite=(CCSprite*)b->GetUserData();
b2Vec2 b2Position=b2Vec2(sprite.position.x/PTM_RATIO,
sprite.position.y/PTM_RATIO);
float32 b2Angle=-1*CC_DEGREES_TO_RADIANS(sprite.rotation);
b->SetTransform(b2Position, b2Angle);
}
}
}
這個方法和我們之前在breakout中寫的tick方法差不多,唯一的差別就是,我們現在是基于cocos2d的精靈位置來更新box2d的body位置。
編譯并運行工程,可能看起來和之前并沒有什么差別。你可能會想,到底我們剛剛寫了這么多代碼有用沒啊!好,接下來我就會展示給你看---激活debug drawing!
激活 Box2D 的Debug Drawing
因為,你是使用box2d的模板建的項目,所以里面已經包含了一個GLES-Render.h和GLES-Render.mm文件,如果想要激活debug drawing,有這兩個文件就足夠了!
接下來,讓我們在HelloWorldScene.h的頂部包含下面的頭文件:
#import"GLES-Render.h"
然后,在HelloWorld類中添加以下成員變量:
GLESDebugDraw*_debugDraw;
接下來,在init方法中添加下面的代碼:
//Enable debug draw_debugDraw=newGLESDebugDraw( PTM_RATIO );
_world->SetDebugDraw(_debugDraw);
uint32 flags=0;
flags+=b2DebugDraw::e_shapeBit;
_debugDraw->SetFlags(flags);
這段代碼是創建一個GLESDebug類,并且把它注冊到world對象里面。我們傳遞一個flag來指定我們需要顯示的仿真細節---這里我們只想顯示box2d的shape。對于你可以設置的所有的flag標志,可以參考b2WorldCallbacks.h文件。
接下來,我們需要添加一個draw方法。在init方法后面添加draw方法:
-(void) draw
{
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
_world->DrawDebugData();
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}
老實說,我現在仍然對opengl陌生,因此,我并不是很清楚這里的opengl代碼做了些什么事。然后,這是激活debug draw的標準代碼,它可以工作!:)
最后需要注意的一點,在dealloc函數中添加清理代碼:
-(void)dealloc {
delete _world;
delete _debugDraw;
[super dealloc];
}
現在,編譯并運行工程,你將會看到所有的box2d shape周圍都有一個粉紅色的矩形區域。如果一切ok的話,你會看到這些粉紅色的shape會跟著sprite運動。
碰撞檢測
現在,是時候壓死幾只貓了!
像之前的breakout游戲一樣,我們將往world對象里面注冊一個contact listener對象。你可以下載我為這個教程制作的contact listener代碼,然后把 MyContactListener.h 和 MyContactListener.mm兩個文件添加到工程中去:
同時,你還可以下載我為本教程制作的美妙的音效。
回到代碼。在HelloWorldScene.h中添加下面的代碼:
#import"MyContactListener.h"#import"SimpleAudioEngine.h"
然后,在HelloWord類中添加下面成員變量:
MyContactListener*_contactListener;
然后,在init方法中加入下面的代碼:
//Create contact listener_contactListener=newMyContactListener();
_world->SetContactListener(_contactListener);//Preload effect[[SimpleAudioEngine sharedEngine] preloadEffect:@"hahaha.caf"];
最后,在tick方法的底部加入下面的代碼:
std::vectortoDestroy;
std::vector::iterator pos;for(pos=_contactListener->_contacts.begin();
pos!=_contactListener->_contacts.end();++pos) {
MyContact contact=*pos;
b2Body*bodyA=contact.fixtureA->GetBody();
b2Body*bodyB=contact.fixtureB->GetBody();if(bodyA->GetUserData()!=NULL&&bodyB->GetUserData()!=NULL) {
CCSprite*spriteA=(CCSprite*) bodyA->GetUserData();
CCSprite*spriteB=(CCSprite*) bodyB->GetUserData();if(spriteA.tag==1&&spriteB.tag==2) {
toDestroy.push_back(bodyA);
}elseif(spriteA.tag==2&&spriteB.tag==1) {
toDestroy.push_back(bodyB);
}
}
}
std::vector::iterator pos2;for(pos2=toDestroy.begin(); pos2!=toDestroy.end();++pos2) {
b2Body*body=*pos2;if(body->GetUserData()!=NULL) {
CCSprite*sprite=(CCSprite*) body->GetUserData();
[_spriteSheet removeChild:sprite cleanup:YES];
}
_world->DestroyBody(body);
}if(toDestroy.size()>0) {
[[SimpleAudioEngine sharedEngine] playEffect:@"hahaha.caf"];
}
這段代碼和我們之前的breakout教程中的代碼基本上差不多,因此你應該比較熟悉了。
還有一件事別忘了,往dealloc方法里面加入清理代碼!這個灰常重要!
delete _contactListener;
[_spriteSheet release];
再運行一下工程看看!
調整shape的邊界
你可能已經注意到了,我們的box2d shape的邊界并不是和sprite的邊界完全吻合。對于有些碰撞檢測要求不是特別精確的游戲,這也許夠了,但是,有些游戲確不行!我們需要嚴格地定義shape的邊界和精靈的邊界重合在一起。
在Box2d里面,你可以通過指定shape的頂點來指定shape的形狀。然后,硬編碼這些頂點數據會很耗時,而且容易出錯。幸運的是, Johannes Fahrenkrug已經開發出了一個非常方便的工具,叫做 VertexHelper,它可以用來非常方便地定義頂點,而且可以導出box2d所需要的格式的數據。
好了,先去下載VertexHelper吧。他是一個Mac應用程序,同時包含了源代碼,因此,你只需要打開 ?VertexHelper.xcodeproj,然后編譯并運行就可以了。當運行工程的時候,你會看到下面的屏幕輸出:(為了方便起見,你可以把編譯好的工程放到Application文件夾下面,以后就直接打開就可以了)
繼續,把sprites.jpg拖到VertexHelper中,拖的時候,放置在 “Drop Sprite Image here”標簽上面。在Rows/Cols部分,把相應的數字設置成1和2.VertexHelper會自動地把圖片劃分成兩個部分。
接下來,把”Edit Mode"復選框打上勾,緊接著,你需要按照逆時針方向在精靈的四周定義一些頂點。注意,box2d將自動地把最后一個頂點與第一個頂點連接起來,因此,我們不需要連接它們。
另外一件非常重要的事情是,當你定義頂點的時候,你需要確保定義的多邊形是凸多邊形。這意味著多邊形的內部沒有一個角大于180度。或者說是,多邊形內任何兩個頂點的連線都在多邊形的內部。如果你對這個定義不是很清楚的話,建議百度一下凸多邊形和凹多邊形。當然,也可以看看下面這個js制作的demo。
最后,注意box2d定義了b2_maxPolygonVertices,它限制了每一個shape最多可以定義的頂點的個數,默認值是8.當然,你可以在b2Settings.h中把這個值改掉。但是,這個教程中,我們只需要8個頂點就夠了。
(這個過程最好用一個視頻來演示一下,不過翻不了墻,也看不了了。不過自己多摸索一下,這個工具還是很容易使用的)
一旦你做完之后,在下拉列表中選擇Box2d,Style選擇“Initialization”。然后把右邊生成的代碼copy下來并粘貼到工程中去。
好,讓我們把工程中的shape定義換一下。打開HelloWorldScene.mm,然后修改 addBoxBodyForSprite方法。首先注釋掉一些代碼,如下圖所示:
/*spriteShape.SetAsBox(sprite.contentSize.width/PTM_RATIO/2,
sprite.contentSize.height/PTM_RATIO/2);*/if(sprite.tag==1) {//Uncomment this and replace the number with the number of vertices//for the cat that you defined in VertexHelper//int num = 6;//b2Vec2 verts[] = {b2Vec2(4.5f / PTM_RATIO, -17.7f / PTM_RATIO),//b2Vec2(20.5f / PTM_RATIO, 7.2f / PTM_RATIO),//b2Vec2(22.8f / PTM_RATIO, 29.5f / PTM_RATIO),//b2Vec2(-24.7f / PTM_RATIO, 31.0f / PTM_RATIO),//b2Vec2(-20.2f / PTM_RATIO, 4.7f / PTM_RATIO),//b2Vec2(-11.7f / PTM_RATIO, -17.5f / PTM_RATIO)};//Then add this//spriteShape.Set(verts, num);}else{//Do the same thing as the above, but use the car data this time}
再多啰嗦一句:在選擇Style的時候一定要選擇“Initialization”,因為b2PolygonShape:Set方法對于“Initialization”會自動計算shape的法向量和質心。
一旦做完之后,編譯并運行工程。這時,你會看到shape的邊界基本上和sprite的邊界吻合在一起了,當然碰撞檢測就會更加真實了!
恩,你已經看到了使用box2d來做碰撞檢測的強大了!
發揮想象,去做更多好玩的物理效果游戲吧!
總結!
這里有本教程的完整源代碼。
注意,這里用來做碰撞檢測的代碼,僅僅是box2d里面實現碰撞檢測的一種方式。Lam在cocos2d論壇里面提出了另外一種方法,使用b2CollidePolygons來做碰撞檢測,詳情請參考這里。如果你只想做碰撞檢測,你可能想看看lam的實現。
對于其它開發者全體在他們的項目里面使用cocos2d和box2d,我非常之感興趣!你在你的項目中使用box2d嗎?或者僅僅用它來做碰撞檢測,或者根本不使用它?或者如果你使用box2d來做碰撞檢測,你是怎么做的呢?
歡迎大家踴躍發言,分享自己的經驗和看法,謝謝!
總結
以上是生活随笔為你收集整理的box2d 碰撞检测_(译)如何使用box2d来做碰撞检测(且仅用来做碰撞检测)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux禁止ping
- 下一篇: SQL Server -- SQLse