cocos2d+Box2dで衝突判定をする
結構他でも書かれているとは思いますが、自分的にまとめました。
手順
- b2ContactListenerのサブクラスを作る(MyContactListenerとする)
- HelloWorldLayer上にてb2WorldにMyContactListenerをセットする
- HelloWorldLayerでb2Bodyを作る際に、対となっているPhysicsSpriteをsetUserDataでセットする
- 衝突時に呼ばれるMyContactListenerのメソッド内で衝突したPhysicsSpriteと位置を得る
b2ContactListenerをb2Worldにセットすると、
b2World内で衝突が発生したらその衝突を検知できます。
衝突(b2Contact)からは衝突した2つのb2Bodyが得られます。
4種のメソッドがあります。僕は衝突の強さも得られる
PostSolve(b2Contact* contact, const b2ContactImpulse* impulse)
と言うのを使いました。
しかし、b2bodyから元のスプライトを得ることは出来ません。
そこで、事前にb2BodyにsetUserDataでUserDataとしてスプライトをセットしておきます。
こうすれば衝突したのがどのスプライトなのか分かります。
また、衝突した位置なのですが、上記で得たスプライトのpositionとかってやると駄目です。
最初に配置した位置が返ってきます。
というのも、物理演算で実際の位置と形が変わっているように見えますが、
実態はb2Bodyから得続けたパラメータを使って
CGAffineTransformMakeというメソッドで
見た目を変え続けているだけっぽいからなのです。(ちゃんと分かってません)
なので、contactから得たb2Bodyから位置を得ます。
CGPoint point = CGPointMake(body->GetPosition().x * PTM_RATIO, body->GetPosition().y * PTM_RATIO);
と言う風にPTM_RATIOぶん掛けることで変換できました。
これで、衝突時に2つのスプライトと衝突位置を得られました。
あとはデリゲートで元のシーンでスプライトを動かしたりなどしました。
MyContactListener.h
#import "Box2D.h" #import <set> #import <algorithm> #import "cocos2d.h" #import "PhysicsSprite.h" @protocol contactListenerProtocol -(void) contactWithA:(PhysicsSprite *)a b:(PhysicsSprite *)b position:(CGPoint)point; @end class MyContactListener : public b2ContactListener { public: std::set<b2Body*>contacts; MyContactListener(); ~MyContactListener(); void *hLayer; id<contactListenerProtocol> delegate; virtual void BeginContact(b2Contact* contact); virtual void EndContact(b2Contact* contact); virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold); virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse); };
MyContactListener.mm
#include "MyContactListener.h" #import "Definition.h" MyContactListener::MyContactListener() : contacts() { } MyContactListener::~MyContactListener() { } void MyContactListener::BeginContact(b2Contact* contact) { } void MyContactListener::EndContact(b2Contact* contact) { } void MyContactListener::PreSolve(b2Contact* contact, const b2Manifold* oldManifold) { } void MyContactListener::PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) { b2Body *bodyA = contact->GetFixtureA()->GetBody(); b2Body *bodyB = contact->GetFixtureB()->GetBody(); //bodyからスプライトの位置を算出し、その中点を衝突点としている CGPoint aPoint = CGPointMake(bodyA->GetPosition().x * PTM_RATIO, bodyA->GetPosition().y * PTM_RATIO); CGPoint bPoint = CGPointMake(bodyB->GetPosition().x * PTM_RATIO, bodyB->GetPosition().y * PTM_RATIO); CGPoint contactPoint = ccpMidpoint(aPoint, bPoint); PhysicsSprite *spriteA = (PhysicsSprite *)contact->GetFixtureA()->GetBody()->GetUserData(); PhysicsSprite *spriteB = (PhysicsSprite *)contact->GetFixtureB()->GetBody()->GetUserData(); //衝突の強さ float force = impulse->normalImpulses[0]; //モノ同士の衝突で、強さが指定より大きいときにイベント if (spriteA && spriteB && force > 12) { [delegate contactWithA:spriteA b:spriteB position:contactPoint]; } }
HelloWorldLayer.h
#import "cocos2d.h" #import "Box2D.h" #import "GLES-Render.h" #import "MyContactListener.h" // HelloWorldLayer @interface GaragaraLayer : CCLayer<contactListenerProtocol> { CCTexture2D *spriteTexture_; // weak ref b2World* world; // strong ref GLESDebugDraw *m_debugDraw; // strong ref CCSpriteBatchNode *batchNode; } // returns a CCScene that contains the HelloWorldLayer as the only child +(CCScene *) scene; @end
HelloWorldLayer.mm
//...中略 -(void) initPhysics { b2Vec2 gravity; gravity.Set(0.0f, 0.0f); world = new b2World(gravity); MyContactListener *listener = new MyContactListener(); listener->delegate = self; world->SetContactListener(listener); //...中略 } -(void) addNewSpriteAtPosition:(CGPoint)p { CCLOG(@"Add sprite %0.2f x %02.f",p.x,p.y); CCNode *parent = [self getChildByTag:kTagParentNode]; //We have a 64x64 sprite sheet with 4 different 32x32 images. The following code is //just randomly picking one of the images int idx = (CCRANDOM_0_1() > .5 ? 0:1); int idy = (CCRANDOM_0_1() > .5 ? 0:1); PhysicsSprite *sprite = [PhysicsSprite spriteWithTexture:spriteTexture_ rect:CGRectMake(32 * idx,32 * idy,32,32)]; [parent addChild:sprite]; sprite.position = ccp( p.x, p.y); // Define the dynamic body. //Set up a 1m squared box in the physics world b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO); b2Body *body = world->CreateBody(&bodyDef); body->SetUserData(sprite); //...中略 } #pragma mark contactListenerDelegate -(void) contactWithA:(PhysicsSprite *)a b:(PhysicsSprite *)b position:(CGPoint)point { //アニメーションしたりパーティクル出したり... }
かなり簡単につくれてしまうので、ホントスゴいと思います。