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

в 14:23, , рубрики: cocos2d-x, game development, геймдев, личный опыт, Разработка под android

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

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

Автор: AlexeyGogol

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js