Тема: "Duck hunt" на QML
Краткая аннотация:
В статье рассматриваются основные моменты создания мобильных игр на основе спрайтовой анимации. Для примера взята игра "Duck hunt". Игра реализована на QML и предназначена для платформы MeeGo Harmattan. Материал статьи акцентирован на следующих вещах:
- теория спрайтовой анимации и реализация ее на QML;
- применение независимых таймеров для анимации спрайтов и реализации логики игры;
- вычисление траектории движения объектов;
- обработка касаний экрана;
- воспроизведение звуков в игре;
- использование вибрации;
- перевод приложения в фоновый режим работы;
В конце статьи затронута тема публикации приложения в магазине Nokia и приведены ссылки на исходный код игры и собранный пакет.
---------------------------------------------------------------------------------------------------------------------------------------
Наверное, каждый время от времени ностальгирует по былым временам. Как собирали вкладыши от «Турбо», играли в кэпсы и дни напролет проводили за приставками . В этой статье мы будем реинкарнировать популярнейшую игру того времени “Duck hunt”. Писать мы ее будем на чистом QML, под нашу любимую платформу MeeGo Harmattan. В конце будет немного о публикации в магазине Nokia.
Гуру программирования вряд ли откроют для себя что-то новое, а вот те, кто только начинает погружаться в загадочный мир программирования, могут почерпнуть для себя что-то интересное.
В итоге у нас получится вот что:
Собственно, задача выглядит следующим образом:
На фоне статичной картинки определенный промежуток времени передвигаются случайным образом анимированные спрайты. При касании экрана – проверяется, есть ли в наличии патроны, и проигрывается звук выстрела. Затем проверяется, находятся ли координаты касания внутри прямоугольника, описанного вокруг передвигающегося спрайта. Если да – аппарат вибрирует, проигрывается звук, анимация спрайта меняется на другую, летят перья и спрайт медленно перемещается вниз экрана (утка убита). При достижении нижней точки – снизу появляется другой спрайт (собака), размахивая подбитой уткой. Если в течение заданного интервала по летающему спрайту так и не попали – передвигаем спрайт за пределы экрана, собака плачет и выпускается новая жертва. Добавим еще вверху экрана скролящийся спрайт с облаками, внизу – панель со статистикой игры и кнопку меню. При нажатии на кнопку отрисовывается панель с кнопками опций. Если в итоге количество пропущенных уток менее трех – стартуем всю сцену сначала, увеличив количество уток и увеличив им скорость перемещения. Если нет – GameOver.
Далее будут рассмотрены только основные моменты создания подобных игр. Подробности можно посмотреть в исходниках, ссылка на GitHub будет в конце статьи.
Про спрайтовую анимацию.
В игре всё построено на спрайтовой анимации, поэтому первым делом создадим новый элемент AnimatedSprite. Чтобы создать новый элемент в QML, достаточно создать файл с одноименным названием, и в нем, используя уже существующие элементы, создать то, что нам надо. Элемент AnimatedSprite будет работать с графическим файлом, в котором все кадры анимации идут последовательно, друг за другом:
Поясню основные моменты нового элемента:
Файл: AnimatedSprite.qml
import QtQuick 1.0
Item{
id:spriteAnimation
property int framesHorizontCount:0 // кол-во изображений по горизонтали
property int framesVerticalCount:0 // кол-во изображений по горизонтали
property int framesCount:(framesHorizontCount*framesVerticalCount)
property int currentFrame:0 // текущий кадр анимации
property string sourcePath:"" // путь до фала со спрайтами
property int animationSpeed:0 // скорость анимации
property int startFrame:0 // номер начального для текущей анимации кадра
property int endFrame:0 // номер последнего для текущей анимации кадра
……………………………………
……………………………………
……………………………………
height:spriteAnimationImage.height/framesVerticalCount
width:spriteAnimationImage.width/framesHorizontCount
// вычисляем высоту и ширину кадра в пикселях
Image{ // элемент, который отрисовывает текущий кадр анимации
id:spriteAnimationImage
source:sourcePath
x:-((spriteAnimation.currentFrame*spriteAnimation.width)-Math.floor(spriteAnimation.currentFrame/spriteAnimation.framesHorizontCount)*(spriteAnimation.framesHorizontCount*spriteAnimation.width))
y:-(Math.floor(spriteAnimation.currentFrame/spriteAnimation.framesHorizontCount)*spriteAnimation.height)
// по этим формулам вычисляем, по каким координатам мы найдем текущий кадр анимации в файле
}
}
В QML2, кстати, появился стандартный элемент AnimatedSprite но мне еще не довелось его попробовать.
На основе нового элемента AnimatedSprite создаем более конкретные элементы: DogAnimation, FeathersSprite, Clouds, DuckSprite, добавив в них состояния (states) для конкретных анимаций. Поясню на основе DogAnimation.qml:
import QtQuick 1.0
Item{
id:dogAnimation
……………………………………
……………………………………
……………………………………
states:[
……………………………………
……………………………………
……………………………………
State {
name: "JUMP"
PropertyChanges { target: dogAnimation; startFrame: 6}
PropertyChanges { target: dogAnimation; endFrame: 8}
……………………………………
// при переходе в состояние "JUMP" объекту будет выставлен начальный кадр анимации, равный 6. Последний кадр – 8. Т.е. в цикле будут отрисовываться только с 6 по 8 кадры из файла со спрайтами.
},
……………………………………
……………………………………
……………………………………
State {
name: "MISS"
PropertyChanges { target: dogAnimation; startFrame: 9}
PropertyChanges { target: dogAnimation; endFrame: 10}
……………………………………
}
]
}
О таймерах.
Отрисовкой элементов AnimatedSprite будут заниматься следующие таймеры:
Timer{
id:dogTimer
interval:dog.animationSpeed
// интервал срабатывания таймера
running:false
// запущен\нет
repeat:true
// повторять ?
onTriggered:{
// обработка срабатывания таймера
if (!mainPage.paused){
// если игра не на паузе
……………………………………
……………………………………
if (dog.currentFrame==dog.endFrame){
……………………………………
dog.currentFrame=dog.startFrame
// кадры кончились. Возвращаемся к первому
} else {
dog.currentFrame++
// переходим к след. кадру
}
}
}
}
Эти же таймеры используются для отслеживания передвижений объекта и для переключения анимаций, к примеру:
if (dog.state=="START_LEVEL"){
if (dog.x>=200){
dog.y=370
dog.targetX=240
dog.targetY=320
dog.animationSpeed=40
dog.state="JUMP"
// меняем анимацию идущей собаки на анимацию прыгающей собаки
dog.z=10
if (!mainPage.muted) chienSaute.play()
}
}
Про траекторию движения.
О траектории движения уток. Добавим переменных в элемент DuckSprite:
x:600
y:360
// текущие координаты утки
targetX: 10+Math.random()*800
targetY: 10+Math.random()*350
// координаты цели. Выбираются случайным образом в пределах экрана
Координаты следующей точки при передвижении вычисляются по следующим нехитрым формулам:
brownDuck.x+=Math.ceil(((brownDuck.targetX-brownDuck.x)/Math.abs(brownDuck.targetX-brownDuck.x))*brownDuck.speed)
brownDuck.y+=Math.ceil(((brownDuck.targetY-brownDuck.y)/Math.abs(brownDuck.targetY-brownDuck.y))*brownDuck.speed)
Если утка долетела до цели – вычисляем координаты следующей цели:
if (Math.ceil((Math.floor(Math.abs(blackDuck.x-blackDuck.targetX)))<=blackDuck.epsilon)&&
(Math.ceil(Math.floor(Math.abs(blackDuck.y-blackDuck.targetY)))<=blackDuck.epsilon)){
blackDuck.targetX=10+Math.random()*800
blackDuck.targetY=10+Math.random()*350
}
Epsilon здесь – некоторая небольшая величина. Служит для предотвращения «подрагивания» утки при подлете к цели на расстояние нескольких пикселей.
Также для выбора анимации важно знать, в какую именно сторону направляется утка (влево-вверх, вверх, вправо-вниз и т.д.). Вычисляется довольно просто – от вычисленных координат следующей точки траектории вычитаем текущие координаты. В зависимости от получившихся знаков разницы (+/-) выбираем правильную анимацию.
Для облаков, идущей собаки и летящих вниз перьев нет смысла вычислять обе новых координаты, т.к. объект движется вдоль одной оси. Таким образом одну координату вычисляем, вторая остается константой.
Об обработке касаний экрана.
Обработкой касаний экрана занимается стандартный элемент MouseArea. Область действия для него – вся рабочая область экрана. Сразу код с подробными комментариями:
MouseArea{
anchors.fill: parent
// область действия – весь родительский элемент (в данном случае – главный экран игры)
onPressed: {
// действие при срабатывании события
if ((!mainPage.paused)&&(dog.state!="START_LEVEL")&&(!initializationFlag)&&(!startMenu.visible)){
// если игра не на паузе и это не начало уровня, и мы не находимся в меню
if (gameStatistic.bullet>0){
// если есть еще патроны
shot.x=mouseX-shot.width/2
shot.y=mouseY-shot.height/2
// вычисляем координаты выстрела
shot.currentFrame=0
shotAnimationTimer.start()
// включаем таймер анимации выстрела
gameStatistic.bullet--
// уменьшаем кол-во оставшихся патронов
rumbleEffect.start();
// вибрируем (подробности далее в статье)
if (!mainPage.muted) shotSound.play()
// если звук не выключен – проигрываем звук выстрела
if ((mouseX>blackDuck.x)&&(mouseX<(blackDuck.x+blackDuck.width))&& (mouseY>blackDuck.y)&&(mouseY<(blackDuck.y+blackDuck.height))){
// если попали в утку
gameStatistic.kill++
// увеличиваем счетчик убитых уток
blackDuck.state = "DEAD"
// меняем анимацию утки на «мертвую утку»
if (!mainPage.muted) duckDeadSound.play()
…………………
…………………
feathers1.x=blackDuck.x-70
feathers1.y=blackDuck.y-70
feathers1.targetX=blackDuck.x
feathers1.targetY=500
feathers1.state="START"
feathers1.currentFrame=-1
feathers1.opacity=1
feathers1.visible=true
featherTimer.start()
// пускаем разлетающиеся во все стороны перья
}
} else {
if (!mainPage.muted) fireSound.play()
// если не попали и звук включен – проигрывается звук выстрела
}
}
}
}
О вибрации.
В QtMobility.feedback 1.1 есть готовый элемент HapticsEffect. Остается его немного настроить и можно использовать:
HapticsEffect {
id: rumbleEffect
attackIntensity: 0.0
attackTime: 0
intensity: 1.0
duration: 250
fadeTime: 0
fadeIntensity: 0.0
}
Чтобы проиграть этот эффект, достаточно вызвать rumbleEffect.start();
О звуках.
Библиотека QtMultimediaKit 1.1. Элемент называется SoundEffect. Чтобы начать его использовать – достаточно указать ему id и, собственно, путь до файла со звуком.
SoundEffect {
id: shotSound
source:"MEDIA/SND/shot.wav"
}
Проигрывается так: shotSound.play()
О публикации в магазине Nokia.
Тут так же ничего сложного. Регистрируемся в магазине: www. publish.nokia.com. Оплачиваем 1 евро. Читаем небольшой мануал (есть на русском). Создаем контент, настраиваем, отправляем на QA. Ждем результатов, правим баги, отправляем заново . Контент успешно проходит QA, публикуется в магазине и мы наслаждаемся отзывами благодарных пользователей N9. Пара нюансов. Если будете продавать контент – с оплатой не все так просто. Оплата будет произведена через месяц после окончания того месяца, когда за вычетом всяческих поборов на вашем аккаунте было более 100 евро. Причем покупать приложение могли как по банковской карте, так и через сотовых операторов. Как я понял, операторы рассчитываются с нокией всего несколько раз в году. А без их платежей довольно трудно набрать нужную для вывода сумму. Нокия, кстати, берет себе всего 30% от итоговой суммы. Плюс у вас должно быть подписанное соглашение с ней. Его пришлют вам по емэйлу сразу после того, как ожидаемая теоритически сумма превысит 100 евро. Вы будете должны его распечатать в двух экземплярах, подписать и отправить в Espoo. Вскоре вам пришлют подписанный и с их стороны договор. Если же к концу года на вашем счету так и не наберется минимальной суммы – тогда вам выплатят то, что есть. Оплата производится исключительно банковским переводом на указанный в аккаунте счет. С платежей вы будете должны самостоятельно уплатить все налоги согласно действующему законодательству РФ. Кстати, законно ли получать частному разработчику суммы в евро от зарубежной компании – для меня так и осталось загадкой. Если кто сталкивался – проконсультируйте, пожалуйста.
По секрету приведу топ стран по количеству покупок одной конкретной игры:
1. С большим отрывом лидирует Mexico
2. Russian Federation
3. Finland
4. Australia
5. Vietnam
6. Argentina
Как видите - не стоит лишать удовольствия попользоваться вашим продуктом наших иностранных единомышленников и делать приложение только на русском языке. Тем более, что в среднестатистической мобильной игре текста не так уж и много.
А где в статистике Китай? . С ним тоже не просто: The content file failed for distribution in mainland China because paid games need to be distributed by licensed partners.
Кстати, одним из требований QA является: приложение в свернутом состоянии должно потреблять минимум ресурсов и никоим образом не издавать никаких звуков. Логично . Достигается следующим образом: в главном цикле игры добавляем проверку
if ((!Qt.application.active)&&(gameStarted)){
mainTheme.stop()
// тушим звуки
pauseMenu.visible=true
mainPage.paused=true
// ставим все циклы на паузу и выводим меню паузы
}
Вот и всё. Надеюсь, не утомил большим количеством кода .
Весь проект залит на GitHub, там вы можете скачать исходники, а также взять собранный .deb пакет, поставить на свой телефон и попробовать игру вживую.
Go create!