Этот пост участвует в конкурсе „ Смартфоны для умных постов “
Несмотря на то, что многие программисты в настоящее время не спешат переводить разработку своих приложений и игр на 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 нашего приложения:
И приведем main.cpp к такому виду:include(qml-box2d/box2d-static.pri)
#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-плагинов в системе.
Но для нашей цели подойдет и первый вариант. Итоговый проект выглядит примерно так:
Если мы теперь попробуем скомпилировать приложение, то увидим стандартный 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.
Но давайте разбавим его статикой.
Или стены, как хотите.
Давайте создадим новый файл 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 стены, обрамляющие мир по краям.
Далее по замыслу у нас есть шарик, который может летать внутри нашего мира и ударяться о стены.
Для описания мяча создайте файл 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: - высота до низа.
Затем давайте запустим его еще раз и посмотрим, что мы получим:
Следующий пункт нашего плана.
Кирпичи, которые нужно сбить мячом.
Но! Нельзя забывать, что нам не хватит места на экране.
Поэтому попробуем реализовать эту часть игры по-другому.
А именно, вверху экрана будет несколько зеленых кирпичей, по которым вы должны постоянно ударять мячом, не давая им покраснеть.
Если кирпич полностью покраснел, ударять по нему бесполезно.
А еще мы введем в игру таймер, отсчитывающий количество времени, пока все кирпичики не станут красными.
Анимация перехода от зеленого к красному будет равна, например, 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 #Разработка игр
-
Проект Mars One – 12 Месяцев 2013 Г.
19 Oct, 24 -
Установка Esxi 5.1 На Nuc (Dc3217By)
19 Oct, 24 -
Реальна Ли Поддержка .Net На Базе Intellij?
19 Oct, 24