1 (24.09.2012 18:28:19 отредактировано qml.portal.ru)

Тема: "Duck hunt" на QML

Краткая аннотация:
В статье рассматриваются основные моменты создания мобильных игр на основе спрайтовой анимации. Для примера взята игра "Duck hunt". Игра реализована на QML и предназначена для платформы MeeGo Harmattan. Материал статьи акцентирован на следующих вещах:
- теория спрайтовой анимации и реализация ее на QML;
- применение независимых таймеров для анимации спрайтов и реализации логики игры;
- вычисление траектории движения объектов;
- обработка касаний экрана;
- воспроизведение звуков в игре;
- использование вибрации;
- перевод приложения в фоновый режим работы;
В конце статьи затронута тема публикации приложения в магазине Nokia и приведены ссылки на исходный код игры и собранный пакет.

---------------------------------------------------------------------------------------------------------------------------------------


Наверное, каждый время от времени ностальгирует по былым временам. Как собирали вкладыши от «Турбо», играли в кэпсы и дни напролет проводили за приставками smile. В этой статье мы будем реинкарнировать популярнейшую игру того времени “Duck hunt”. Писать мы ее будем на чистом QML, под нашу любимую платформу MeeGo Harmattan. В конце будет немного о публикации в магазине Nokia.

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

В итоге у нас получится вот что:

Собственно, задача выглядит следующим образом:
На фоне статичной картинки определенный промежуток времени передвигаются  случайным образом анимированные спрайты.  При касании экрана – проверяется, есть ли в наличии патроны, и проигрывается звук выстрела. Затем проверяется, находятся ли координаты касания внутри прямоугольника, описанного вокруг передвигающегося спрайта. Если да – аппарат вибрирует, проигрывается звук, анимация спрайта меняется на другую, летят перья и спрайт медленно перемещается вниз экрана (утка убита). При достижении нижней точки – снизу появляется другой спрайт (собака), размахивая подбитой уткой. Если в течение заданного интервала по летающему спрайту так и не попали – передвигаем спрайт за пределы экрана, собака плачет и выпускается новая жертва.  Добавим еще вверху экрана скролящийся спрайт с облаками, внизу – панель со статистикой игры и кнопку меню. При нажатии на кнопку отрисовывается панель с кнопками опций.   Если в итоге количество пропущенных уток менее трех – стартуем всю сцену сначала, увеличив количество уток и увеличив им скорость перемещения.  Если нет – GameOver.


Далее будут рассмотрены только основные моменты создания подобных игр. Подробности можно посмотреть в исходниках, ссылка на GitHub будет в конце статьи.


Про спрайтовую анимацию.

В игре всё построено на спрайтовой анимации, поэтому первым делом создадим новый элемент AnimatedSprite. Чтобы создать новый элемент в QML, достаточно создать файл с одноименным названием,  и в нем, используя уже существующие элементы, создать то, что нам надо. Элемент AnimatedSprite будет работать с графическим файлом, в котором все кадры анимации идут последовательно, друг за другом:

http://qml.ucoz.com/MaemoWorld/BlackDuck.png

Поясню основные моменты нового элемента:
Файл: 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}
    ……………………………………
}
    ]
}

http://qml.ucoz.com/MaemoWorld/dog.png


О таймерах.

Отрисовкой элементов 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()
            // если не попали и звук включен – проигрывается звук выстрела
         }
      }
   }
}

http://qml.ucoz.com/MaemoWorld/screen_03-ijunja-12_14-18-33.png


О вибрации.

В 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. Ждем результатов, правим баги, отправляем заново smile. Контент успешно проходит QA, публикуется в магазине и мы наслаждаемся отзывами благодарных пользователей N9. Пара нюансов. Если будете продавать контент – с оплатой не все так просто. Оплата будет произведена через месяц после окончания того месяца, когда за вычетом всяческих поборов на вашем аккаунте было более 100 евро. Причем покупать приложение могли как по банковской карте, так и через сотовых операторов. Как я понял, операторы рассчитываются с нокией всего несколько раз в году. А без их платежей довольно трудно набрать нужную для вывода сумму. Нокия, кстати, берет себе всего 30% от итоговой суммы.  Плюс у вас должно быть подписанное соглашение с ней. Его пришлют вам по емэйлу сразу после того, как ожидаемая теоритически сумма превысит 100 евро. Вы будете должны его распечатать в двух экземплярах, подписать и отправить в Espoo. Вскоре вам пришлют подписанный и с их стороны договор. Если же к концу года на вашем счету так и не наберется минимальной суммы – тогда вам выплатят то, что есть. Оплата производится исключительно банковским переводом на указанный в аккаунте счет. С платежей вы будете должны самостоятельно уплатить все налоги согласно действующему законодательству РФ. Кстати, законно ли получать частному разработчику суммы в евро от зарубежной компании – для меня так и осталось загадкой. Если кто сталкивался – проконсультируйте, пожалуйста.

По секрету приведу топ стран по количеству покупок одной конкретной игры:
1.    С большим отрывом лидирует Mexico
2.    Russian Federation
3.    Finland
4.    Australia
5.    Vietnam
6.    Argentina

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

А где в статистике Китай? smile. С ним тоже не просто: The content file failed for distribution in mainland China because paid games need to be distributed by licensed partners.

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

if ((!Qt.application.active)&&(gameStarted)){
                mainTheme.stop()
                // тушим звуки
                pauseMenu.visible=true
                mainPage.paused=true
                // ставим все циклы на паузу и выводим меню паузы                  
            }


Вот и всё.  Надеюсь, не утомил большим количеством кода smile.

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

Go create!

2

Re: "Duck hunt" на QML

qml.portal.ru, обширно smile
Выдели, пожалуйста, аннотацию.

На то, о чем можно спросить на форуме, не отвечаю через личные сообщения, аську, почту и т.п. для общения есть jabber-конференция: maemo@conference.jabber.org.
QKit - инструментарий для быстрого кроссплатформенного создания приложений на QML.
FAQ по Nokia N9 - читаем, пополняем.
Прежде, чем задать вопрос, воспользуйтесь поиском!

Сайт KiRiK

Поделиться

3

Re: "Duck hunt" на QML

qml.portal.ru
дэбка для n9 или n900 ?

Краш-тест Nokia N900 или Nokia N9 на TechnoCrash!

Nokia N900 Black 32 Gb + Nokia N9 White 64 Gb = вынос мозга big_smile

Поделиться

4

Re: "Duck hunt" на QML

abdrahman пишет:

qml.portal.ru
дэбка для n9 или n900 ?

qml.portal.ru пишет:

Писать мы ее будем на чистом QML, под нашу любимую платформу MeeGo Harmattan.

N900³ - Forever
+ Jolla

5

Re: "Duck hunt" на QML

Eddy737 пишет:
abdrahman пишет:

qml.portal.ru
дэбка для n9 или n900 ?

qml.portal.ru пишет:

Писать мы ее будем на чистом QML, под нашу любимую платформу MeeGo Harmattan.

на N900 тоже пойти должна через meecolay core

Краш-тест Nokia N900 или Nokia N9 на TechnoCrash!

Nokia N900 Black 32 Gb + Nokia N9 White 64 Gb = вынос мозга big_smile

Поделиться

6

Re: "Duck hunt" на QML

KiRiK пишет:

Выдели, пожалуйста, аннотацию.

Добавил в начало статьи аннотацию.
Пофиксил иллюстрации.

7

Re: "Duck hunt" на QML

спасибо шикарное и оч качественная игрушка, с вибрацией вообще супер, побольше бы таких веселых и не замороченных игрушек, и замороченных  тоже )))

Поделиться