Как стать автором
Обновить

История о создании игры

Время на прочтение7 мин
Количество просмотров20K


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

Пример описания одного из уровней на XML
<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.
Теги:
Хабы:
+20
Комментарии11

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн