Использование двух редакторов анимации в игровом проекте
Использование двух редакторов анимации в игровом проекте

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

Удаление дочерней анимации из костей родительской анимации может потребоваться при определенных игровых условиях.

Реализация удаления анимации двух редакторов имеет свои особенности. В CocoStudio нам достаточно вызвать функцию удаления, ранее добавленной Bone c дочерней анимацией.

// функция уничтожает двигатель CocoStudio
void JCEngine::_destroyCCSFinal(float /*dt*/)
{
  Node *owner = physicsBody_->getOwner();
	cocostudio::Bone *boneEngine = static_cast<cocostudio::Armature*>(owner)->getBone("engine");
	if (boneEngine)
		static_cast<cocostudio::Armature*>(owner)->removeBone(boneEngine, false);

	armature_ = nullptr;
}

В случае с Dragonbones кроме создания функции удаления, ранее добавленного Slot с дочерней анимацией,

// функция уничтожает двигатель Dragonbones
void JCEngine::_destroyDGBFinal(float /*dt*/)
{
	Node *owner = physicsBody_->getOwner();
	auto slotEngine = static_cast<dragonBones::CCArmatureDisplay*>(owner)->getArmature()->getSlot("engine");
	if (slotEngine)
		static_cast<dragonBones::CCArmatureDisplay*>(owner)->getArmature()->removeSlot(slotEngine);

	armature_->removeFromParent();
	this->_disposeArmature();
	armature_ = nullptr;
}

необходимо внести небольшие дополнения в код DragonBonesC++ RunTime. Изменения вносятся в несколько файлов:

Директория {project folder}\Classes\dragonBones\armature, файл Slot.h 

строка 152, добавить объявление функции

void clearSlot();

Директория {project folder}\Classes\dragonBones\armature, файл Slot.cpp

Строка 16, добавить функцию

// очистка слота при его удалении
void Slot::clearSlot()
{
  _onClear();
}

Директория {project folder}\Classes\dragonBones\armature, файл Armature.h 

строка 311, добавить объявление функции

void removeSlot(Slot *value);

Директория {project folder}\Classes\dragonBones\armature, файл Armature.cpp

строка 462, добавить функцию

// функция удаляет слот из списка слотов анимации
void Armature::removeSlot(Slot* value)
{
  _slots.erase(std::find(_slots.begin(), _slots.end(), value));
  value->clearSlot();
}

Далее хотел бы обратить внимание на один тонкий момент при удалении дочерней анимации из кости персонажа. Удаление необходимо производить после осуществления всех внутренних расчётов движка.

У нас в игре удаление двигателя осуществляется после того, как полностью показана анимация его разрушения. Поэтому внутри функций обрабатывающей события анимации необходимо запустить один раз планировщик событий с нулевым временем. Это делается для того, чтобы приступить к удалению двигателя персонажа после всех расчетов внутри движка.

Функции для двух редакторов анимации немного отличаются друг от друга, но в целом, различия незначительные.

// функция обрабатывает окончание проигрывания анимации CocoStudio
void JCEngine::_onCCSAnimationEvent(cocostudio::Armature* /*armature*/, cocostudio::MovementEventType movementType, const std::string& movementID)
{
	if (movementType == cocostudio::MovementEventType::COMPLETE)
	{
		if (movementID.compare("Destroy") == 0)
		{
			Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR(JCEngine::_destroyCCSFinal), this, 0.0f, 0.0f, 0.0f, false);
		}
	}
}
// функция обратного вызова при завершении конечной анимации Dragonbones
void JCEngine::_onDGBAnimationEvent(cocos2d::EventCustom* event)
{
	const auto eventObject = (dragonBones::EventObject*)event->getUserData();
	if (eventObject->animationState->name.compare("Destroy") == 0)
	{
		Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR(JCEngine::_destroyDGBFinal), this, 0.0f, 0.0f, 0.0f, false);
	}
}

Отличается также и применение шейдеров к анимации, созданной в CocoStudio и Dragonbones.

Во второй версии движка, можно было применять шейдер к анимации CocoStudio без каких-либо дополнительных действий. Но в третьей версии cocos2d-x необходимо самостоятельно применять шейдер ко всем дочерним элементам анимации. В случае с Dragonbones шейдер применяется средствами движка ко всем дочерним элементам анимации.

шейдеры
шейдеры

В результате работа с шейдерами для двух редакторов анимации может быть реализована следующим образом.

// рекурсивная функция применения шейдера
void JCChangeColor::_applyShader(Node *node, int tag)
{
	if (!node)
		return;

	Sprite *sprite = dynamic_cast<Sprite*>(node);
	cocostudio::Armature *ccsArmature = dynamic_cast<cocostudio::Armature*>(node);

	if (ccsArmature)
		this->_applyShaderCCSBones(ccsArmature, tag);
	else
		this->_applyShaderNode(sprite, tag);

	auto children = node->getChildren();
	for (auto child : children)
	{
		this->_applyShader(child, tag);
	}
}
// функция применяет шейдер к ноде
void JCChangeColor::_applyShaderNode(Node *node, int tag)
{
	if (!node)
		return;

	if (tag >= 0)
	{
		GLProgram* program = new GLProgram();
		program->initWithByteArrays(ccPositionTextureColor_noMVP_vert, szEffectFragSource);

		node->setGLProgram(program);
    
    program->autorelease();
		CHECK_GL_ERROR_DEBUG();

		program->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
		program->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);
		program->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
		CHECK_GL_ERROR_DEBUG();

		program->link();
		CHECK_GL_ERROR_DEBUG();

		program->updateUniforms();
		CHECK_GL_ERROR_DEBUG();

		GLint location = program->getUniformLocationForName("matrixEffect");
		program->use();
		program->setUniformLocationWithMatrix4fv(location, matrices_[tag].matrix, 1);
	}
	else
	{
		node->setGLProgram(GLProgramCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP));
	}
}
// функция применяет шейдер ко всем элементам анимации CocoStudio
void JCChangeColor::_applyShaderCCSBones(cocostudio::Armature *armature, int tag)
{
	const cocos2d::Map<std::string, cocostudio::Bone*> boneDic = armature->getBoneDic();
	for (auto& object : boneDic)
	{
		cocostudio::Bone *bone = object.second;
		if (bone)
		{
			const Vector<cocostudio::DecorativeDisplay*> list = bone->getDisplayManager()->getDecorativeDisplayList();
			for (auto& display : list)
			{
				Node* node = display->getDisplay();
				this->_applyShaderNode(node, tag);
			}
		}
	}
}

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

загрузка ресурсов
загрузка ресурсов

Это позволяет нам максимально распараллелить процесс разработки. Т.е. пока я пишу код, мой коллега может:

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

  • создавать или заменять любые игровые объекты;

  • строить или вносить изменения в уровни для любого режима;

  • менять настройки всей игры или отдельного объекта.

Мы работаем вместе около 8 лет, при этом количество личных встреч можно пересчитать по пальцам двух рук. Это же можно сказать и о разговорах по голосовой связи, они еще более редкие.

Я немного отвлекся от темы статьи, поэтому продолжу далее.

Для добавления нового объекта, мы создаем для него JSON файл с настройками. В этом файле содержится много различной информации описывающий игровой объект. Я остановлюсь на небольшой его части - анимации и настройках физического тела.

[{
"Armature":"Crocco",
"ArmatureType":1,
"ArmatureTransform":"TransformEffect0",
"Icon":"CroccoDB",
"PhysicsBody": { "Width":270, "Height":525, "ShiftX":0, "ShiftY":0 }
}]

Любой персонаж в нашей игре может использовать анимацию, созданную в CocoStudio или Dragonbones. По этой причине в настроечном файле, кроме названия проекта анимации "Armature":"Crocco", мы указываем еще тип используемой персонажем анимации "ArmatureType":1.

По умолчанию, если в настройках не задан тип анимации, наш код будет считать, что анимация персонажа создана в CocoStudio и будет работать с этим типом анимации.

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

if (!setting.armature.empty())
  this->_addPathAnimation(PathAnamitionsPersons + setting.armature, setting.armatureType);
// функция добавляет пути файлов анимации для последующей загрузки
void JCResourcesData::_addPathAnimation(const std::string &pathAnimations, int armatureType)
{
	pathAnimations_.push_back(std::pair<std::string, int>(pathAnimations, armatureType));
}

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

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

// функция загружает анимации из файлов
void JCResourcesData::_loadAnimations()
{
	std::sort(pathAnimations_.begin(), pathAnimations_.end());
	pathAnimations_.resize(unique(pathAnimations_.begin(), pathAnimations_.end()) - pathAnimations_.begin());
	
	itPaths_ = pathAnimations_.begin();
	
	if (JCGlobalSetting::getInstance()->isFastLoad())
		this->_updateloadAnimations(0);
	else
		Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR(JCResourcesData::_updateloadAnimations), this, 0, false);
}
// функция обновления загрузки файлов анимации
void JCResourcesData::_updateloadAnimations(float /*dt*/)
{
	if (listener_)
	{
		if (pathAnimations_.empty())
			listener_(Percent100);
		else
		{
			size_t count = pathAnimations_.size();
			size_t cur = itPaths_ - pathAnimations_.begin();
			listener_(cur * Percent100 / count);
		}
	}

	if (itPaths_ != pathAnimations_.end())
	{
		if ((*itPaths_).second == JCArmatureEditor::Type::COCOSTUDIO)
		{
			if (FileUtils::getInstance()->isFileExist((*itPaths_).first + PrefixCSArmature))
				ArmatureDataManager::getInstance()->addArmatureFileInfo((*itPaths_).first + PrefixCSArmature);
			else
				this->_loadDefaultAnimation((*itPaths_));
		}

		if ((*itPaths_).second == JCArmatureEditor::Type::DRAGONBONE)
		{
			bool isDefault = false;
			if (FileUtils::getInstance()->isFileExist((*itPaths_).first + PrefixDBBones))
				this->_loadDragonBonesAnimation((*itPaths_).first, PrefixDBBones);
			else
				isDefault = true;

			if (FileUtils::getInstance()->isFileExist((*itPaths_).first + PrefixDBAtlas))
				this->_loadDragonBonesAnimation((*itPaths_).first, PrefixDBAtlas);
			else 
				isDefault = true;

			if (isDefault)
				this->_loadDefaultAnimation((*itPaths_));
		}

		++itPaths_;

		if (JCGlobalSetting::getInstance()->isFastLoad())
			this->_updateloadAnimations(0);
	}
	else
		Director::getInstance()->getScheduler()->unschedule(CC_SCHEDULE_SELECTOR(JCResourcesData::_updateloadAnimations), this);
}

Если по какой-то причине анимация, указанная в файле настроек не была найдена, срабатывает защита. В этом случае мы загружаем анимацию для персонажа по умолчанию. Она представляет собой самый первый вариант анимации персонажа на момент создания прототипа игры.

// функция загружает анимацию по умолчанию
void JCResourcesData::_loadDefaultAnimation(std::pair<std::string, int> &pathFile)
{
	size_t pos = pathFile.first.find_last_of('/');
	std::string name = pathFile.first.substr(pos + 1, pathFile.first.back());
	std::string path = pathFile.first.substr(0, pos + 1);

	if (path.compare(PathAnamitionsPersons) == 0)
	{
		pathFile.second = 0;
		ArmatureDataManager::getInstance()->addArmatureFileInfo(DefaultPersonAnimation);
		this->_replacePersonArmatureName(name);
	}
}
// функция заменяет название анимации в файле настроек персонажа
void JCResourcesData::_replacePersonArmatureName(const std::string &name)
{
	for (auto it = personSettings_.begin(); it != personSettings_.end(); ++it)
	{
		if (it->armature.compare(name) == 0)
		{
			it->armature = DefaultPersonName;
			it->armatureType = 0;
		}
	}
}

Предварительная загрузка ресурсов анимации CocoStudio или Dragonbones также немного отличается друг от друга. При попытке повторной загрузки проекта анимации Dragonbones, движок выдает об этом предупреждение. Поэтому для загрузки ресурсов редактора анимации Dragonbones у нас используется дополнительная функция.

// загрузка ресурсов анимации Dragonbones
void JCResourcesData::_loadDragonBonesAnimation(const std::string &pathRes, const std::string &prefix)
{
	std::string path = pathRes + prefix;
	auto it = std::find(loadedDGBAnimationDefault_.begin(), loadedDGBAnimationDefault_.end(), path);
	if (it != loadedDGBAnimationDefault_.end())
		return;

	loadedDGBAnimationDefault_.push_back(path);
	const auto factory = dragonBones::CCFactory::getFactory();

	if (prefix.compare(PrefixDBBones) == 0) 
		factory->loadDragonBonesData(path);

	if (prefix.compare(PrefixDBAtlas) == 0)
		factory->loadTextureAtlasData(path);
}

Не смотря на то, что игровой проект выполнен в стиле 2.5D, в нем используется 2D физика Chipmunk.

использование физики Chipmunk
использование физики Chipmunk

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

// функция создает физическое тело
void JCPerson::_createBody()
{
	float width = personSetting_.physicsBodyInfo.width / 2;
	float height = personSetting_.physicsBodyInfo.height / 2;
	float shiftX = personSetting_.physicsBodyInfo.shiftX;
	float shiftY = personSetting_.physicsBodyInfo.shiftY;
	
	Vec2 verts[] = { Vec2(width, height), Vec2(width, -height * 0.2f), Vec2(width * 0.3f, -height), Vec2(-width * 0.3f, -height), Vec2(-width, -height * 0.2f), Vec2(-width, height) };
	int points = sizeof(verts) / sizeof(verts[0]);

	if (anyArmature_->getArmatureType() == JCArmatureEditor::Type::COCOSTUDIO)
	{
		physicsBody_ = PhysicsBody::createPolygon(verts, points, PhysicsMaterial(BodyDensity, BodyRestitution, BodyFriction)
			, Vec2(-armature_->getContentSize().width / 2 + shiftX, -armature_->getContentSize().height + height + shiftY));
	}
	else
	{
		physicsBody_ = PhysicsBody::createPolygon(verts, points, PhysicsMaterial(BodyDensity, BodyRestitution, BodyFriction)
			, Vec2(shiftX, shiftY));
	}
}

Теперь, как мне кажется, тема использования двух редакторов анимации в игровом проекте на cocos2d-x раскрыта полностью. Я думаю, что больше не осталось моментов, которые не были затронуты в этой или в предыдущей статье.

К сожалению, тема создания игр при помощи coco2d-x в России и странах СНГ мало освещается. Найти хороший русскоязычный материал по coco2d-x для разработчиков пишущих на языке с++ довольно трудно. Очень надеюсь, что данная статья окажется полезной коллегам.

В завершении, чтобы сделать статью немного веселее, прикладываю небольшое видео игрового процесса альфа версии.

А если кому-то интересно более подробно познакомиться с нашим игровым проектом, я оставлю ссылку на краудфандинговую площадку, где мы хотим собрать немного средств на его развитие.

Комментарии (0)