jarinosuke blog

about software engineering, mostly about iOS

box2dチュートリアル

f:id:jarinosuke0808:20101219154136p:image

第一回

一回目の今回はbox2dについての自分自身での復習も含め、テンプレートのソースを読んでいきたいと思います。

ただ!

このテンプレートが、というかBox2dの仕様なのかよく知りませんが、僕は若干ソースを追いにくく全体を把握できませんでした。
なので、今まで使ってきましたがもう一度Box2dのコードを読み進めながら、それを改良していきたいと思います。Chipmunkは今回は無しです。
また、今回のソースは以下に上げてあるので、pullして照らし合わせながら読んで頂けると分かりやすいと思います。

git clone https://github.com/jarinosuke/KuwagataFalls

おかしな所などありましたら、githubでコメント頂けると嬉しいです。

まずはWorld

box2dは現実と同じように物理の力が働きます。なので「世界」を作らないといけません。
HelloWorldScene.hにb2Worldクラスをクラス変数として加えます。

@interface HelloWorld : CCLayer
{
     b2World* world;
}

そしてinitメソッド内でinitializeします。

          b2Vec2 gravity = b2Vec2(0.0f, -10.0f);
          bool doSleep = true;
          world = new b2World(gravity, doSleep);

上記をみて分かるとおりBox2dのAPIC++で書かれています。
b2Worldクラスの初期化には二つのパラメータが必要です。一つは重力の向き、Box2dではCGPoint型は使用できません。b2Vec2を使います。
二つ目はブール値ですが、これを真にすると、物理エンジン内で活動的に動いていない物体に対して衝突検知などの演算を行わないように設定できます。
これだけでBox2dの「世界」のセットアップは終わりです。

画面の端に「壁」を用意

次は物理エンジンの動作を画面内に止めるために画面の端に「壁」を作りましょう。

b2BodyDef containerBodyDef; 
b2Body* containerBody = world->CreateBody(&containerBodyDef);

後々、staticな物体の他にdynamicな物体も出てきますが、どちらもworldのCreateBodyメソッドで作成します。
ちなみにstaticというのは静的つまり地面や壁など動かない物体のこと、dynamicは動く物体のことで、衝突検知や傾き、速度演算など様々な処理が必要になります。
CreateBodyの引数に用いられているb2BodyDefですが、これはbodyを作るために必要な全ての情報を持つ構造体です。
ただ、これだけではbody(物体)を作っただけに過ぎず、body(物体)の形などのプロパティについて何も決めていません。

壁を象る

以下で画面の四辺を遮る壁の具体的な定義を行っています。
引数で用いている変数については後ほど定義します。

b2PolygonShape screenBoxShape;
int density = 0;

// Bottom
screenBoxShape.SetAsEdge(lowerLeftCorner, lowerRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);

// Top
screenBoxShape.SetAsEdge(upperLeftCorner, upperRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);

// Left side
screenBoxShape.SetAsEdge(upperLeftCorner, lowerLeftCorner);
containerBody->CreateFixture(&screenBoxShape, density);

// Right side
screenBoxShape.SetAsEdge(upperRightCorner, lowerRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);

b2PolygonShapeクラスに一つずつEdge(辺)をセットして、それをcontainerBodyのCreateFixtureメソッドの引数に渡して辺を作成しています。
densityは密度という意味で、動的な物体が衝突したときの挙動に関係してきます。

話が少しそれますが、b2PolygonShapeクラスにはsetAsBoxというメソッドもあり、それを使った方が四辺を囲む箱を簡単に定義できそうな気がしますが、それだと内側にもオブジェクトができてしまうので駄目なのです。
一つ一つ辺を作りましょう。

以下が定義していなかった変数になります。

CGSize screenSize = [CCDirector sharedDirector].winSize;
float widthInMeters = screenSize.width / PTM_RATIO;
float heightInMeters = screenSize.height / PTM_RATIO;
b2Vec2 lowerLeftCorner = b2Vec2(0, 0);
b2Vec2 lowerRightCorner = b2Vec2(widthInMeters, 0);
b2Vec2 upperLeftCorner = b2Vec2(0, heightInMeters);
b2Vec2 upperRightCorner = b2Vec2(widthInMeters, heightInMeters);

ここでPTM_RATIOという見たことのないマクロが組み込まれていることに気付きます。
実はBox2d内では動作を最適化するために、距離をメートル法に合わせています。
実際の値はというと、

#define PTM_RATIO 32

です。慣例的にBox2d内では32pxが1mと扱われているらしいです。なので32px * 32pxの箱形のbodyがあればそれは1mの正方形、ということになります。

ここで少し工夫したいことがあります。
Box2d内では常に座標はメートルを用いないといけません。またb2Vec2はCGPoint型とは同じではありません。
だけれどSpriteの表示位置などはCGPointで扱わないといけません。
これは非常にややこしい、ということでこれらを簡単に変換してくれるメソッドを定義しましょう。

-(b2Vec2) toMeters:(CGPoint)point {
return b2Vec2(point.x / PTM_RATIO, point.y / PTM_RATIO);
}
-(CGPoint) toPixels:(b2Vec2)vec {
return ccpMult(CGPointMake(vec.x, vec.y), PTM_RATIO);
}

これで「壁」は出来上がりです。

物体を生成する

以下に動的物体をBox2d内に生成するコードを記します。

          CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"jarinosuke.tron.png" capacity:TILESET_ROWS * TILESET_COLUMNS];
          [self addChild:batch z:0 tag:kTagBatchNode];
         
          [self addNewSpriteAt:ccp(screenSize.width/2, screenSize.height/2)];

          //以下の一行はbodyを連結させるためのメソッドです。余裕があったら見てみて下さい。
          //[self addSomeJoinedBodies:CGPointMake(screenSize.width / 4, screenSize.height - 50)];
         
          //Schedule step method
          [self scheduleUpdate];

CCSpriteBatchNodeで画像を持ってきて、それをaddしています。
ではaddNewSpriteAtの中身を見てみましょう。

-(void) addNewSpriteAt:(CGPoint)pos
{
     b2BodyDef bodyDef;
     bodyDef.type = b2_dynamicBody;
    
     bodyDef.position = [self toMeters:pos];
   
     bodyDef.userData = [self addRandomSpriteAt:pos];
     b2Body* body = world->CreateBody(&bodyDef);
    
     [self bodyCreateFixture:body];
}

-(void) bodyCreateFixture:(b2Body*)body
{
     b2PolygonShape dynamicBox;
     float tileInMeters = TILESIZE / PTM_RATIO;
     dynamicBox.SetAsBox(tileInMeters * 0.5f, tileInMeters * 0.5f);
    
     b2FixtureDef fixtureDef;
     fixtureDef.shape = &dynamicBox;    
     fixtureDef.density = 0.3f;
     fixtureDef.friction = 0.5f;
     fixtureDef.restitution = 0.6f;
     body->CreateFixture(&fixtureDef);
}

-(CCSprite*) addRandomSpriteAt:(CGPoint)pos
{
     CCSpriteBatchNode* batch = (CCSpriteBatchNode*)[self getChildByTag:kTagBatchNode];
   
     int idx = CCRANDOM_0_1() * TILESET_COLUMNS;
     int idy = CCRANDOM_0_1() * TILESET_ROWS;
     CGRect tileRect = CGRectMake(TILESIZE * idx, TILESIZE * idy, TILESIZE, TILESIZE);
     CCSprite* sprite = [CCSprite spriteWithBatchNode:batch rect:tileRect];
     sprite.position = pos;
     [batch addChild:sprite];
   
     return sprite;
}

「壁」を生成したときと同じように、b2BodyDefを作っています。さきほどと違うのは、それのプロパティにいくつか代入しているところです。
typeに動的物体を表すb2_dynamicBodyを、positionには座標を、userDataには表示するためにCCSpriteが入ります。
そして完成したbodyをbodyCreateFixtureメソッドに渡し、仕上げを行います。そのメソッド内では、「箱」作成時同様、bodyの形を決めたり、bodyの特徴である三大要素のdensity(質量)、friction(摩擦係数)、restitution(反発係数)を設定しています。

三つのメソッドはそれぞれBody、Fixture、Spriteの生成を別々に行っていることが分かります。
何が言いたいというと、Box2d内で扱う情報と、実際に表示するためのSpriteは別々に座標などのプロパテティ情報を処理しなければいけない、ということです。
Spriteは自動的に物理エンジンに沿って動作してくれません。SpriteもBodyもBox2dのworld内でStepメソッドを定期的に実行し、その中で座標や角度を更新しないといけません。以下のようにしてみましょう。

-(void) update:(ccTime)delta {
  
     float timeStep = 0.03f;
     int32 velocityIterations = 8;
     int32 positionIterations = 1;
     world->Step(timeStep, velocityIterations, positionIterations);

     for (b2Body* body = world->GetBodyList(); body != nil; body = body->GetNext()) {
          CCSprite* sprite = (CCSprite*)body->GetUserData();
          if (sprite!= NULL) {
               sprite.position = [self toPixels:body->GetPosition()];
               float angle = body->GetAngle();
               sprite.rotation = CC_RADIANS_TO_DEGREES(angle) * -1;
          }
     }
}

Box2dの更新については、3つのパラメータを元にしています。timeStepは直近のstepから何step過ぎた後にworldを更新するか、velocityIterationsとpositionIterationsについては物理シミュレーションの正確さを決める値になります。
それらを元のworldのstepを定義した後に、worldからGetBodyListを使って全てのbodyにアクセスし、もしSpriteを持っている場合にはSpriteの座標をbodyを元にpxに変換し、角度もラジアンから戻して代入しています。

まとめ

いかがでしたでしょうか。
一通り自分でもテンプレートを見直してみて、やっとBox2dを理解できたように感じます。
Spriteとworldの関係をみても分かるように、Box2dはがちがちなクラス設計ではなく利用者がプログラムを書きやすいように、ゆるく設計されているように僕は思いました。