前回はKarakuri Frameworkを使って、C++でiPhone用のアプリを開発できるという話をしましたが、「iPhone用のBox2Dサンプル」を使ってもほぼC++だけでアプリ開発ができることがわかりました。幸いWindows上でのBox2Dを触った経験があるので、そのときに作ったサンプルを移植することを目指してみます。

今回はまず、iPhone版Box2Dがどういう構造になっているのかを分析していきます。

iPhone版Box2Dはここからダウンロードできます。「Download GNU tarball」というリンクをクリックしてください。Box2Dのバージョンは2.0.2と若干古いのですが、画面表示にOpenGL ESを使い、タッチ操作や加速度センサーにも対応しているので、ゲームの基本形として使うことができます。

http://box2d.svn.sourceforge.net/viewvc/box2d/trunk/

ダウンロードしたファイルを解凍し、その中の「Box2D.xcoredeproj」ファイルを探してXCodeで開き、SDKバージョンなどの更新を行ってビルドすると次のようなプログラムが表示されます。一覧から実行したいテストを選んで実行。ドラッグや傾きで操作できます。ダブルタップで一覧に戻りますが、うまく戻らないこともありました。その辺はどうせ使わないので気にしなくてもいいでしょう。

ipbox2d_0ipbox2d_1

プロジェクト内の構成はこんな感じになっています。

ipbox2d_2

「Source」フォルダ内がBox2Dのソースコード。これはC++版そのままです。「Classes」フォルダ内はiPhone依存の処理で、主にObjective-Cで書かれています。「Tests」フォルダ内は各テストのソースコードですが、これもC++です。

要するにC、C++、Objective-Cの3つを理解していないとこのプロジェクトは理解できないわけです。この辺がiPhone開発のハードルが高いところなんですが……。

 

■Box2DAppDelegateとBox2DViewを調べてみる

今回解析するのは、主に「Classes」フォルダ内のObj-C部分です。

iPhoneアプリでは「××AppDelegate.m/.h」という名前のソースコードが起点になります。メインウィンドウから処理を委譲されており、Windowsでいえば「WinMain」とかウィンドウプロシージャなどに相当します。

★Box2DAppDelegate.mm

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    [application setStatusBarHidden:true];

     [glView removeFromSuperview];

     glView.animationInterval = 1.0 / 60.0;

     testEntriesView=[[TestEntriesViewController alloc] initWithStyle:UITableViewStylePlain];
     [testEntriesView setDelegate:self];
     [glView setDelegate:self];

     [windowaddSubview:[testEntriesViewview]];
}

-(void) selectTest:(int) testIndex
{
     [[testEntriesView view] removeFromSuperview];
     [window addSubview:glView];
     [glView startAnimation];
     [glView selectTestEntry:testIndex];
}

-(void) leaveTest
{
     [glView stopAnimation];
     [glView removeFromSuperview];
     [window addSubview:[testEntriesView view]];
}

アプリ起動時にapplicationDidFinishLaunchingメソッドが呼び出され、その中でglViewを外してtestEntriesViewに付け替えています。これがテスト一覧になっています。テーブルから実行したいテストを選ぶとselectTestメソッドが呼ばれ、そこで今度はtestEntriesViewを外してglViewに付け替えています。つまり、glViewの中からテストの代わりに自分が実行したいC++ソースコードを呼び出せばいいのです。

Box2DAppDelegate.hを見るとglViewの定義はBox2DView.mmに書かれていることがわかります。続いてそちらを解析します。

★Box2DView.mm

-(void) selectTestEntry:(int) testIndex
{
     // Destroy existing scene
     delete test;

     entry = g_testEntries + testIndex;
     test = entry->createFcn();

     doubleClickValidCountdown=0;

     sceneScale=10.0f;
     positionOffset=CGPointMake(0, 0);
     lastWorldTouch=CGPointMake(0, 0);
}

先ほどselectTestメソッドから呼び出されていたselectTestEntryメソッドをチェック。g_testEntriesはTestEntry構造体の配列。ここに各テストプログラムの名前とテスト作成関数のアドレスが入っています。それを+ testIndexでずらして目的の要素を探し、そのcreateFcn関数を呼び出しています。

ということは、Testクラスのサブクラスを作成し、それをg_testEntriesに登録すればいいことになります。

 

■Testクラスを調べてみる

Testクラスの定義はBox2DTest.mmとBox2DTest.hに書かれています。

★Box2DTest.h

class Test
{
public:

     Test();
     virtual ~Test();

     void SetGravity(floatx,float y);
     void SetTextLine(int32 line) { m_textLine = line; }
     void DrawTitle(intx, int y, constchar *string);
     virtual void Step(Settings* settings);
     virtual void Keyboard(unsignedchar key) { B2_NOT_USED(key); }
     void ShiftMouseDown(constb2Vec2& p);
     virtual void MouseDown(constb2Vec2& p);
     virtual void MouseUp(constb2Vec2& p);
     void MouseMove(constb2Vec2& p);
     void LaunchBomb();
     void LaunchBomb(constb2Vec2& position, constb2Vec2& velocity);

     void SpawnBomb(constb2Vec2& worldPt);
     void CompleteBombSpawn(constb2Vec2& p);

     // Let derived tests know that a joint was destroyed.
     virtual void JointDestroyed(b2Joint* joint) { B2_NOT_USED(joint); }
     virtual void BoundaryViolated(b2Body* body) { B2_NOT_USED(body); }
     GLESDebugDraw m_debugDraw;

protected:
     friend class DestructionListener;
     friend class BoundaryListener;
     friend class ContactListener;

     b2AABB m_worldAABB;
     ContactPoint m_points[k_maxContactPoints];
     int32 m_pointCount;
     DestructionListener m_destructionListener;
     BoundaryListener m_boundaryListener;
     ContactListener m_contactListener;

     int32 m_textLine;
     b2World* m_world;
     b2Body* m_bomb;
     b2MouseJoint* m_mouseJoint;
     b2Vec2 m_bombSpawnPoint;
     bool m_bombSpawning;
     b2Vec2 m_mouseWorld;
};

ここからはC++なのでだいぶ見慣れた感じになってきました。Testのサブクラスを作って、必要に応じてvirtual付き関数をオーバーライドしていくのですね。

それと、サンプルを見るとTestクラスのサブクラスでは、Createという名前の静的関数を必ず定義しないといけないようです。

static Test* Create()
{
     return new LineJoint;
}

この関数は自分自身のインスタンスを作って返すだけのもので、Box2DView.mmのselectTestEntryメソッド内から利用されていました。まぁ、機械的にこの通りにやっとけば問題ないですね。

 

ゲームの主な処理はStep関数をオーバーライドして書けばいいですね。

タッチイベントの受け取りは、Box2DView.mmのtouchesBeganメソッドの定義を見るとTestクラスのMouseDown呼び出しているので、これをオーバーライドすればよし。

★Box2DView.mm

- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{

     if (doubleClickValidCountdown> 0)
     {
          [_delegate leaveTest];
          return;
     }
     doubleClickValidCountdown = FRAMES_BETWEEN_PRESSES_FOR_DOUBLE_CLICK;

     panning=false;
     for (UITouch *touch in touches)
     {
          CGPoint touchLocation=[touch locationInView:self];
          CGPoint worldPosition=[self screenSpaceToWorldSpace:touchLocation];
          lastScreenTouch=touchLocation;

          lastWorldTouch=worldPosition;
          test->MouseDown(b2Vec2(lastWorldTouch.x,lastWorldTouch.y));

          if (!test->m_mouseJoint) panning=true;
     }
}

これで大事なことはほぼわかりました。その他の細かく気づいた部分をいくつか列挙します。

●画面を横向きにするにはどうしたらいい?

Box2DViewクラスのインスタンス(glView)は、インターフェースビルダーで作成している模様。そっちで横向き固定にしてしまえばいいかな。

●テキストを表示するには?

GLES-Render.mmの中のGLESDebugDrawクラスの定義を見ると、テキスト表示は現在未実装。完成を待つか自分で作るしかなさそうです。

●画像を表示するには?

画像表示はOpenGL ESを勉強して自分で作ることになります。デバッグドローを無効にしたいときは、Box2DTest.hのSettignsの定義を変えます。

●加速度センサーもサポートされている

加速度センサーで傾きを検知すると、Box2DView内のメソッドから、TestクラスのSetGravity関数が呼び出されます。これをそのままBox2Dの重力設定に反映させているようです。加速度センサーを無視したいときは、SetGravity関数内の処理を書き換える?

 

これで解析終了。次はWindows用に作成したスーパーマリオ風アクションゲームを移植してみましょう。