Qt Quick and Box2d: Simulate Physics

  • Tutorial
This post participates in the competition “ Smart Phones for Smart Posts
image
Even though many programmers are in no hurry to transfer the development of their applications and games to the Qt Quick track, the infrastructure around the technology itself only grows and develops every day. .

So, the simulation of physics in two-dimensional space came down to it. Or rather, before the advent of the QML plugin. which allows with the inherent Qt Quick ease to integrate Box2D physics engine into its applications. We’ll talk about this today. More precisely, let’s analyze the implementation of a simple arkanoid, how quickly you can create a simple game, never before working with physical engines and almost unfamiliar terminology.

Bind QML and Box2d


First of all, we need to get the source code of the plugin. To do this, click on the link gitorious.org/qml-box2d/qml-box2d/trees/master and on the right click on the button "Download master as tar.gz". Set aside the archive aside and go to Qt Creator.

Here, create a new project, such as “Qt Qucik Application”. In the wizard, enter the name, location in the file system, select the Qt profile, then continue to complete.

And now one of the most important parts begins. And usually one of the most difficult in OTHER languages ​​and technologies. You must actually connect the plugin to a freshly created application. To do this, unpack the resulting archive into the root directory of the application and rename the resulting directory qml-box2d-qml-box2d to qml-box2d. B add one new line to the .pro file of the application:
include(qml-box2d/box2d-static.pri)

And we will bring main.cpp to this form:
#include<QtGui/QApplication>#include"qmlapplicationviewer.h"#include"box2dplugin.h"Q_DECL_EXPORT intmain(int argc, char *argv[]){
    QScopedPointer<QApplication> app(createApplication(argc, argv));
    Box2DPlugin plugin;
    plugin.registerTypes("Box2D");
    QScopedPointer<QmlApplicationViewer> viewer(QmlApplicationViewer::create());
    viewer->setOrientation(QmlApplicationViewer::ScreenOrientationLockLandscape);
    viewer->setMainQmlFile(QLatin1String("qml/Quickanoid/main.qml"));
    viewer->showExpanded();
    return app->exec();
}

Here the line #include "box2dplugin.h" includes the plugin header, and the lines
    Box2DPlugin plugin;
    plugin.registerTypes("Box2D");

we register in the application the types of Qt / Box2D that will be available and necessary for us in the future in QML.
That's all. This is enough to connect the plugin as a statically linked library. Of course, the plugin can be assembled as an independent unit and put into a common directory of all QML plugins in the system. But the first option is also suitable for our purpose. The appearance of the resulting project is approximately as follows:
image

If we try to compile the application now, we will see the standard Hello World, which is the default template for the project in Qt Quick. But it is not interesting. We are interested in using physics.

We formalize the description of the game


So, we decided what we will do arkanoid. We list what we need in a toy of this plan:
  • The default window is 360x640 - for easier porting in the future to mobile devices. And of course, fixing it in landscape mode.
  • The background of the application is a simple picture, against which it will be convenient to play.
  • 4 walls bordering our world at the edges of the window.
  • A ball flying within the world.
  • Platform at the bottom of the window, for beating the ball.
  • A few bricks in the upper part of the window, which must be knocked down with our ball.
  • Time counter on the screen.
  • Start and finish screens of the game.

We realize the completed task


Here on this simple TK and we will continue to work. As shown above, in main.cpp, we already told our application to start in landscape mode. It means more need to edit C ++ - we do not have code. Open the main.qml file and bring it to the form:
importQtQuick 1.0importBox2D 1.0Image {
    id: screen;
    source: "images/bg.jpeg"
    width: 640
    height: 360
    World {
        id: world
        width: screen.width
        height: screen.height
        gravity.x: 0
        gravity.y: 0
    }
}

What have we done? We created a window with a size of 640x360, set its background and added one child of the World type, which in the future should be the ancestor of all physical objects. As you might guess, the World object describes the entire game world and sets its basic parameters, namely:
  • gravity - X and Y gravity. For our application, gravity is not needed.
  • And a few parameters, with the correct translation of which, unfortunately, I have problems: timeStep, velocityIterations, positionIterations, frameTime

Their description can be found in the header file box2dworld.h The

empty physical world in three lines is cool. But let's dilute it with static. Or walls, whatever you like. Create a new QML file, call it Wall.qml. add next to the application and fill it with the following contents:
import QtQuick 1.0
import Box2D 1.0
Body {
    property aliasimage: image.source
    bodyType: Body.Static
    fixtures: Box {
        anchors.fill: parent
        friction: 1.0
    }
    Rectangle {
        anchors.fill: parentcolor: "brown"
    }
    Image {
        id: image
        anchors.fill: parentsource: "images/wall.jpg"
        fillMode: Image.Tile
    }
}

Theory break

The wall, like all objects on the scene (and the Wold object is essentially a scene), are objects of type Body. Therefore, Body is the base class for all physical elements. It has the following properties:
  • active - enable / disable physics on an element
  • linearVelocity - linear acceleration
  • fixtures - body boundaries by which collisions will be determined
  • bodyType - body type, static, dynamic or kinematic
  • fixedRotation - disable rotation
  • sleepingAllowed - allow physics to automatically turn off to save resources
  • linearDamping, angularDamping, bullet - not clear at first sight

The body, as such, cannot handle collisions with other objects. In order to teach the body this, you must set the fixtures property. Values ​​for this property can be Circle, Box, and Polygon. All of them are descendants of the Fixture base class, which is responsible for interacting with other objects. On its own, it is of course inaccessible from QML, but only through its three descendants. For clarity, we list the available properties.
Fixture Class:
  • density - specific gravity
  • friction - friction force
  • restitution - resilience / recoil
  • groupIndex - the index in the group (presumably the group is one Body object)
  • collidesWith - a list of objects. which the current object is in contact with at the moment
  • sensor, categories - additional parameters

Each of the descendants extends this class a little with its own properties:
  • The Box class does not add new properties, but uses the standard width and height to specify the borders of the rectangle.
  • The Circle class introduces a radius property, which, oddly enough, is the radius of a circular object, such as a wheel.
  • The Polygon class adds a verticles property that contains a list of the object's vertices for a more accurate physical simulation.

Back to practice

From theory it becomes clear that the wall is a physical body (Body) such as a rectangle (Box) and is graphically represented by a picture with a fill. And now, having one wall, we can create as many walls as we like, we need them 4. Open main.qml and inside the World object, after gravity.y: 0, add a description of our walls:
Wall {
    id: wallLeft
    width: 10
    anchors {
        bottom: parent.bottom
        left: parent.left
        top: parent.top
    }
}
Wall {
    id: wallRight
    width: 10
    anchors {
        bottom: parent.bottom
        right: parent.right
        top: parent.top
    }
}
Wall {
    id: wallTop
    height: 10
    anchors {
        left: parent.left
        right: parent.right
        top: parent.top
    }
}
Wall {
    id: wallBottom
    height: 10
    anchors {
        left: parent.left
        right: parent.right
        bottom: parent.bottom
    }
}

We save everything and run our application, on the screen we will see a background image and 4 walls framing the world around the edges.
image
Further, according to the plan, we have a ball that can fly within our world and hit walls. To describe the ball, create the Ball.qml file and fill it with the contents of the following nature:
importQtQuick 1.0importBox2D 1.0Body {
    id: ball
    fixedRotation: false
    sleepingAllowed: false
    fixtures: Circle {
        id: circle
        radius: 12
        anchors.fill: parent
        density: 0;
        friction: 10;
        restitution: 1.05;
    }
    Image {
        id: circleRect
        anchors.centerIn: parent
        width: circle.radius * 2
        height: width
        smooth: true
        source: "images/ball.png"
    }
}

The same as with the wall, only instead of Box we have Circle. Add our ball to the world we created, after describing the last wall in the World object, add the ball description:
Ball {
    id: ball
    x: parent.width/2
    y: parent.height/2
}

We start, we see a ball in the center of the screen, which does not move anywhere for lack of gravity and linear acceleration. What a clever girl ...
The next step is a platform that represents the only player control that we will hit the ball with. According to the previous scheme, the new Platform.qml file, in it:
importQtQuick 1.0importBox2D 1.0Body {
    id: platform
    width: platformBg.width
    height: platformBg.height
    x: parent.width/2 - width/2
    y: parent.height - platformBg.height - 5
    bodyType: Body.Static
    fixtures: Box {
        id: platformBox
        anchors.fill: parent
        friction: 10
        density: 300;
    }
    Image {
        id: platformBg
        smooth: true
        source: "images/platform.png"
    }
    MouseArea {
        anchors.fill: parent
        drag.target: platform
        drag.axis: Drag.XAxis
        drag.minimumX: 0
        drag.maximumX: screen.width - platform.width
    }
}

This physical object differs from others in that we allow the user to drive them across the screen using the mouse / finger cursor in the horizontal direction. In the main.qml, after the Ball description, add the platform description:
Platform {
   id: platform
}

At the moment, I advise you to recall our walls. For the good, we know for sure that they work, but since we are limited by the size of the screen, we can hide our walls outside the screen. so as not to callus eyes and do not interfere. To do this, in turn, in each of the Wall objects inside World, add one of the properties: to the left leftMargin: -width, to the right rightMargin: -width, to the top topMargin: -height, and to the bottom bottomMargin: -height. After that, run again and look at what we get:
image

The next item on our plan. Bricks to be knocked down with a ball. But! We must not forget that we will not have enough space on the screen. Therefore, we will try to implement this part of the game differently. Namely, at the top of the screen there will be several green bricks, on which you constantly need to peel with a ball, not allowing them to turn red. If the brick turns red completely - to beat it is already useless. And in the game we introduce a timer that counts the amount of time until the moment when all the bricks turn red. The animation of the transition from green to red will be equal to, for example, 20 seconds. After the brick turns red completely, it disappears. If we manage to get on the brick, then the 20-second timer is reset and the brick begins to redden again. Let's start with the description of the brick in the Brick.qml file:
import QtQuick 1.0
import Box2D 1.0
Body {
    id: brick
    width: parent.width/5 - 5
    height: 15
    anchors {
        top: parent.top
        topMargin: -height/2
    }
    signal disappear()
    property bool contacted : false
    bodyType: Body.Static
    fixtures: Box {
        anchors.fill: parent
        friction: 1.0
        onBeginContact: {
            contacted = true
        }
        onEndContact: {
            contacted = false
        }
    }
    Timer {
        id: timer
        interval: 20000; running: true; repeat: false
        onTriggered: { brick.visible = false; brick.active = false; disappear(); }
    }
    Rectangle {
        id: brickRect
        anchors.fill: parent
        radius: 6
        state: "green"
        states: [
            State {
                name: "green"
                when: brick.contacted
                PropertyChanges {
                    target: brickRect
                    color: "#7FFF00"
                }
                PropertyChanges {
                    target: timer
                    running: false
                }
            },
            State {
                name: "red"
                when: !brick.contacted
                PropertyChanges {
                    target: brickRect
                    color: "red"
                }
                PropertyChanges {
                    target: timer
                    running: true
                }
            }
        ]
        transitions: [
            Transition {
                from: "green"
                to: "red"
                ColorAnimation { from: "#7FFF00"; to: "red"; duration: 20000; }
            }
        ]
    }
    functionshow(){
        brick.visible = true;
        brick.active = true;
        state = "green"
    }
    functionhide(){
        brick.visible = false;
        brick.active = false;
    }
}

As you can see, there is also nothing complicated: a description of the body, a description of its display, two states with smooth animation of the transition between them, a timer counting down 20 seconds with a restart after each collision with the ball, and the auxiliary function show (). In the main.qml file, after the platform declaration, add the declarations of our bricks:
Brick {
    id: brick1
    x: 3;
    onDisappear: bricksCount--
}
Brick {
    id: brick2
    anchors {
        left:brick1.right
        leftMargin: 5
    }
    onDisappear: bricksCount--
}
Brick {
    id: brick3
    anchors {
        left:brick2.right
        leftMargin: 5
    }
    onDisappear: bricksCount--
}
Brick {
    id: brick4
    anchors {
        left:brick3.right
        leftMargin: 5
    }
    onDisappear: bricksCount--
}
Brick {
    id: brick5
    anchors {
        left:brick4.right
        leftMargin: 5
    }
    onDisappear: bricksCount--
}

By the way, don’t ask me why I didn’t use the Row and Repeat elements - using them to automatically create elements of the Body type, the application crashes. At the very beginning of the file, add the declaration of a new variable, after determining the height and width:
propertyint bricksCount: 5

According to it, we will consider the number of remaining bricks, when it equals two for example - we finish the game. That is, the logic of user interaction with the game will be simple - it is necessary that at least three bricks remain on the screen as much as possible. Let's describe the seconds counter at the very bottom of the World object:
Text {
    id: secondsPerGame
    anchors {
        bottom: parent.bottom
        left: parent.left
    }
    color: "white"
    font.pixelSize: 36
    text: "0"
    Timer {
        interval: 1000; running: true; repeat: true
        onTriggered: secondsPerGame.text = parseInt(secondsPerGame.text) + 1
    }
}

What is left for us? It remains to add start and finish screens, well, and slightly correct the logic of the game. Actually, these are trifles that can be omitted in the article. I will give only finally a complete final listing of the main.qml file:
import QtQuick 1.0import Box2D 1.0
Image {
    id: screen;
    source: "images/bg.jpeg"
    width: 640
    height: 360
    property int bricksCount: 5
    World {
        id: world
        width: screen.width
        height: screen.height
        visible: false
        gravity.x: 0
        gravity.y: 0
        Wall {
            id: wallLeft
            width: 10
            anchors {
                bottom: parent.bottom
                left: parent.left
                leftMargin: -width
                top: parent.top
            }
        }
        Wall {
            id: wallRight
            width: 10
            anchors {
                bottom: parent.bottom
                right: parent.right
                rightMargin: -width
                top: parent.top
            }
        }
        Wall {
            id: wallTop
            height: 10
            anchors {
                left: parent.left
                right: parent.right
                topMargin: -height
                top: parent.top
            }
        }
        Wall {
            id: wallBottom
            height: 10
            anchors {
                left: parent.left
                right: parent.right
                bottom: parent.bottom
                bottomMargin: -height
            }
            onBeginContact: {
                console.log(other)
                finishGame()
            }
        }
        Ball {
            id: ball
            x: parent.width/2
            y: parent.height/2
        }
        Platform {
            id: platform
        }
        Brick {
            id: brick1
            x: 3;
            onDisappear: bricksCount--
        }
        Brick {
            id: brick2
            anchors {
                left:brick1.right
                leftMargin: 5
            }
            onDisappear: bricksCount--
        }
        Brick {
            id: brick3
            anchors {
                left:brick2.right
                leftMargin: 5
            }
            onDisappear: bricksCount--
        }
        Brick {
            id: brick4
            anchors {
                left:brick3.right
                leftMargin: 5
            }
            onDisappear: bricksCount--
        }
        Brick {
            id: brick5
            anchors {
                left:brick4.right
                leftMargin: 5
            }
            onDisappear: bricksCount--
        }
        Text {
            id: secondsPerGame
            anchors {
                bottom: parent.bottom
                left: parent.left
            }
            color: "white"
            font.pixelSize: 36text: "0"
            Timer {
                id: scoreTimer
                interval: 1000; running: true; repeat: true
                onTriggered: secondsPerGame.text = parseInt(secondsPerGame.text) + 1
            }
        }
    }
    Item {
        id:screenStart
        anchors.fill: parent
        visible: falseText {
            id: startGame
            anchors.centerIn: parent
            color: "white"
            font.pixelSize: 36text: "Start Game!"
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    screen.startGame()
                }
            }
        }
    }
    Item {
        id:screenFinish
        anchors.fill: parent
        visible: falseText {
            id: score
            anchors.centerIn: parent
            color: "white"
            font.pixelSize: 36text: "Game over! Your score is: " + secondsPerGame.text
        }
        Text {
            id: restartGame
            anchors {
                top: score.bottom
                horizontalCenter: parent.horizontalCenter
            }
            color: "white"
            font.pixelSize: 36text: "Restart Game!"
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    screen.startGame()
                }
            }
        }
    }
    function startGame() {
        screen.state = "InGame";
        bricksCount = 5
        brick1.show()
        brick2.show()
        brick3.show()
        brick4.show()
        brick5.show()
        secondsPerGame.text = "0"
        platform.x = screen.width/2 - platform.width/2
        ball.linearVelocity.x = 0
        ball.linearVelocity.y = 0
        ball.active = true;
        ball.x = platform.x + platform.width/2
        ball.y = platform.y - ball.height
        ball.x = screen.width/2
        ball.y = screen.height/2
        ball.applyLinearImpulse(Qt.point(50, 300), Qt.point(ball.x, ball.y))
        scoreTimer.running = true
    }
    function finishGame(){
        screen.state = "FinishScreen";
        brick1.hide()
        brick2.hide()
        brick3.hide()
        brick4.hide()
        brick5.hide()
        ball.active = false;
        ball.applyLinearImpulse(Qt.point(0,0), Qt.point(ball.x, ball.y))
        scoreTimer.running = false
    }
    onBricksCountChanged: {
        console.log(bricksCount)
        if (bricksCount <=2){
            finishGame()
        }
    }
    Component.onCompleted: {
        startGame()
    }
    states: [
        State {
            name: "StartScreen"
            PropertyChanges {
                target: screenStart
                visible: true
            }
        },
        State {
            name: "InGame"
            PropertyChanges {
                target: world
                visible: true
            }
        },
        State {
            name: "FinishScreen"
            PropertyChanges {
                target: screenFinish
                visible: true
            }
        }
    ]
    state: "StartScreen"
}

In total


Here is a demo application. Now I propose to look at what happened in the end, and then read a couple of final lines and write your impressions of the work done by the developers of the plug-in. We look:

In my opinion, it turned out well. As a matter of fact, it took only two evenings (yesterday and today) to develop the application itself and write this article. Firstly, it speaks of simplicity and a very low threshold for entering development using QML, and secondly, of the quality of the code that developers can achieve over and over again both the Qt framework itself and third-party developers writing similar plugins for it.

A plus. Of course, I want to note that Box2D itself is not tied to any OS and is platform independent, therefore, the created application will work equally well on both desktop and mobile platforms. Well, even in this example, you can see screenshots from under Windows and video from under Linux.

Of course. in this article, not all Box2D functionality that was ported to QML was considered, at least Joints remained. On the other hand, I believe that this material is quite enough to understand the essence of things. And already having an idea of ​​a bunch of QML / Box2D, you can easily rivet toys using physics. It can be labyrinths using the phone’s accelerometer and falling cubes, fun flying away from hitting each other and cars or motorbikes of the X-Moto type and much more. In this case, do not forget. that QML is just a wrapper on C ++ classes and the application itself will work as if it were originally written in C ++.

As usual, the source code can be collected on the project page: code.google.com/p/quickanoid/downloads/list

Thank you for your time.

Also popular now: