2014/01/21

liquidfunをSpriteKit上で動かしてみる

前回の記事「2D物理エンジンliquidfunをiOSでコンパイルできるようにする」ではliquidfunをコンパイルできるようにしました。

今回はliquidfunを実際に使って、SpriteKitで箱を落とすことができるようにします。

Worldをつくる

liquidfun (Box2D) では最初にワールドオブジェクトを作成する必要があります。

それにはまず、Box2D.hをインクルードします。

#include <Box2D/Box2d.h>

そして、MySceneのメンバにb2Worldを追加します。

@interface LFSMyScene () {
    b2World* _world;
}
@end

次に、_world-initWithSize:で初期化しますが、b2Worldにはデフォルトコンストラクタがなく、Objective-Cクラス内でコンストラクタイニシャライザを呼ぶ手段がありません。 そのため、_worldnew経由で作成します。

-(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
        b2Vec2 gravity(0.0f, -10.0f);
        _world = new b2World(gravity);
    }
    return self;
}

なお、myLabel関連のコードは不要なので削除しています。

そして、_worldが生ポインタなので、dealloc時に削除されるようにしておきます。

-(void)dealloc {
    delete _world;
}

地面をつくる

_world初期化後に、続いて次のようなコードで、地面用の箱として、スクリーン下の見えない位置に、幅はスクリーン幅一杯で、高さは20の箱を生成しています。

    // Creating a ground box
    CGSize s = UIScreen.mainScreen.bounds.size;

    b2BodyDef groundBodyDef;
    groundBodyDef.position.Set(s.width / DISPLAY_SCALE / 2, -10.0f);

    b2Body* groundBody = _world->CreateBody(&groundBodyDef);

    b2PolygonShape groundBox;
    groundBox.SetAsBox(s.width / DISPLAY_SCALE / 2, 10.0f);

    groundBody->CreateFixture(&groundBox, 0.0f);

ちょっと複雑に見えるのは、Box2Dではbody (位置と速度を保持) とfixture (形状を保持) が分かれているからです。

また、positionセットはオブジェクトの中心を指定しています。

なお、Box2Dおよび、SpriteKitの両方で原点が左下なので、原点を合わせています。

ただし、スケールが違うので、それを次のように調整しています (32ピクセル = 1単位 = 1mになる)。

const float DISPLAY_SCALE = 32.0;

実際試したところ、単位が大きいボディでの挙動がこちらの思う挙動と違っていたので、このように調整しています。

タップで箱を出す

続いて箱を追加できるようにします。

元のテンプレートコードが、既にタップしたときにSKNodeを追加するようになっているので、それを修正していきます。

まず、-touchesBegan:withEvent:SKAction関連のコードも不要なので削除したりして、次のようにします。

    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        CGSize size = CGSizeMake(32, 32);

        SKSpriteNode *node = [SKSpriteNode spriteNodeWithColor:UIColor.whiteColor size:size];
        node.position = location;
        [self addChild:node];
    }

これで、SpriteKit側の箱は作成できました。

UIKit Dynamicsと違って、モデルとビューが別々になっているので、モデルであるBox2Dでも箱をつくる必要があります。新たにb2FixtureDefを利用していること以外は地面を作ったときと似たコードになっています。

    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position.Set(location.x / SCALE, location.y / SCALE);
    b2Body* body = _world->CreateBody(&bodyDef);

    b2PolygonShape dynamicBox;
    dynamicBox.SetAsBox(size.width / SCALE / 2, size.height / SCALE / 2);

    b2FixtureDef 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側の各ノードの位置を更新します。

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */

    const float32 timeStep = 1.0f / 60.0f;
    const int32 velocityIterations = 6;
    const int32 positionIterations = 2;

    _world->Step(timeStep, velocityIterations, positionIterations);

    for (b2Body* body = _world->GetBodyList(); body != nullptr; body = body->GetNext()) {
        const b2Vec2 position = body->GetPosition();
        const float32 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ループ内を次のように書きかえます。

for (b2Body* body = _world->GetBodyList(); body != nullptr;) {
    b2Body* next = body->GetNext();

    const b2Vec2 position = body->GetPosition();
    const float32 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 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。