Qt Quick И Box2D: Моделирование Физики

Этот пост участвует в конкурсе „ Смартфоны для умных постов

Qt Quick и Box2d: моделирование физики

Несмотря на то, что многие программисты в настоящее время не спешат переводить разработку своих приложений и игр на Qt Quick, инфраструктура вокруг самой технологии с каждым днем только растет и развивается.

Так дело дошло до моделирования физики в двумерном пространстве.

Вернее, до появления плагина QML. который позволяет вам интегрировать физический движок Box2D в ваши приложения с присущей Qt Quick легкостью.

Именно об этом мы и поговорим сегодня.

Точнее, давайте рассмотрим на примере реализации простого арканоида, чтобы увидеть, как быстро можно создать простую игру, никогда раньше не работая с физическими движками и почти не зная терминологии.



Соединение QML и Box2d

Прежде всего, нам нужно получить исходники плагина.

Для этого перейдите по ссылке gitorious.org/qml-box2d/qml-box2d/trees/master и правой кнопкой мыши нажмите кнопку «Загрузить мастер как tar.gz».

Давайте пока отложим архив и перейдем в Qt Creator. Здесь мы создаем новый проект, например «Приложение Qt Qucik».

В мастере вводим имя, расположение в файловой системе, выбираем профиль Qt, далее, далее, финиш.

Теперь наступает одна из самых важных частей.

И обычно один из самых сложных на ДРУГИХ языках и технологиях.

Вам необходимо фактически подключить плагин к вновь созданному приложению.

Для этого распакуйте полученный архив в корневой каталог приложения и переименуйте полученный каталог qml-box2d-qml-box2d в qml-box2d. B добавьте одну новую строку в файл .

pro нашего приложения:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

include(qml-box2d/box2d-static.pri)

И приведем main.cpp к такому виду:

#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(); }

Здесь строкой #include «box2dplugin.h» мы подключаем заголовок плагина, а строками

Box2DPlugin plugin; plugin.registerTypes("Box2D");

прописываем в приложении типы Qt/Box2D, которые будут нам доступны и нужны в будущем в QML. Вот и все.

Этого достаточно, чтобы подключить плагин как статически подключаемую библиотеку.

Конечно, плагин можно собрать как самостоятельную единицу и разместить в общей директории всех QML-плагинов в системе.

Но для нашей цели подойдет и первый вариант. Итоговый проект выглядит примерно так:

Qt Quick и Box2d: моделирование физики

Если мы теперь попробуем скомпилировать приложение, то увидим стандартный Hello World, который является шаблоном по умолчанию для проекта в Qt Quick. Но это не интересно.

Мы заинтересованы в использовании физики.



Формализуем описание игры

Итак, мы определились, что будем делать с Арканоидом.

Перечислим, что нам понадобится в игрушке такого плана:

  • Окно по умолчанию — 360х640 — для облегчения переноса на мобильные устройства в будущем.

    И конечно же исправим это в ландшафтном режиме.

  • Фоном приложения является простая картинка, на фоне которой будет удобно играть.

  • 4 стены, ограничивающие наш мир по краям окна.

  • Мяч, летающий внутри мира.

  • Площадка внизу окна для удара по мячу.

  • Вверху окна есть несколько кирпичей, которые нужно сбить нашим шариком.

  • Счетчик времени на экране.

  • Стартовый и финишный экраны игры.



Реализуем выполненную задачу

Именно над этим простым техническим заданием мы и продолжим работать.

Как показано выше, в файле main.cpp мы уже указали нашему приложению запуск в ландшафтном режиме.

Это означает, что нам больше не нужно редактировать код C++.

Откройте файл main.qml и приведите его к виду:

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 } }

Что мы наделали? Мы создали окно размером 640x360, установили его фон и добавили один дочерний элемент типа World, который в будущем должен стать предком всех физических объектов.

Как нетрудно догадаться, объект World описывает весь игровой мир и задает его основные параметры, а именно:

  • Gravity — гравитация по X и Y. Для нашего приложения гравитация не нужна.

  • И несколько параметров, с правильным переводом которых, к сожалению, у меня проблемы: timeStep, VelocityIterations, PositionIterations, FrameTime.
Их описание можно найти в заголовочном файле box2dworld.h. Пустой физический мир в три строчки — это круто.

Но давайте разбавим его статикой.

Или стены, как хотите.

Давайте создадим новый файл QML, назовем его Wall.qml. сложите его рядом с приложением и заполните следующим содержимым:

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 } }



Теория перерыва
Стена, как и все объекты сцены (а объект «Wold» по сути является сценой), являются объектами типа «Тело».

Следовательно, Body является базовым классом для всех физических элементов.

Он имеет следующие свойства:

  • active — включить/выключить физику на элементе
  • линейная скорость - линейное ускорение
  • фикстуры — границы тела, по которым будут обнаруживаться столкновения
  • bodyType — тип кузова: статический, динамический или кинематический.

  • fixRotation - запретить вращение
  • SleepAllowed — разрешить автоматическое отключение физики для экономии ресурсов.

  • LinearDamping, angularDamping, Bullet — непонятно на первый взгляд
Само тело не может справиться с столкновениями с другими объектами.

Чтобы научить тело этому, вам нужно установить свойство светильников.

Значениями этого свойства могут быть Circle, Box и Polygon. Все они являются потомками базового класса Fixture, отвечающего за взаимодействие с другими объектами.

Конечно, он недоступен из QML сам по себе, а только через трех своих потомков.

Для наглядности перечислим доступные свойства.

Класс светильника:

  • плотность - удельный вес
  • трение - сила трения
  • восстановление - эластичность/отдача
  • groupIndex — индекс в группе (предположительно группа — это один объект Body)
  • collidesWith — список объектов.

    с которым текущий объект находится в контакте в текущий момент

  • датчик, категории - дополнительные параметры
Каждый из потомков немного расширяет этот класс своими свойствами:
  • Класс Box не добавляет никаких новых свойств, но использует стандартную ширину и высоту для определения границ прямоугольника.

  • Класс Circle вводит свойство radius, которое, как ни странно, является радиусом круглого объекта, например колеса.

  • Класс Polygon добавляет свойство verticles, содержащее список вершин объекта, для более точного моделирования физики.



Вернемся к практике
Из теории становится понятно, что стена представляет собой физическое тело (Body) типа прямоугольника (Box) и графически изображается картинкой с заливкой.

И теперь, имея одну стену, мы можем создать столько стен, сколько захотим, но их нам понадобится 4. Открываем main.qml и внутри объекта World после Gravity.y:0 добавляем описание наших стен:

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 } }

Сохраняем все и запускаем наше приложение, на экране мы увидим фоновое изображение и 4 стены, обрамляющие мир по краям.



Qt Quick и Box2d: моделирование физики

Далее по замыслу у нас есть шарик, который может летать внутри нашего мира и ударяться о стены.

Для описания мяча создайте файл Ball.qml и наполните его следующим содержимым:

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" } }

То же, что и со стеной, только вместо Box у нас Круг.

Добавим наш шар в созданный нами мир, после описания последней стены в объекте World добавим описание шара:

Ball { id: ball x: parent.width/2 y: parent.height/2 }

Запускаем, видим в центре экрана шарик, который никуда не движется из-за отсутствия гравитации и линейного ускорения.

Какой умный парень.

Следующий шаг — это платформа, представляющая собой единственный орган управления игрока, с помощью которого мы будем отбивать мяч.

По предыдущей схеме создается новый файл Platform.qml, в нем:

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 } }

Этот физический объект отличается от других тем, что мы разрешаем пользователю перемещать его по экрану с помощью курсора мыши/пальца в горизонтальном направлении.

В main.qml после описания Ball добавьте описание платформы:

Platform { id: platform }

В этот момент советую вспомнить о наших стенах.

Что ж, мы точно знаем, что они работают, но поскольку мы ограничены размером экрана, мы можем скрыть наши стены за пределами экрана.

чтобы они не раздражали глаз и не мешались.

Для этого поочередно добавьте к каждому из объектов Стены внутри Мира по одному из свойств: leftMargin: -width влево, rightMargin: -width вправо, topMargin: -height to the top, и BottomMargin: - высота до низа.

Затем давайте запустим его еще раз и посмотрим, что мы получим:

Qt Quick и Box2d: моделирование физики

Следующий пункт нашего плана.

Кирпичи, которые нужно сбить мячом.

Но! Нельзя забывать, что нам не хватит места на экране.

Поэтому попробуем реализовать эту часть игры по-другому.

А именно, вверху экрана будет несколько зеленых кирпичей, по которым вы должны постоянно ударять мячом, не давая им покраснеть.

Если кирпич полностью покраснел, ударять по нему бесполезно.

А еще мы введем в игру таймер, отсчитывающий количество времени, пока все кирпичики не станут красными.

Анимация перехода от зеленого к красному будет равна, например, 20 секундам.

После того, как кирпич станет полностью красным, он исчезает. Если нам удастся попасть по кирпичу, то 20-секундный таймер сбрасывается и кирпич снова начинает краснеть.

Начнем с описания кирпича в файле Brick.qml:

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; } }

Как видите, здесь тоже нет ничего сложного: описание тела, описание его отображения, два состояния с плавной анимацией перехода между ними, таймер, отсчитывающий 20 секунд с перезапуском после каждого столкновения с телом.

ball и вспомогательную функцию show().

В файл main.qml после объявления платформы добавляем объявления наших кирпичей:

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-- }

Кстати, не спрашивайте меня, почему я не использовал элементы Row и Repeat — их использование для автоматического создания элементов типа Body приводит к сбою приложения.

В самом начале файла добавьте объявление новой переменной, предварительно определив высоту и ширину:

property int bricksCount: 5

Мы будем использовать его для подсчета количества оставшихся кирпичей; когда оно достигнет, например, двух, мы завершаем игру.

То есть логика взаимодействия пользователя с игрой будет проста – необходимо, чтобы на экране как можно дольше оставалось хотя бы три кирпичика.

Опишем счетчик секунд в самом низу объекта World:

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 } }

Что нам остается? Осталось только добавить стартовый и финишный экраны и немного улучшить игровую логику.

Собственно, это мелочи, которые в статье можно опустить.

Я просто приведу вам полный окончательный листинг файла main.qml:

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" }



В итоге

Вот такое получилось демо-приложение.

Теперь предлагаю посмотреть, что получилось в итоге, а затем прочитать пару финальных строк и написать свои впечатления о работе разработчиков плагина.

Давайте посмотрим: На мой взгляд, получилось хорошо.

Собственно говоря, на разработку самого приложения и написание этой статьи ушло всего два вечера (вчера и сегодня).

Это, во-первых, говорит о простоте и очень низком барьере входа в разработку с использованием QML, а во-вторых, о качестве кода, которого удается добиться за счет разработчиков как самого Qt-фреймворка, так и сторонних разработчиков, пишущих для него подобные плагины.

и снова.

Плюс.

Хотелось бы, конечно же, отметить, что сам Box2D не привязан ни к какой ОС и является платформенно-независимым, поэтому созданное приложение будет одинаково хорошо работать как на настольных, так и на мобильных платформах.

Ну даже в этом примере можно увидеть скриншоты из-под Windows и видео из-под Linux. Конечно.

В этой статье не рассмотрен весь функционал Box2D, перенесенный в QML; по крайней мере, Джойнты все еще остаются.

С другой стороны, я считаю, что этого материала вполне достаточно, чтобы понять суть вещей.

А уже имея представление о комбинации QML/Box2D, вы легко сможете клепать игрушки с помощью физики.

Это могут быть лабиринты с использованием акселерометра телефона и падающих кубиков, забавные разлеты от ударов друг о друга, автомобили или мотоциклы типа X-Moto и многое другое.

В то же время не будем забывать.

что QML — это всего лишь оболочка над классами C++, а само приложение будет работать так, как если бы оно изначально было написано на C++.

Как обычно, исходники можно скачать со страницы проекта: code.google.com/p/quickanoid/downloads/list Спасибо за ваше время.

Теги: #n9_contest #Qt #qt fast #qml #box2d #Разработка игр

Вместе с данным постом часто просматривают: