エンジニアリングにはほど遠い

iPhoneアプリとかサイトとかをつくっていくブログです。

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
{
    //アニメーションしたりパーティクル出したり...
}

かなり簡単につくれてしまうので、ホントスゴいと思います。