
Статья будет о создании второй по счету моей игры под Android, на кроссплатформенном движке Cocos2D-x (v3.1). Код преимущественно на С++, местами Java и Lua. Попытаюсь вкратце рассказать про основные моменты разработки.
Вступление
Прошел ровно год как я серьезно решил попасть в индустрию. Тогда разработка под мобильные платформы казалось невероятно перспективным, а разработка таких, относительно простых, игр – простой. Будучи 18-летним студентом с небольшим навыком программирования и работы с графикой, но с огромными амбициями хотелось делать что-то свое, что-то огромное и совершенное. Реальность быстро поставила на место и было решено сделать что-то маленькое но максимально использовать весь свой опыт и навыки. Отсутствие ограничений во времени и отсутствие ответственности перед собой растянуло разработку игры и выход в Google Play состоялся аж в конце марта текущего (2014) года. Несмотря на весьма теплые отзывы на разных форумах, результаты совсем не порадовали — 200 загрузок за все время и около 0.30€ с рекламы.

Разочарование только добавило азарта. Проанализировав неудачу и прочитав немного литературы, через полтора месяца решил написать что-то простое чтобы вложиться в месяц-два разработки. Сказано — сделано! Я захотел сделать головоломку-пазл. Суть игры в том что дается изображение и элементы изображения в разных формах (полупрозрачные и перевернутые, например) и нехитрыми манипуляциями надо поставить их на место.

Cocos2D-x
Cocos2D-x — порт популярного движка под iOS. Бесплатный и кроссплатформенный. Если вы только решаете начинать делать игры под мобильные устройства и не знаете какой движок выбрать, обязательно рассмотрите его как вариант.
Преимущества, которые стали ключевыми:
- кроссплатформенность
- C++
- разработка в Windows, с последующим переносом на Android
- поддержка Lua
- открытый исходный код
Отсутствие полноценной документации и уроков усложняют начало работы с движком, но в целом он себя проявил очень хорошо. К тому же комьюнити постоянно расширяет функционал и документацию. Позволяет писать 2D игры практически любой сложности, так как можно вызывать функции OpenGL ES напрямую.
Если кому-то будет интересно то могу написать небольшой туториал по созданию «Hello, world!» и основным моментам.
Концепт
Итак, как было сказано выше, концепт игры в том что дается картинка и некоторые ее части (элементы) разбросаны в разных местах. Задачей игрока является поставить все элементы на свое место. Элементы могут иметь разные свойства, например: вращение, прозрачность, изменение размера и т.д. Также должна быть возможность создания разных форм и разного поведения.

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


В качестве иконок для Google Play и самого приложения использовал скриншот одного из уровней.

Реализация уровней
Из описания ясно что главной задачей есть создание такого кода, который без проблем бы позволил создавать разные уровни без перекомпиляции, и при этом сохранять достаточную гибкость для создания элементов разных видов. Для этих целей были выбраны XML и Lua. XML описывает уровень, позиции элементов, их позиции на картинки, форму, прозрачность, размеры и т.д. А также имеет теги, в которые можно вставить кусочки кода написанного на Lua.
<level pack="1" id="9"> <element shape="circle2.png" x="-196" y="124" texx="-196" texy="124" width="100" height="" rotation="100"> <onCreate> e4Opacity = 5 e4BeginOpacityAnimation = false element:setOpacity(e4Opacity) </onCreate> <onTouchMovedFunction> local dX = touch:getLocation().x - touch:getPreviousLocation().x local dY = touch:getLocation().y - touch:getPreviousLocation().y local scale = level:getScale() element:setRotation(element:getRotation() + dX / 2 / scale+ dY / 2 / scale) </onTouchMovedFunction> <onUpdateFunction> if not e4BeginOpacityAnimation then return end if 150 > e4Opacity then e4Opacity = e4Opacity + 1 element:setOpacity(e4Opacity) end </onUpdateFunction> <onDestroy> e4Opacity = nil e4BeginOpacityAnimation = nil </onDestroy> </element> <element shape="square2.png" x="-359" y="-177" texx="-359" texy="-177" width="100" height="" rotation="180"> <onCreate> e3Opacity = 5 e3BeginOpacityAnimation = false element:setOpacity(e3Opacity) </onCreate> <onTouchMovedFunction> local dX = touch:getLocation().x - touch:getPreviousLocation().x local dY = touch:getLocation().y - touch:getPreviousLocation().y local scale = level:getScale() element:setRotation(element:getRotation() + dX / 2 / scale+ dY / 2 / scale) </onTouchMovedFunction> <onUpdateFunction> if not e3BeginOpacityAnimation then return end if 150 > e3Opacity then e3Opacity = e3Opacity + 1 element:setOpacity(e3Opacity) end </onUpdateFunction> <onDestroy> e3Opacity = nil e3BeginOpacityAnimation = nil e4BeginOpacityAnimation = true </onDestroy> </element> <element shape="triangle2.png" x="319" y="69" texx="319" texy="69" width="150" height="" rotation="180"> <onCreate> e2Opacity = 5 e2BeginOpacityAnimation = false element:setOpacity(e2Opacity) </onCreate> <onTouchMovedFunction> local dX = touch:getLocation().x - touch:getPreviousLocation().x local dY = touch:getLocation().y - touch:getPreviousLocation().y local scale = level:getScale() element:setRotation(element:getRotation() + dX / 2 / scale+ dY / 2 / scale) </onTouchMovedFunction> <onUpdateFunction> if not e2BeginOpacityAnimation then return end if 150 > e2Opacity then e2Opacity = e2Opacity + 1 element:setOpacity(e2Opacity) end </onUpdateFunction> <onDestroy> e2Opacity = nil e2BeginOpacityAnimation = nil e3BeginOpacityAnimation = true </onDestroy> </element> <element shape="waves.png" x="311" y="-227" texx="-365" texy="30" width="200" height="0"> <onCreate> e1Opacity = 10 e1BeginOpacityAnimation = false element:setOpacity(e1Opacity) </onCreate> <onTouchBeganFunction>e1BeginOpacityAnimation = true</onTouchBeganFunction> <onTouchMovedFunction> local dY = touch:getLocation().y - touch:getPreviousLocation().y local dX = touch:getLocation().x - touch:getPreviousLocation().x local scale = level:getScale() element:setPosition(element:getPositionX() + dX / scale, element:getPositionY() + dY / scale) </onTouchMovedFunction> <onUpdateFunction> if not e1BeginOpacityAnimation then return end if 200 > e1Opacity then e1Opacity = e1Opacity + 2 element:setOpacity(e1Opacity) end </onUpdateFunction> <onDestroy> e1Opacity = nil e1BeginOpacityAnimation = nil e2BeginOpacityAnimation = true </onDestroy> </element> <element x="-50" y="50" texx="300" texy="50" width="100" height="100"> <onTouchMovedFunction> local dY = touch:getLocation().y - touch:getPreviousLocation().y local dX = touch:getLocation().x - touch:getPreviousLocation().x local scale = level:getScale() element:setPosition( element:getPositionX() + dX / scale, element:getPositionY() + dY / scale) </onTouchMovedFunction> </element> <element x="50" y="50" texx="-300" texy="-50" width="100" height="100"> <onTouchMovedFunction> local dY = touch:getLocation().y - touch:getPreviousLocation().y local dX = touch:getLocation().x - touch:getPreviousLocation().x local scale = level:getScale() element:setPosition( element:getPositionX() + dX / scale, element:getPositionY() + dY / scale) </onTouchMovedFunction> </element> <element x="-50" y="-50" texx="-400" texy="-100" width="100" height="100"> <onTouchMovedFunction> local dY = touch:getLocation().y - touch:getPreviousLocation().y local dX = touch:getLocation().x - touch:getPreviousLocation().x local scale = level:getScale() element:setPosition( element:getPositionX() + dX / scale, element:getPositionY() + dY / scale) </onTouchMovedFunction> </element> <element x="50" y="-50" texx="400" texy="-100" width="100" height="100"> <onTouchMovedFunction> local dY = touch:getLocation().y - touch:getPreviousLocation().y local dX = touch:getLocation().x - touch:getPreviousLocation().x local scale = level:getScale() element:setPosition( element:getPositionX() + dX / scale, element:getPositionY() + dY / scale) </onTouchMovedFunction> </element> </level>
С помощью Lua можно писать такие функции как:
- onCreate — вызывается при создании элемента
- onTouchBeganFunction — вызывается при «прикосновении» к элементу
- onTouchMovedFunction — вызывается при перемещении прикосновения
- onTouchEndedFunction — вызывается при окончании прикосновения
- onUpdateFunction — вызывается при обновлении элемента
- onDestroy — вызывается при уничтожении элемента
В каждую функции передаются такие переменные как сам эелемент, спрайт элемента, спрайт формы, уровень и спрайт уровня. В функции «onTouch» также передается переменная типа «Touch», которая имеет в себе такие данные как положение прикосновения или предыдущее положение. В связи с тем что Cocos2D-x поддерживает Lua, то можно работать с функциями и объектами Cocos2D-x напрямую, передавая их в качестве аргументов. Такая реализация уровней получилась достаточно эффективной и очень гибкой (ведь даже можно, например, закрыть игру при нажатии на элемент или управлять другими элементами).
Реализация вызова Lua-функций элементов на Cocos2D-x выглядит так (на примере функции onCreate, которая вызывается при создании элемента):
if (!m_scriptFunctionOnCreate.empty()) { LuaEngine* engine = LuaEngine::getInstance(); LuaStack* luaS = engine->getLuaStack(); luaS->executeString(m_scriptFunctionOnCreate.c_str()); lua_getglobal(luaS->getLuaState(), "onCreate"); luaS->pushObject(this, "cc.Node"); // element luaS->pushObject(m_sprite, "cc.Sprite"); // element sprite if (m_shapeMaskSprite != nullptr) { luaS->pushObject(m_shapeMaskSprite, "cc.Sprite"); // shape } else { luaS->pushNil(); } luaS->pushObject(getParent(), "cc.Node"); // level luaS->pushObject(((Level*)getParent())->getLevelSprite(), "cc.Sprite"); // levelSprite lua_call(luaS->getLuaState(), 5, 0); luaS->clean(); }
Вместо заключения
Основной целью создания этой игры было скорее «сделать, чтобы сделать», поэтому нету никакого ожидания успеха. Возможно в ближайшем будущем выложу её исходный код в свободный доступ.
Если будет интересно, то в следующих статьях расскажу про подключения AdMob и Google Analytics в игру, написанную на Cocos2D-x.

