qml-box2dを使ってみよう

  • 3
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

さて、Advent Calendarも無事に8日目を迎えました。昨日はasobotさんによるQt CreatorのRC版の情報でした。新しいリリースバージョンが出てくるのが楽しみですね。

さて、今回のカレンダーはここまでQMLを扱ったエントリーなしという驚愕の事実に気がつきまして、本日はQML関係としまして、qml-box2dかる〜くご紹介しておきます。hermit4は、普段QML自体もほとんど使う事がない初心者ですので、色々と間違いもあるかと思います。何か気がつかれた方はコメント下さい。

基礎知識

QMLとは

QMLはQtを使って実装された宣言型UI記述言語です。

MainForm.ui.qml
import QtQuick 2.4

Rectangle {
    property alias mouseArea: mouseArea

    width: 360
    height: 360

    MouseArea {
        id: mouseArea
        anchors.fill: parent
    }

    Text {
        anchors.centerIn: parent
        text: "Hello World"
    }
}
main.qml
import QtQuick 2.4
import QtQuick.Window 2.2

Window {
    visible: true
    MainForm {
        anchors.fill: parent
        mouseArea.onClicked: {
            Qt.quit();
        }
    }
}

このコードは、QMLアプリケーションのプロジェクトを新規作成すると生成されるコードです。MainFormは、Rectangle(長方形)の中にMouseAreaというマウスのイベントを取得可能な見えない領域と、Hello Worldの文字を生成しています。main.qmlでは、Windowの中にMainFormを一つ用意しており、マウスのクリック時に動作を終了するよう定義しています。

上記のような宣言構文で画面上の配置を行い、動作をJavaScriptで定義できるほか、C++で記述した拡張ライブラリを登録して利用する事もできます。

Qtプロジェクトでは、標準ライブラリのQt QuickモジュールとQt Widgetのような標準的なGUIアプリのコントロールを提供するQt Quick Controlsモジュールの2つのライブラリが用意されています。上記の例では、QtQuickモジュールを利用しています。

日本語では、22日目に記事を書いて下さる予定のIoriAyane(折戸さん)による「Qt QuickではじめるクロスプラットフォームUIプログラミング」という書籍が販売されています。QML/Qt Quick初期の頃のものなので、そのままでは色々素直に動かなかったりはしますが、Qtに付属のチュートリアルやサンプルと合わせて、なぜ動かないかも含めて調べていくと、きっといつの間にかQMLが使いこなせるようになるかと思います。

Box2Dとは

昨年のQt Advent Calendarの13日目にyasukikudanさんが、「QGraphcsViewでBox2Dを表示する」というエントリーを登録して下さっています。まぁ、細かい説明なしのシンプルなものなので、なんぞこれって思った方もいるかもしれません。

Box2Dは、2次元上での物理演算エンジンで、ある重力下での質量、速度、摩擦といった力学計算を行ってくれるゲーム用のエンジンです。スマートフォンでゲームをたしなむ方ですと、Angry Birdsの動作演算に利用されているものだといえば、なんとなく想像がつくかもしれません。

元々はC++で作られたものですが、C#, JavaScript, Python, ActionScript等に移植されている人気のエンジンです。
実際、どういう事が出来るのかは、このあとqml-box2dの使い方で見ていきたいと思います。

qml-box2d

QML Box2Dについて

QMLはC++で拡張することができると述べましたが、qml-box2dもその一つで、C++で実装されたBox2DをQML上で動作できるように改良したQMLモジュールライブラリです。現在、GitHubにて公開されています。

Install方法

本日は時間もない1ので、Advent Calendarも乱立するご時世に、わざわざQtを見に来られているので、既にQtの環境はお持ちかと思います。ついでに、Git環境も既に整っているという前提で話を進めてしまいます。わからなければ、コメント残して下さい。あとで手直しします。

$ git clone https://github.com/qml-box2d/qml-box2d.git
$ cd qml-box2d
$ qmake
$ make
$ make install

Windows環境の場合は、Qtメニューからたどれるコマンドプロンプトを開き、環境に応じてvcbarsall.batを通した後、makeの代わりに\Tools\QtCreator\bin\jom.exe を使うと素直にビルドできました。なお、Qtのinstall環境によっては、make installは管理者権限が必要な場合もあります。

make installを実施すると、Qtに、qml-box2dのライブラリがinstallされます。

 例えば、Mac OS X の場合は、イカの通り
./Qt5.5.1/5.5/clang_64/qml/Box2D.2.0/libBox2D.dylib
./Qt5.5.1/5.5/clang_64/qml/Box2D.2.0/qmldir

動作検証

qml-box2dもexampleが色々用意されていますので、まずはそちらを動かしてみましょう。

$ qmlscene <path to qml-box2d>/examples/boxes/boxes.qml

上から3つの箱が落ちてくるだけのシンプルなアプリが実行されるかと思います。
boxes.gif

概要説明

最初にサンプルの内容を説明しよう・・・と思ったのですが、これがまた意外と量が多いので余計にわかりにくくなりそうです。というわけで、まずは、qml-box2dのソースコードを見てみましょう。

box2dplugin.cpp
void Box2DPlugin::registerTypes(const char *uri)
{
#if !defined(STATIC_PLUGIN_BOX2D)
    Q_ASSERT(QLatin1String(uri) == QLatin1String("Box2D"));
#endif

    qmlRegisterType<Box2DWorld>(uri, versionMajor, versionMinor, "World");
    qmlRegisterUncreatableType<Box2DProfile>(uri, versionMajor, versionMinor, "Profile",
                                             QStringLiteral("Property group of World"));
    qmlRegisterType<Box2DBody>(uri, versionMajor, versionMinor, "Body");
    qmlRegisterUncreatableType<Box2DFixture>(uri, versionMajor, versionMinor, "Fixture",
                                             QStringLiteral("Base type for Box, Circle etc."));
    qmlRegisterType<Box2DBox>(uri, versionMajor, versionMinor, "Box");
    qmlRegisterType<Box2DCircle>(uri, versionMajor, versionMinor, "Circle");
    qmlRegisterType<Box2DPolygon>(uri, versionMajor, versionMinor, "Polygon");
    qmlRegisterType<Box2DChain>(uri, versionMajor, versionMinor, "Chain");
    qmlRegisterType<Box2DEdge>(uri, versionMajor, versionMinor, "Edge");
    qmlRegisterType<Box2DDebugDraw>(uri, versionMajor, versionMinor, "DebugDraw");
    qmlRegisterUncreatableType<Box2DJoint>(uri, versionMajor, versionMinor, "Joint",
                                           QStringLiteral("Base type for DistanceJoint, RevoluteJoint etc."));
    qmlRegisterType<Box2DDistanceJoint>(uri, versionMajor, versionMinor, "DistanceJoint");
    qmlRegisterType<Box2DPrismaticJoint>(uri, versionMajor, versionMinor, "PrismaticJoint");
    qmlRegisterType<Box2DRevoluteJoint>(uri, versionMajor, versionMinor, "RevoluteJoint");
    qmlRegisterType<Box2DMotorJoint>(uri, versionMajor, versionMinor, "MotorJoint");
    qmlRegisterType<Box2DWeldJoint>(uri, versionMajor, versionMinor, "WeldJoint");
    qmlRegisterType<Box2DPulleyJoint>(uri, versionMajor, versionMinor, "PulleyJoint");
    qmlRegisterType<Box2DFrictionJoint>(uri, versionMajor, versionMinor, "FrictionJoint");
    qmlRegisterType<Box2DWheelJoint>(uri, versionMajor, versionMinor, "WheelJoint");
    qmlRegisterType<Box2DMouseJoint>(uri, versionMajor, versionMinor, "MouseJoint");
    qmlRegisterType<Box2DGearJoint>(uri, versionMajor, versionMinor, "GearJoint");
    qmlRegisterType<Box2DRopeJoint>(uri, versionMajor, versionMinor, "RopeJoint");
    qmlRegisterType<Box2DRayCast>(uri, versionMajor, versionMinor, "RayCast");

    qmlRegisterUncreatableType<Box2DContact>(uri, versionMajor, versionMinor, "Contact", QStringLiteral("Contact class"));
}

QMLで利用できる型として定義をしているのが上記のメソッドです。

  • World
  • Body
  • [UncreatableType] Fixture
    • Box
    • Circle
    • Polygon
    • Chain
    • Edge
  • DebugDraw
  • [UncreatableType] Joint
    • DistanceJoint
    • PrismaticJoint
    • RevoluteJoint
    • MotorJoint
    • WeldJoint
    • PullyJoint
    • FrictionJoint
    • WheelJoint
    • MouseJoint
    • GearJoint
    • RopeJoint
  • RayCast
  • [UncreatableType] Contact

    World

    シュミレーション上の1つの世界を表します。

  • 重力の設定

  • 物理シュミレーションの調整

  • 指定領域のFixtureの検出

  • 衝突検出と交錯したFixtureの検出

といった機能を持ちます。プロパティグループとしてProfileを保持しています。

Body

物理演算上の基本オブジェクト。ただしこれ自体が衝突したり、跳ねたりする”物”を表しているのではなく、演算上のプロパティに過ぎない。だったらBodyという命名をするなって突っ込みを入れたくなりますよね。

  • 質量
  • 速度
  • 回転慣性
  • 角速度
  • 場所
  • 角度

といった特性を保持します。

Fixture

物理演算上で、物質の形状、大きさ、材質特性を保持します。Bodyに対して割り当てられるもので、FixtureをもったBodyが衝突や、バウンドする演算対象の物質となります。

  • 形状(円や多角形)
  • 衝突特性(どう弾むのか)
  • 摩擦
  • 密度(重さ)

といった特性を保持します。QMLでは、Fixtureは、以下のような種類が用意されています。

  • Box
  • Circle
  • Polygon
  • Chain
  • Edge

DebugDraw

box2dは物理演算エンジンで、描画エンジンではないため、演算結果の数値が求められるだけで、通常は描画しません。DebugDrawは、物理演算のデバッグ用に演算結果を画面上に描画する機能を提供します。

Joint

Body間の制約条件を示すもので、物質同士がどのような関係で存在しているのかという状態をしめします。たとえば固く溶接されているのか、ヒンジのようなもので接続されているのか、そういった関連性をJointで定義します。種類としては、以下のものが用意されています。

  • DistanceJoint
  • PrismaticJoint
  • RevoluteJoint
  • MotorJoint
  • WeldJoint
  • PullyJoint
  • FrictionJoint
  • WheelJoint
  • MouseJoint
  • GearJoint
  • RopeJoint

RayCast

レイキャストは、衝突判定のため、光線のようにまっすぐ伸びる仮想的な線を利用して衝突を判定するための機能を提供しています。

RayCast {
    maxFraction:
}

Contact

box2dはゲームエンジンですので、衝突の結果として、物体の消滅、破損、サウンド効果等を与える必要があります。Contactは物体の衝突について、どう扱うかの定義を行うための機能を提供しています。

シンプルな例

exampleの中に色々入ってはいますが、最初の一歩としては大きすぎるので、簡単なものを作ってみましょう。
テキストエディタか何かで、以下のコードを書いてみて下さい。

simple.qml
import QtQuick 2.4
import QtQuick.Window 2.2
import Box2D 2.0

Window {
    visible: true
    width: 640
    height: 480
    Rectangle {
        id: screen
        anchors.fill: parent
        World { 
            id: physicsWorld 
        }

        DebugDraw {
            visible: true
        }

        Body {
            world: physicsWorld
            bodyType: Body.Dynamic
            Box {
                x: screen.width/2
                y: 10
                width: 10
                height: 10
            }
        }
    }
}

編集が終わったら、qmlsceneで実行してみましょう。

$ qmlscene simple.qml

simple.gif

四角い形状の何かが、画面の上から下へ落ちて行ったかと思います。

DebugDrawを使って、演算結果を画面表示しているのは、Body+Box(Fixture)は、演算の為の形状であって描画対象ではないため、DebugDrawを通さないと画面に何も表示されないからです。
それでは、DebugDrawを外して画面上に表示させるのにはどうしたらよいかというと、表示対象の物を用意してあげる必要があります。

simple2.qml
import QtQuick 2.4
import QtQuick.Window 2.2
import Box2D 2.0

Window {
    visible: true
    width: 640
    height: 480
    Rectangle {
        anchors.fill: parent
        World { 
            id: physicsWorld 
        }

        Rectangle {
            id: rectangle
            x: parent.width/2
            y: 10
            width: 10
            height: 10
            color: "red"
            Body {
                id: boxBody
                target: rectangle
                world: physicsWorld
                bodyType: Body.Dynamic

                Box {
                    id: box
                    x: rectangle.x
                    y: rectangle.y
                    width: rectangle.width
                    height: rectangle.height
                }
            }
        }
    }
}

描画対象となるRectangle(id=rectangle)を用意し、Bodyはtargetをrectangleとして、その移動を演算して反映させるようにします。これで、rectangleは、box2dの演算結果に基づいて、Body+Boxと同様に画面上を落下していくことになるのです。

simple2.gif

実際、何をどう使うと、どんなことができるのかという事は、exampleを一つずつ見ていくのが良いかと思います。

まとめ

本日は、QMLに2D空間での物理演算エンジンであるbox2dを組み合わせたqml-box2dライブラリについてご紹介致しました。ゲームを作ろうとすると、物理演算が必要になってくるわけですが、コレを使うとずいぶんと楽になりそうですね。

なにぶん、十分な時間もなく書き付けている(本日終了30分前にまだ書いてる)ので、ごちゃごちゃしていてすいません。明日は、informationseaさんによる、Windows環境でのStatic版Qtのビルド方法だそうです。お楽しみに。

Appendix.A qml-box2d Examples

ui.qmlをqmlsceneで見れば、選択メニューで一覧できるようですが、一通りのせておきます。

共有アイテム

実装を簡略にするためによく使う機能を名前を付けて使い回せるようにしている

  • shared/BoxBody.qml
    Body + Box Fixtureの組み合わせ
  • shared/ChainBody.qml
    Body + Chain Fixtureの組み合わせ
  • shared/CircleBody.qml
    Body + Circle Fixtureの組み合わせ
  • shared/EdgeBody.qml
    Body + Edgeの組み合わせ
  • shared/ImageBoxBody.qml
    BoxBodyにImageという画像描画を組み合わせたもの
  • shared/PhysicsItem.qml
    Body + Itemの組み合わせ。
  • shared/PolygonBody.qml
    Body + Polygon Fixtureの組み合わせ
  • shared/RectangleBoxBody.qml
    BoxBodyにRectangleという描画領域を組み合わせたもの
  • shared/ScreenBoundaries.qml
    演算対象物が領域外に抜けて行かないようにするためのボーダーを用意するためのもの。

実装例

- accelerometer/accelerometer.qml

accelerometers.gif

BoxesにQMLのAccelerometerを組み合わせたもの。

- boxes/boxes.qml

動作検証時に使ったサンプル。

- cannon/main.qml

canon.gif

大砲をうつというゲームっぽい作りのもの。
なお、画像ではわかりませんが、Shoot時に発射音も出しています。

- contacts/main.qml

ベルトコンベアと吸い込み・吐き出しといった動作にくわえ、吐き出された時に色が変わるといったプロパティ変化もついています。

サイズ圧縮の関係でGif画像はせわしないですが、もっとゆっくり動作してます。
contacts_s.gif

- demolition/demolition.qml

マウスのクリックで弾け飛ぶboxが並んでいます。ぴょんぴょんしているのは、hermit4が適当にクリックしているからです。
demolition.gif

- distance/distance.qml

Joint関連のサンプルですかね。マウスをクリックすると位置によりカラフルな円が回転しながら動作し、それに引っ張られて後ろの長方形も移動します。画像だとちょっとわかりにくいので、実際のサンプルを動かしてみて下さい。

distance.gif

- filtering/filterling.qml

落下したカラフルなボックスのうち、同じ色は重なるデモです。
filtering.gif

- fixtures/main.qml

それぞれのFixtureの例
fixtures.gif

- friction/main.qml

ぶつかった時の動作に関するサンプル

friction.gif

- gear/main.qml

回転体や、重さで開くゲート、動作するゲートなど動くものとの接触などのサンプルでしょうか。
画像を小さくする上でだいぶ早くなっているので、何がなんだかわかりにくいですが・・・。

gear.gif

- impulse

落下したものが弾むサンプル
impulse.gif

- monera/monera.qml

マウスが触れると移動しながらメッセージを表示するサンプル
monera.gif

- motorjoint/boart.qml

MortorJointの例。波間をゆらゆらとボートが揺れるような動きが実装できる。
motorjoint.gif

- mouse/main.qml

Mouseで操作可能なボックスの例でしょうか。マウスでドラッグすると物体を放り投げられます。
mouse.gif

- movingBox/movingBox.qml

キーボードのカーソルキーで赤い四角形を動かすサンプルです。
movingBox.gif

- polygons/polygons.qml

Polygons Fixtureのサンプル
polygons.gif

- prismatic/prismatic.qml

PrismaticJointのサンプル
prismatic.gif

- pulley/main.qml

PullyJointのサンプル
pully.gif

- raycast/main.qml

RayCastのサンプル
raycast.gif

- revolute/revolute.qml

RevoluteJointのサンプル。マウスのクリックで停止、カーソルキーの左右で動く方向や勢いが変えられます。
revolute.gif

- rope/main.qml

RopeJointのサンプル。これはみたままでわかりやすいですね。
rope.gif

- weld/main.qml

WeldJointのサンプル
weld.gif

- wheel

WheelJointのサンプル
wheel.gif

Appendix.B qml-box2d プロパティ

それでは、各タイプについてプロパティを抜き出していってみます。ただの羅列でごめんなさい。そのうちキチンと調べてまとめたいと思います。

World

World {
    running:
    timeStep:
    velocityIterations:
    positionIterations:
    gravity:
    autoClearForces:
    pixelsPerMeter:
    profile:
    enableContactEvents:
}

Profile {
    step:
    collide:
    solve:
    solveInit:
    solveVelocity:
    solvePosition:
    broadphase:
    solveTOI:
    synchronize:
    emitSignals:
}

Body

Body {
    world:
    target:
    linearDamping:
    angularDamping:
    bodyType:
    bullet:
    sleepingAllowed:
    fixedRotation:
    active:
    awake:
    linearVelocity:
    angularVelocity:
    fixtures:
    gravityScale:
}

Fixture

Box {
    // Fixture共通
    density
    friction
    restitution
    sensor
    categories
    collidesWith
    groupIndex

    // Box用
    x
    y
    width
    height
    rotation
}

Circle {
    density
    friction
    restitution
    sensor
    categories
    collidesWith
    groupIndex
    x
    y
    radius
}

Polygon {
    density
    friction
    restitution
    sensor
    categories
    collidesWith
    groupIndex
    vertices
}

Chain {
    density
    friction
    restitution
    sensor
    categories
    collidesWith
    groupIndex
    vertices
    loop
    prevVertex
    nextVertex
}

Edge {
    density
    friction
    restitution
    sensor
    categories
    collidesWith
    groupIndex
    vertices
}

Joint

DistanceJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:

    localAnchorA:
    localAnchorB:
    length:
    frequencyHz:
    dampingRatio:
}

PrismaticJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:

    localAnchorA:
    localAnchorB:
    localAxisA:
    referenceAngle:
    enableLimit:
    lowerTranslation:
    upperTranslation:
    enableMotor:
    maxMotorForce:
    motorSpeed:
}

RevoluteJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:

    localAnchorA:
    localAnchorB:
    referenceAngle:
    enableLimit:
    lowerAngle:
    upperAngle:
    enableMotor:
    motorSpeed:
    maxMotorTorque:
}

MotorJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    linearOffset:
    angularOffset:
    maxForce:
    maxTorque:
    correctionFactor:
}

WeldJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:

    referenceAngle:
    frequencyHz:
    dampingRatio:
    localAnchorA:
    localAnchorB:
}

PullyJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    groundAnchorA:
    groundAnchorB: 
    localAnchorA:
    localAnchorB:
    lengthA:
    lengthB:
    ratio:
}

FrictionJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    localAnchorA:
    localAnchorB:
    maxForce:
    maxTorque:
}

WheelJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    localAnchorA:
    localAnchorB:
    localAxisA:
    dampingRatio:
    frequencyHz:
    maxMotorTorque:
    motorSpeed:
    enableMotor:
}

MouseJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    target:
    maxForce:
    frequencyHz:
    dampingRatio:
}

GearJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    joint1:
    joint2:
    ration:
}

RopeJoint {
    JointType:
    collideConnected:
    bodyA:
    bodyB:
    localAnchorA:
    localAnchorB:
    maxLength:
}

Contact

Contact {
    enabled:
    fixtureA:
    fixtureB:
    childIndexA:
    childIndexB:
    friction:
    restitution:
    tangentSpeed:
}

注釈


  1. Macがハングアップしてまともに動作しなくなり格闘してました。ふと気が付いてカスペルスキーさんを止めたら動いた・・・ 

この投稿は Qt Advent Calendar 20158日目の記事です。