前回の記事「2D物理エンジンliquidfunをiOSでコンパイルできるようにする」ではliquidfunをコンパイルできるようにしました。
今回はliquidfunを実際に使って、SpriteKitで箱を落とすことができるようにします。
Worldをつくる
liquidfun (Box2D) では最初にワールドオブジェクトを作成する必要があります。
それにはまず、Box2D.hをインクルードします。
1 | #include <Box2D/Box2d.h> |
そして、MySceneのメンバにb2World
を追加します。
1 2 3 4 | @interface LFSMyScene () { b 2 World* _world; } @end |
次に、_world
を-initWithSize:
で初期化しますが、b2World
にはデフォルトコンストラクタがなく、Objective-Cクラス内でコンストラクタイニシャライザを呼ぶ手段がありません。
そのため、_world
をnew
経由で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 | -( id )initWithSize:(CGSize)size { if ( self = [ super initWithSize :size]) { /* Setup your scene here */ self .backgroundColor = [ SKColor colorWithRed : 0 .15 green : 0 .15 blue : 0 .3 alpha : 1 .0 ]; // Creating a World b 2 Vec 2 gravity( 0 .0f , - 1 0 .0f ); _world = new b 2 World(gravity); } return self ; } |
なお、myLabel
関連のコードは不要なので削除しています。
そして、_world
が生ポインタなので、dealloc
時に削除されるようにしておきます。
1 2 3 | -( void )dealloc { delete _world; } |
地面をつくる
_world
初期化後に、続いて次のようなコードで、地面用の箱として、スクリーン下の見えない位置に、幅はスクリーン幅一杯で、高さは20の箱を生成しています。
1 2 3 4 5 6 7 8 9 10 11 12 | // Creating a ground box CGSize s = UIScreen .mainScreen .bounds .size ; b 2 BodyDef groundBodyDef; groundBodyDef .position .Set (s .width / DISPLAY_SCALE / 2 , - 1 0 .0f ); b 2 Body* groundBody = _world->CreateBody(&groundBodyDef); b 2 PolygonShape groundBox; groundBox .SetAsBox (s .width / DISPLAY_SCALE / 2 , 1 0 .0f ); groundBody->CreateFixture(&groundBox, 0 .0f ); |
ちょっと複雑に見えるのは、Box2Dではbody (位置と速度を保持) とfixture (形状を保持) が分かれているからです。
また、position
セットはオブジェクトの中心を指定しています。
なお、Box2Dおよび、SpriteKitの両方で原点が左下なので、原点を合わせています。
ただし、スケールが違うので、それを次のように調整しています (32ピクセル = 1単位 = 1mになる)。
1 | const float DISPLAY_SCALE = 3 2 .0 ; |
実際試したところ、単位が大きいボディでの挙動がこちらの思う挙動と違っていたので、このように調整しています。
タップで箱を出す
続いて箱を追加できるようにします。
元のテンプレートコードが、既にタップしたときにSKNode
を追加するようになっているので、それを修正していきます。
まず、-touchesBegan:withEvent:
のSKAction
関連のコードも不要なので削除したりして、次のようにします。
1 2 3 4 5 6 7 8 | for ( UITouch *touch in touches) { CGPoint location = [touch locationInNode : self ]; CGSize size = CGSizeMake( 3 2 , 3 2 ); SKSpriteNode *node = [ SKSpriteNode spriteNodeWithColor :UIColor .whiteColor size :size]; node .position = location; [ self addChild :node]; } |
これで、SpriteKit側の箱は作成できました。
UIKit Dynamicsと違って、モデルとビューが別々になっているので、モデルであるBox2Dでも箱をつくる必要があります。新たにb2FixtureDef
を利用していること以外は地面を作ったときと似たコードになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | b 2 BodyDef bodyDef; bodyDef .type = b 2 _dynamicBody; bodyDef .position .Set (location .x / SCALE , location .y / SCALE); b 2 Body* body = _world->CreateBody(&bodyDef); b 2 PolygonShape dynamicBox; dynamicBox .SetAsBox (size .width / SCALE / 2 , size .height / SCALE / 2 ); b 2 FixtureDef fixtureDef; fixtureDef .shape = &dynamicBox; fixtureDef .density = 1 .0f ; fixtureDef .friction = 0 .3f ; fixtureDef .restitution = 0 .8f ; body->CreateFixture(&fixtureDef); body->SetUserData((__bridge void *) node); |
b2FixtureDef
では動的なオブジェクトであることや摩擦などを設定しています。
当たり前ですが、モデルとビューで箱の大きさを一致させておかないと、「なんだか隙間が空いているけど跳ねかえってしまう」なんてことになるので注意してください。
また、最後の1行で、SKNodeのデータをBox2D側で保持するようにしています。これは後で箱の座標を更新するときに利用します。
ノードの生存期間はBox2Dボディの生存期間と一致するはずですので、__bridge
にしています。
箱を動かす
最後にフレーム描画前に呼ばれる-update:
内で、Box2Dのワールドを更新して、更新後の情報に基づいてSpriteKit側の各ノードの位置を更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | -( void )update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ const float 3 2 timeStep = 1 .0f / 6 0 .0f ; const int 3 2 velocityIterations = 6 ; const int 3 2 positionIterations = 2 ; _world->Step(timeStep, velocityIterations, positionIterations); for (b 2 Body* body = _world->GetBodyList(); body != nullptr; body = body->GetNext()) { const b 2 Vec 2 position = body->GetPosition(); const float 3 2 angle = body->GetAngle(); SKNode* node = (__bridge SKNode*) body->GetUserData(); node .position = CGPointMake(position .x * DISPLAY_SCALE , position .y * DISPLAY_SCALE); node .zRotation = angle; } } |
_world->Step()
でBox2Dのダイナミックオブジェクトのちょっとだけ動きます。引数の値はLiquidFun Programmer’s Guideでの値をそのまま使っています。
その後は、_world->GetBodyList()
でリストを辿りながらSpriteKit側の各ノードの位置を更新しており、先ほどボディごとに付けていたユーザデータからノードを取り出して、位置と角度をセットしています。
落ちた箱を消す
下に落ちたボディをノードと共に削除します。そのため、-update:
内のfor
ループ内を次のように書きかえます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | for (b 2 Body* body = _world->GetBodyList(); body != nullptr;) { b 2 Body* next = body->GetNext(); const b 2 Vec 2 position = body->GetPosition(); const float 3 2 angle = body->GetAngle(); SKNode* node = (__bridge SKNode*) body->GetUserData(); if (position .y >= 0 ) { node .position = CGPointMake(position .x * DISPLAY_SCALE , position .y * DISPLAY_SCALE); node .zRotation = angle; } else if (node) { [node removeFromParent ]; _world->DestroyBody(body); } body = next; } |
おわりに
liquidfunとSpriteKitを使って、タップで箱を落とすことができるようにしました。
以前のUIKit Dynamics でのサンプルとあまり実現したものは変わらないように見えますが、途中で変に処理が重くなったりしないので、ゲームには向いています (というか、Box2D自体がゲーム向けを謳っています)。
なお、この時点でのサンプルコードはhttps://github.com/safx/liquidfun-ios-sample/tree/m1に上げておきました。
基本的なコードはLiquidFun Programmer’s Guide: Hello LiquidFunに従っていますので、詳細はそちらも参照ください。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。