Статья будет о создании второй по счету моей игры под 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.
Автор: AlexeyGogol