その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

UIKit で物理演算エンジンを使用する

cocos2d や Unity などのゲームエンジンや openFrameworks では、標準で物理演算エンジンがサポートされていて手軽に扱えますが、ビューを作成したり画像を表示したりといった基本的な部分の実装方法や、ものによっては使用言語も違うため、「物理演算エンジンを使用したい」というだけの場合はかえって導入障壁が高くなる場合もあります。


それらを使用せず、UIKit ベースでの iOS アプリケーションに物理演算エンジンを単体で導入する方法、すなわち UIView オブジェクトを剛体として物理演算に基づいて動かす方法を紹介します。


準備

1. Box2D のソースをダウンロード

適当なフォルダで、次のように svn の checkout を実行します。

svn checkout http://box2d.googlecode.com/svn/trunk/ box2d-read-only


2. Box2Dをプロジェクトに追加

box2d-read-only/Box2D/Box2D フォルダごと、プロジェクトに追加します。


3. Box2Dのヘッダファイルのパスを登録

Build Settings の "Header Search Path" に

${PROJECT_DIR}

を登録します。


4. 拡張子を変更

Box2D は C++ で書かれているため、Objective-C++ としてコンパイルされるよう、Box2D を使用するクラスファイルの拡張子を .mm にします。


5. ヘッダをインポート

手順4で拡張子を .mm にしたクラスで、ヘッダをインポートします。

#import <Box2D/Box2D.h>


問題がなければ、この時点でビルドに成功します。

物理ワールドと地面の設定

1. メンバ変数を宣言

物理ワールドを保持するためのメンバ変数を宣言します。

@interface ViewController ()
{
    b2World *world;
}
@end


2. 物理ワールドを生成

まず、重力を b2Vec2 型で設定します。

b2Vec2 gravity;
gravity.Set(0.0f, -9.81f);

9.81 は地球表面での重力加速度です。Box2D の座標系が左下を (0, 0) として右上に向かって大きくなるので、重力値は下に向かうという意味でマイナスの符号がついています。


次に設定した重力の値をコンストラクタの引数に渡して b2World 型の物理ワールド(物理演算でシミュレーションする空間)を生成します。

world = new b2World(gravity);
world->SetContinuousPhysics(true);


3. 壁の剛体を生成

画面の上下左右に当たり判定のある壁を生成します。


まず、壁の剛体を b2Body 型として生成します。

// 壁の剛体を定義
b2BodyDef edgeBodyDef;
edgeBodyDef.position.Set(0, 0);

// 壁の剛体を生成
b2Body *edgeBody = world->CreateBody(&edgeBodyDef);


次に、壁の形状を設定します。上下左右に壁を設けるため、形状の型として b2EdgeShape を使用します。


上下左右のそれぞれの辺ごとに両端の座標を設定し、壁の剛体として生成した b2Body オブジェクトに CreateFixture メソッドを用いてセットします。

// 壁の形状を設定
b2EdgeShape edgeShape;
float32 widthByMeter  = screenSize.width  / kPointsPerMeter;
float32 heightByMeter = screenSize.height / kPointsPerMeter;

// 下
edgeShape.Set(b2Vec2(0, 0),
              b2Vec2(widthByMeter, 0));
edgeBody->CreateFixture(&edgeShape, 0);

// 上
edgeShape.Set(b2Vec2(0, heightByMeter),
              b2Vec2(widthByMeter, heightByMeter));
edgeBody->CreateFixture(&edgeShape, 0);

// 左
edgeShape.Set(b2Vec2(0, heightByMeter),
              b2Vec2(0, 0));
edgeBody->CreateFixture(&edgeShape, 0);

// 右
edgeShape.Set(b2Vec2(widthByMeter, heightByMeter),
              b2Vec2(widthByMeter, 0));
edgeBody->CreateFixture(&edgeShape, 0);

上記コードで用いている kPointPerMeter は、「UIKit におけるポイント単位での長さを、Box2D の世界における長さの単位であるメートルに変換する」ための定数で、サンプルでは次のように定義してあります。

#define kPointsPerMeter 16

この定義は、1メートル=16ポイントということを意味しています。

UIViewオブジェクトの剛体を生成

UIView オブジェクト(UIView または UIView を継承するクラスのオブジェクト)に対して b2Body 型の剛体を生成します。ここでは次のように、「引数に渡された UIView オブジェクトに対して剛体を生成する」メソッドを実装します。

- (void)addPhysicalBodyForView:(UIView *)physicalView {
    
    CGSize screenSize = self.view.bounds.size;
    CGPoint pos = physicalView.center;
    CGSize physicalSize = physicalView.bounds.size;

    // 剛体を定義
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position.Set(pos.x / kPointsPerMeter,
                         (screenSize.height - pos.y) / kPointsPerMeter);
    bodyDef.userData = (__bridge void *)physicalView;
    
    // 剛体を生成
    b2Body *body = world->CreateBody(&bodyDef);
    
    // 剛体の形状を設定
    b2PolygonShape dynamicBox;
    
    CGFloat boxHalfWidth  = physicalSize.width  / kPointsPerMeter / 2.0;
    CGFloat boxHalfHeight = physicalSize.height / kPointsPerMeter / 2.0;
    dynamicBox.SetAsBox(boxHalfWidth, boxHalfHeight);
    
    // 形状と各種パラメータを剛体にセット
    b2FixtureDef fixtureDef;
    fixtureDef.shape = &dynamicBox;
    fixtureDef.density = 3.0f;
    fixtureDef.restitution = 0.5f;
    body->CreateFixture(&fixtureDef);
}

ここでは形状の型として「多角形の剛体」を定義できる b2PolygonShape を使用し、SetAsBox メソッドで長方形に設定しています。


また引数に渡された UIView オブジェクトと生成する剛体とを結びつけるために、

  • UIView オブジェクトの座標を Box2D の座標形に変換し、b2BodyDef 構造体の position 要素にセットして位置を合わせる
  • userData 要素にUIView オブジェクトのポインタを格納

ということを行っています。

時間を経過させる

物理演算に基づいて剛体を動かすには、物理ワールドの時間を進める必要があります。


時間管理用のNSTimerを生成します。

@property (nonatomic, assign) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
                                              target:self
                                            selector:@selector(step:)
                                            userInfo:nil
                                             repeats:YES];

ここでは60FPSで動作するようタイマーを生成しています。


時間が1/60秒経過するごとに呼ばれるメソッドを次のように実装します。

- (void)step:(NSTimer *)timer {
    
    // 物理ワールドの時間を進める
    int32 velocityIterations = 8;
    int32 positionIterations = 1;
    
    world->Step(1.0f/60.0f, velocityIterations, positionIterations);
    

    // UIViewオブジェクトを剛体に合わせて移動、回転させる
    CGFloat screenHeight = self.view.bounds.size.height;
    for (b2Body *aBody = world->GetBodyList(); aBody; aBody = aBody->GetNext())
    {

        if (aBody->GetUserData() == NULL) {
            
            continue;
        }
        
        // 剛体のuserDataに格納されているUIViewオブジェクトへのポインタを取得
        UIView *aView = (__bridge UIView *)aBody->GetUserData();
        
        // 剛体の現在位置に合わせUIViewオブジェクトを移動させる
        b2Vec2 bodyPos = aBody->GetPosition();
        CGFloat newCenterX = bodyPos.x * kPointsPerMeter;
        CGFloat newCenterY = screenHeight - bodyPos.y * kPointsPerMeter;
        aView.center = CGPointMake(newCenterX, newCenterY);
        
        // 剛体の現在の角度に合わせUIViewオブジェクトを回転させる
        CGAffineTransform transform;
        transform = CGAffineTransformMakeRotation(-aBody->GetAngle());
        aView.transform = transform;
    }
}

物理ワールドの時間を経過させ、その経過によって生じた剛体の位置と向きの変化を、剛体に結びつけられた UIView オブジェクトに反映させています。

タップで剛体を生成

ビューをタップしたら物理演算に基づいて動作するUIViewオブジェクトを生成するようにします。


次のように UITapGestureRecognizer オブジェクトを生成し、ビューに登録します。

UIGestureRecognizer *gesture;
gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                  action:@selector(tapped:)];
[self.view addGestureRecognizer:gesture];


そしてタップジェスチャーのハンドラメソッドを次のように実装します。

- (void)tapped:(UITapGestureRecognizer *)gesture {
    
    CGRect frame = CGRectMake(0, 0, 50, 30);
    UIView *phisicalView = [[UIView alloc] initWithFrame:frame];
    phisicalView.backgroundColor = [UIColor colorWithRed:0.0
                                                   green:204.0/255.0
                                                    blue:1.0
                                                   alpha:1.0];
    [self.view addSubview:phisicalView];
    
    CGPoint tappedPos = [gesture locationInView:gesture.view];
    phisicalView.center = tappedPos;

    [self addPhysicalBodyForView:phisicalView];
}


UIViewオブジェクトを新たに生成し、手順『UIViewオブジェクトの剛体を生成』で実装した addPhysicalBodyForView: メソッドでその UIView オブジェクトに対して剛体を生成しています。

以上の手順で、画面をタップすると物理演算に基づいて動く UIView オブジェクトが生成されるようになります。

ワールドの解放

生成した物理ワールド(b2World インスタンス)が必要なくなったら、delete で解放します。

delete(world);


ワールドを解放すると、CreateBody や CreateFixture で生成された剛体やフィクスチャも解放されます。


剛体やフィクスチャを個別に解放したい場合は、それぞれ DestroyBody, DestroyFixture を使用します。