実は筆者は状況をよく知らないのだが、iOS の世界では cocos2d というのが2Dゲームエンジンのデファクトスタンダードらしい。
かの有名な アングリーバード も cocos2d を使っているらしいぞ。
んで、cocos2d は2次元物理エンジンとして Box2D を使っている。
というわけで、本稿では Qt で Box2D を使用する方法を説明してみることにする。
Box2D 配布パッケージは VS2010、xcode4 でビルド可能なプロジェクトが含まれている。
なので、本稿は VS2010 + QtAddin 環境を前提とする。
QtCreator でも頑張ればビルド出来るかもしれないが、面倒なので筆者はやっていない。
出来た人は教えてね。
筆者は Box2D_v2.2.1.zip をダウンロードした。
解凍して出来る Box2D_v2.2.1 ディレクトリの下に、Build があり、その下に vs2010 と xcode4 がある。
Box2D_v2.2.1 +- Box2D // Box2D ヘッダ・ソースファイル ディレクトリ +- Build | +- vs2010 | | +- Box2D.sln // for VS2010 ソリューションファイル | | +- bin | | +- Debug // デバッグ用ライブラリ、exe がここに生成される | | +- Release // リリース用ライブラリ、exe がここに生成される | +- xcode4 | +- Box2D.xcodeproj // xcode 用プロジェクトファイルだと思われる :
これは何かと言うと、実はオブジェクトを (0, 4) の位置から落下させた時の、1/60秒ごとの x y 座標値と
オブジェクトの角度である。
数値で示されても何だかよくわからない。
というわけで、まずはこの HelloWorld を Qt で表示してみることにする。
本稿では、QMainWindow 派生クラスをメインウィンドウとして説明する。 QDialog や QWidget でも可能である。が、その具体的な方法については説明しない。
たとえば、 C:/Box2D_v2.2.1 に置いたとすれば、C:/Box2D_v2.2.1 をインクルードパスに追加する。
VisualStudio であれば、プロジェクトのプロパティを開き、構成プロパティ>VC++ディレクトリ>インクルードディレクトリ を設定する。
Box2D の各オブジェクトを管理するクラスは b2World である。
メインウィンドウは b2World オブジェクトを保持することにする。
#include <Box2D/Box2D.h> class MainWindow : public QMainWindow { ..... private: b2World *m_world; };
MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) : QMainWindow(parent, flags) { // 下方向の重力ありで、b2World オブジェクト生成 m_world = new b2World(/*gravity=*/ b2Vec2(0.0f, -10.0f)); }
※ 上記ソースでは、HelloWorld と同じように、Y 軸方向の重力をマイナス値に指定しているが、 後で都合により座標系を上下反転するので、最終的はY軸方向の重力はプラス値に設定する。
HelloWorld で見たように、Box2D は物理オブジェクトのデータ値を持っているだけで、画面に表示する機能は無い。
Qt でオブジェクトデータを画面に表示するには、以下の2つの方法がある。
前者の方法は単純・原始的で何でも好きに出来る。が、何でも自分でやらなくてはならず、記述するコード量が増える。
後者の方法は Qt の2次元グラフィックスフレームについての知識が必要となるが、
コード量は少ない。
というわけで、本稿では後者の方法を採用することにする。
QGraphicsScene がモデル、QGraphicsView がビュー、そして MainWindow がコントローラ というわけだ。
#include <Box2D/Box2D.h> class QGraphicsScene; class QGraphicsView; class MainWindow : public QMainWindow { ..... private: b2World *m_world; QGraphicsScene *m_scene; QGraphicsView *m_view; };
#include <QtGui> MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) : QMainWindow(parent, flags) { ..... m_view = new QGraphicsView(m_scene = new QGraphicsScene()); // シーン・ビュー生成 m_view->setRenderHint(QPainter::Antialiasing); // アンチエイリアス m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // スクロールバー非表示 m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setCentralWidget(m_view); // ビューをセントラルウィジットに指定 ..... }
Box2D の長さの単位はメートルだ。オブジェクトの座標は 0.1~10(メートル)の範囲に収めるとよい、とドキュメントには書いてある。
そして、座標系は数学で一般に使われる直交座標系だ。X軸方向は右方向、Y軸方向は下方向がプラスの座標値となる。
QGraphicsScene の場合、単位は通常ピクセルだ。そしてY軸方向は上方向がプラスである。 これはコンピュータプログラミングにおいてよく使われる座標系である。
オブジェクトの位置は b2Body オブジェクトが保持しているので、それを対応する QGraphicsItem の座標値に変換しなくていけない。
その際、原点を合わせるのが分かりやすいと考える。
画面の左上点が (0, 0) で、右方向がX軸のプラスだ。
そして Box2D では下方向がY軸のマイナス方向なので、画面に表示されるオブジェクトのY座標値はマイナスとなる。
だがしかし、Box2B の演算は上下方向に対して対称であり、重力加速度は任意の方向に設定可能である。 したがって、helloWorld で b2Vec2 gravity(0.0f, -10.0f); としていた箇所を b2Vec2 gravity(0.0f, 10.0f); にすれば、 上下を反転出来る。
MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) : QMainWindow(parent, flags) { // y座標プラス方向の重力ありで、b2World オブジェクト生成 m_world = new b2World(/*gravity=*/ b2Vec2(0.0f, 10.0f)); }
次に、スケールだが、Box2D の推奨範囲の 10メートルを 1000ピクセルに対応させるのが分かりやすいと考える。
つまり、Box2D 座標値を QGraphicsItem 座標に変換するには、以下の関数を使うとよい。
#define SCALE 100 void updateItem(const b2Body *body, QGraphicsItem *item) { b2Vec2 position = body->GetPosition(); item->setPos(position.x * SCALE, position.y * SCALE); }
ついでに言うと、Box2D オブジェクトは位置だけでなく角度情報を持っている。
なので、これもいっしょに変換してあげると便利だ。
b2Body の角度の単位はラジアンだが、QGraphicsItem の方はデグリー(1周360度)だ。
また回転方向が逆(QGraphicsItem は時計の回転方向がプラス。Box2D は数学でよく使われる反時計周りがプラスだ。)なのだが、
Box2D 座標系を上下反転するので、回転方向は同一ということになる。
したがって、回転も考慮した変換関数は以下のようになる。
#define SCALE 100 #define PI 3.1415926536 void updateItem(const b2Body *body, QGraphicsItem *item) { b2Vec2 position = body->GetPosition(); item->setPos(position.x * SCALE, position.y * SCALE); float32 angle = body->GetAngle(); item->setRotation((angle * 360.0) / (2 * PI)); }
矩形等のオブジェクトは、Box2D では b2Body オブジェクトとして、QGraphicsScene 上では QGraphicsRectItem オブジェクトとして、
それぞれ独立に存在する。
従って、それらの対応付けしてやる必要がある。
対応付けの方法として以下の方法が考えられる。
最初の2つは、QGraphicsItem オブジェクトが b2Body オブジェクトをポイントする方法、
最後のはその逆の方法である。
どれも大差ないのだが、画面上のオブジェクト全体の更新処理を行うために、全てのオブジェクトを順に処理する必要があり、
b2Body はそのための機構(下記ソース)を提供しているので、それを利用するために最後の方法を採用することにする。
// m_world は Box2D オブジェクト全体を管理する b2World クラスのインスタンス for(b2Body *body = m_world.GetBodyList(); body != 0; body = body->GetNext()) { // body に対する処理 ..... }
Box2D では、微小時間(通常は 1/60秒)毎の処理は void b2World::Step(float32, int32, int32) により行われる。
Box2D HelloWorld では、以下のようなコードになっていた。
for (int32 i = 0; i < 60; ++i) { world.Step(timeStep, velocityIterations, positionIterations); オブジェクトの位置・角度を表示 }
イベントループが止まってしまうから、このようなループを Qt プログラムに持ち込むわけにはいかない。 そこで QTimer を使用する。
class MainWindow : public QMainWindow { ..... protected slots: void onTimer(); private: QTimer *m_timer; ..... };
#define FPS 50 MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) : QMainWindow(parent, flags) { .... m_timer = new QTimer(); connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer())); m_timer->start(1000/FPS); } void MainWindow::onTimer() { float32 timeStep = 1.0f / FPS; int32 velocityIterations = 6; int32 positionIterations = 2; m_world->Step(timeStep, velocityIterations, positionIterations); for(b2Body *body = m_world->GetBodyList(); body != 0; body = body->GetNext()) { void *ptr = body->GetUserData(); if( ptr != 0 ) { updateItem(body, (QGraphicsItem *)ptr); } } }
タイマー処理プログラムは上記のようになる。
Box2D は 60FPS を推奨しているが、1000/60 が割り切れないのが嫌なので 50FPS にしている。
タイマーが発生するごとに、m_world->Step() をコールし、変化した b2Body 座標を、
先に定義した updateItem() をコールすることで QGraphicsItem に反映させている。