📜 ⬆️ ⬇️

Qt Quick and Box2d: Simulate Physics

This post participates in the competition " Smart phones for smart posts "
image
Even despite the fact that many programmers at the moment are not in a hurry to put the development of their applications and games on Qt Quick, the infrastructure around the technology itself is growing and developing every day.

So it came to the simulation of physics in two-dimensional space. Or rather, before the appearance of the QML-plugin. which allows, with the inherent Qt Quick, to easily integrate the physics engine Box2D into its applications. That's about it today and talk. Or rather, let's look at the example of the implementation of a simple arkanoid, how quickly you can create a simple game, never before working with physics engines and almost neznaya terminology.

Link QML and Box2d


First of all, we need to get the source 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 “Download master as tar.gz” button. Postpone until the archive aside and go to Qt Creator.

Here, we 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, further, 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 the newly 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 we add one new line to the .pro file of our application:
include(qml-box2d/box2d-static.pri) 

And main.cpp we will result in such type:
 #include <QtGui/QApplication> #include "qmlapplicationviewer.h" #include "box2dplugin.h" Q_DECL_EXPORT int main(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"); 

register in the application the types of Qt / Box2D, which 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, a plugin can be assembled as an independent unit and put in a general directory of all QML plug-ins in the system. But for our goal and the first option. 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 project template in Qt Quick. But it is not interesting. We are interested in using physics.

We formalize the description of the game


So, we have decided what we will do arkanoid. We list what we need in the toy of this plan:

Implement the task


It is for this simple TZ that we will work further. As shown above, in main.cpp, we have already instructed our application to run in landscape mode. It means more need to edit C ++ - we have no code. Open the main.qml file and bring it to the form:
 import QtQuick 1.0 import Box2D 1.0 Image { 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 the size of 640x360, set its background and added one child element of the World type, which in the future should be the ancestor for all physical objects. As it is easy to guess, the World object describes the entire game world and sets its basic parameters, namely:

Their description can be peeped in the box2dworld.h header file.

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

Break on theory

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:

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 base class Fixture, which is responsible for interacting with other objects. Independently, of course, it is not available from QML, but only after three of its descendants. We list the properties available for clarity.
Fixture class:

Each of the descendants slightly extends this class with its own properties:

Back to practice

From theory it becomes clear that the wall is a physical body (Body) of the rectangle type (Box) and is graphically represented as a filled picture. And now, having one wall, we can create as many walls as we like, we also need them 4. Open the 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 at the edges.
image
Further, according to the plan we have a ball that can fly within our world and hit the walls. To describe the ball, create the Ball.qml file and fill it with the following contents:
 import QtQuick 1.0 import Box2D 1.0 Body { 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, but instead of Box we have Circle. Add our ball to the world we created, after describing the last wall in the World object, we add the ball description:
 Ball { id: ball x: parent.width/2 y: parent.height/2 } 

We start, we see the ball in the center of the screen, which does not move anywhere due to the lack of gravity and linear acceleration. From clever what ...
The next step is a platform representing the only player control with which we will beat the ball. According to the previous scheme, the new Platform.qml file, in it:
 import QtQuick 1.0 import Box2D 1.0 Body { 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 the others in that we allow the user to lead them around the screen with the mouse / finger cursor in the horizontal direction. After the Ball description, we add a description of the platform to main.qml:
 Platform { id: platform } 

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

The next item of our plan. Bricks that must 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 bricks of green color, on which it is constantly necessary to hit the ball, preventing them from turning red. If the brick becomes red finally - peel on it already to no purpose. And in the game, we introduce a timer that counts the amount of time until 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, it disappears. If we have time 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; } } ] } function show() { brick.visible = true; brick.active = true; state = "green" } function hide() { brick.visible = false; brick.active = false; } } 

As you can see, there is also nothing complicated here: 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 announcement, we add the advertisements 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 the Body type, the application crashes. At the very beginning of the file we will add the declaration of a new variable, after determining the height and width:
 property int bricksCount: 5 

According to it, we will count the number of remaining bricks, when it reaches the example of two - we end 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. We 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 do we have left? It remains to add the start and finish screens, well, a bit to correct the logic of the game. Actually it is the little things that you can omit in the article. I’ll just give you a complete final listing of the main.qml file:
 import QtQuick 1.0 import 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: 36 text: "0" Timer { id: scoreTimer interval: 1000; running: true; repeat: true onTriggered: secondsPerGame.text = parseInt(secondsPerGame.text) + 1 } } } Item { id:screenStart anchors.fill: parent visible: false Text { id: startGame anchors.centerIn: parent color: "white" font.pixelSize: 36 text: "Start Game!" MouseArea { anchors.fill: parent onClicked: { screen.startGame() } } } } Item { id:screenFinish anchors.fill: parent visible: false Text { id: score anchors.centerIn: parent color: "white" font.pixelSize: 36 text: "Game over! Your score is: " + secondsPerGame.text } Text { id: restartGame anchors { top: score.bottom horizontalCenter: parent.horizontalCenter } color: "white" font.pixelSize: 36 text: "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 concluding lines and write my impressions of the work done by the developers of the plugin. We look:

In my opinion it turned out well. In fact, the development of the application itself and the writing of this article took only two evenings (yesterday and today). First of all, it speaks of the simplicity and very low threshold for entering development using QML, and secondly, of the quality of the code that developers over and over again manage to achieve both the Qt framework and third-party developers writing similar plug-ins 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 desktop as well as on mobile platforms. Well, even in this example, you can see screenshots from under Windows and videos from under Linux.

Of course. This article does not cover all the functionality of Box2D, which was transferred to QML, there are still at least Joint s. On the other hand, I believe that this material is quite enough for understanding the essence of things. And already having an idea about the QML / Box2D bundle, you can easily rivet toys using physics. It can also be labyrinths using the phone’s accelerometer and falling cubes that are fun to blow from each other, cars and cars like X-Moto, and much more. In this case, do not forget. that QML is only a wrapper on C ++ classes and the application itself will work as if originally written in C ++.

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

Thank you for your time.

Source: https://habr.com/ru/post/133586/


All Articles