実は筆者は状況をよく知らないのだが、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 に反映させている。