Вставляем Spine Generic Runtime в проект на С++

в 7:14, , рубрики: c++, game development, Spine, анимация

Всем привет!

Недавно перед нами встала задача добавления в проект скелетной анимации. По совету коллег мы обратили внимание на Spine.
После того, как стало понятно, что возможности редактора удовлетворяют нашим нуждам (здесь есть обзор редактора анимаций), мы стали вставлять Spine в наш С++ движок.

Исходники

«Общие» исходники редактора на С лежат здесь. Исходники при компиляции выдают 3 ошибки – при интеграции надо реализовать 3 функции. Они будут описаны ниже.

Формат данных

Сэкспортированые данные (здесь можно скачать графический редактор и несколько сэкспортированных анмиаций для етста) состоят из json-файла анимации, текстуры (атласа) и файла описания атласа.

Интеграция, атлас

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

// объявление класса  
class SpineAtlas
{
public:
	SpineAtlas(const std::string& name);
	~SpineAtlas();

private:
	std::string		mName;
	spAtlas*		mAtlas;
};
// загрузка атласа, store::Load и store::Free – функции движка, загружающие файл в память и освобождающие память соответственно.
SpineAtlas::SpineAtlas(const std::string& name) : 
	mName(name), mAtlas(0)
{
	int length = 0;
	const char* data = (const char*)store::Load(name + ".atlas", length); 
	if (data)
	{
		mAtlas = spAtlas_create(data, length, "", 0);
		store::Free(name + ".atlas");
	}
}
// выгрузка атласа
SpineAtlas::~SpineAtlas()
{
    spAtlas_dispose(mAtlas);
}

Функция spAtlas_create из конструктора SpineAtlas вызывает функцию _spAtlasPage_createTexture, которая должна быть переопределена при интеграции Spine в движок. Здесь же определим и парную ей функцию _spAtlasPage_disposeTexture.

extern "C" void _spAtlasPage_createTexture(spAtlasPage* self, const char* path)
{
	Texture* texture = textures::LoadTexture(path);
	self->width = texture->width;
	self->height = texture->height;
	self->rendererObject = texture;
}

extern "C" void _spAtlasPage_disposeTexture(spAtlasPage* self)
{
	Texture* texture = (Texture*)self->rendererObject;
	render::ReleaseTexture(texture);
}

Функция textures::LoadTexture загружает текстуру из файла по указанному пути. render::ReleaseTexture – платформозависимая выгрузка текстуры из памяти.

Интеграция, анимация

Простейший wrapper для Spine анимации выглядит следующим образом.

// объявление класса
class SpineAnimation
{
public:
	SpineAnimation(const std::string& name);
	~SpineAnimation();

	void			Update(float timeElapsed);
	void			Render();

	void			Play(const std::string& skin, const std::string& animation, bool looped);
	void			Stop();

	void			OnAnimationEvent(SpineAnimationState* state, int trackIndex, int type, spEvent* event, int loopCount);

private:
	spAnimation*		GetAnimation(const std::string& name) const;
	void			FillSlotVertices(Vertex* points, float x, float y, spSlot* slot, spRegionAttachment* attachment);

	std::string		mName;
	std::string		mCurrentAnimation;
	SpineAtlas*		mAtlas;
	spAnimationState*	mState;
	spAnimationStateData* mStateData;
	spSkeleton*		mSkeleton;
	bool			mPlaying;
}; 
// загрузка анимации 
SpineAnimation::SpineAnimation(const std::string& name) : 
	mName(name), mAtlas(0), mState(0), mStateData(0), mSkeleton(0), mSpeed(1), mPlaying(false), mFlipX(false)
{
	mAtlas = gAnimationHost.GetAtlas(mName);

	spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
	spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(skeletonJson, (name + ".json").c_str());
	assert(skeletonData);
	spSkeletonJson_dispose(skeletonJson);

	mSkeleton = spSkeleton_create(skeletonData);
	mStateData = spAnimationStateData_create(skeletonData);
	mState = spAnimationState_create(mStateData);
	mState->rendererObject = this;
	spSkeleton_update(mSkeleton, 0);
	spAnimationState_update(mState, 0);
	spAnimationState_apply(mState, mSkeleton);
	spSkeleton_updateWorldTransform(mSkeleton);
}
// выгрузка анимации
SpineAnimation::~SpineAnimation()
{
	spAnimationState_dispose(mState);
	spAnimationStateData_dispose(mStateData);
	spSkeleton_dispose(mSkeleton);
}
// update анимации
void SpineAnimation::Update(float timeElapsed)
{
	if (IsPlaying())
	{
		spSkeleton_update(mSkeleton, timeElapsed / 1000); // timeElapsed - ms, Spine использует время в секундах
		spAnimationState_update(mState, timeElapsed / 1000);
		spAnimationState_apply(mState, mSkeleton);
		spSkeleton_updateWorldTransform(mSkeleton);
	}
}
// отрисовка
void SpineAnimation::Render()
{
	int slotCount = mSkeleton->slotCount; 
	Vertex vertices[6];
	for (int i = 0; i < slotCount; ++i) 
	{
		spSlot* slot = mSkeleton->slots[i];
		spAttachment* attachment = slot->attachment;
		if (!attachment || attachment->type != SP_ATTACHMENT_REGION) 
			continue;
		spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment;
		FillSlotVertices(vertices], 0, 0, slot, regionAttachment);
		texture = (Texture*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject;
	}
}
// заполнение одной вершины в формате triangle list 
// формат структуры Vertex: xyz – координаты, uv – текстурные координаты, с - цвет 
void SpineAnimation::FillSlotVertices(Vertex* points, float x, float y, spSlot* slot, spRegionAttachment* attachment)
{
	Color color(mSkeleton->r * slot->r, mSkeleton->g * slot->g, mSkeleton->b * slot->b, mSkeleton->a * slot->a);
	points[0].c = points[1].c = points[2].c = points[3].c = points[4].c = points[5].c = color;
	points[0].uv.x = points[5].uv.x = attachment->uvs[SP_VERTEX_X1];
	points[0].uv.y = points[5].uv.y = attachment->uvs[SP_VERTEX_Y1];
	points[1].uv.x = attachment->uvs[SP_VERTEX_X2];
	points[1].uv.y = attachment->uvs[SP_VERTEX_Y2];
	points[2].uv.x = points[3].uv.x = attachment->uvs[SP_VERTEX_X3];
	points[2].uv.y = points[3].uv.y = attachment->uvs[SP_VERTEX_Y3];
	points[4].uv.x = attachment->uvs[SP_VERTEX_X4];
	points[4].uv.y = attachment->uvs[SP_VERTEX_Y4];
	float* offset = attachment->offset;
	float xx = slot->skeleton->x + slot->bone->worldX;
	float yy = slot->skeleton->y + slot->bone->worldY;
	points[0].xyz.x = points[5].xyz.x = x + xx + offset[SP_VERTEX_X1] * slot->bone->m00 + offset[SP_VERTEX_Y1] * slot->bone->m01;
	points[0].xyz.y = points[5].xyz.y = y - yy - (offset[SP_VERTEX_X1] * slot->bone->m10 + offset[SP_VERTEX_Y1] * slot->bone->m11);
	points[1].xyz.x = x + xx + offset[SP_VERTEX_X2] * slot->bone->m00 + offset[SP_VERTEX_Y2] * slot->bone->m01;
	points[1].xyz.y = y - yy - (offset[SP_VERTEX_X2] * slot->bone->m10 + offset[SP_VERTEX_Y2] * slot->bone->m11);
	points[2].xyz.x = points[3].xyz.x = x + xx + offset[SP_VERTEX_X3] * slot->bone->m00 + offset[SP_VERTEX_Y3] * slot->bone->m01;
	points[2].xyz.y = points[3].xyz.y = y - yy - (offset[SP_VERTEX_X3] * slot->bone->m10 + offset[SP_VERTEX_Y3] * slot->bone->m11);
	points[4].xyz.x = x + xx + offset[SP_VERTEX_X4] * slot->bone->m00 + offset[SP_VERTEX_Y4] * slot->bone->m01;
	points[4].xyz.y = y - yy - (offset[SP_VERTEX_X4] * slot->bone->m10 + offset[SP_VERTEX_Y4] * slot->bone->m11);
}
// Глобальный listener для обработки событий анимации
void SpineAnimationStateListener(spAnimationState* state, int trackIndex, spEventType type, spEvent* event, int loopCount)
{
	SpineAnimation* sa = (SpineAnimation*)state->rendererObject;
	if (sa)
		sa->OnAnimationEvent((SpineAnimationState*)state, trackIndex, type, event, loopCount);
}
// проигрывание анимации
void SpineAnimation::Play(const std::string& animationName, bool looped)
{
	if (mCurrentAnimation == animationName) // не запускаем анмиацию повторно
		return;

	spAnimation* animation = GetAnimation(animationName);  
	if (animation)
	{
		mCurrentAnimation = animationName;	   

		spTrackEntry* entry = spAnimationState_setAnimation(mState, 0, animation, looped);
		if (entry)
			entry->listener = SpineAnimationStateListener;
		mPlaying = true;
	}
	else
		Stop();
}
// остановка анимации
void SpineAnimation::Stop()
{
	mCurrentAnimation.clear();   
	mPlaying = false;
}
// получение анимации по имени
spAnimation* SpineAnimation::GetAnimation(const std::string& name) const
{
	return spSkeletonData_findAnimation(mSkeleton->data, name.c_str());
}
// остановка анимации по завершению
void SpineAnimation::OnAnimationEvent(SpineAnimationState* state, int trackIndex, int type, spEvent* event, int loopCount)
{
	spTrackEntry* entry = spAnimationState_getCurrent(state, trackIndex);
	if (entry && !entry->loop && type == SP_ANIMATION_COMPLETE)
		Stop();
}

Функция spSkeletonJson_readSkeletonDataFile из конструктора SpineAnimaion вызывает функцию _spUtil_readFile. Это последняя из трех функций, которые должны быть реализованы в коде, для интеграции Spine. Она использует malloc в стиле Spine.

extern "C" char* _spUtil_readFile(const char* path, int* length)
{
	char* result = 0;
	const void* buffer = store::Load(path, *length);
	if (buffer)
	{
		result = (char*)_malloc(*length, __FILE__, __LINE__); // Spine malloc
		memcpy(result, buffer, *length);
		store::Free(path);
	}
	return result;
}

Дополнительные фичи

При загрузке файла анимации можно указать глобальный scale (SpineAnimation::SpineAnimation).

spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
skeletonJson->scale = scale;

Skinning реализуется следующим образом (SpineAnimation::Play):

if (!skinName.empty())
{
	spSkeleton_setSkinByName(mSkeleton, skinName.c_str()); 
	spSkeleton_setSlotsToSetupPose(mSkeleton);
}

При проигрывании можно задавать скорость анимации, а также зеркалить ее по горизонтали и/или вертикали (SpineAnimaiton::Update):

if (IsPlaying())
{
	mSkeleton->flipX = mFlipX;
	mSkeleton->flipY = mFlipY;
	spSkeleton_update(mSkeleton, timeElapsed * mSpeed / 1000);		
	...
}

Выбранную анимацию можно запустить по желаемой траектории. Позиция анимации учитывается при заполнении вертекстов при отрисовке (SpineAnimaiton::Render)

FillSlotVertices(vertices], mPosition.x, mPosition.y, slot, regionAttachment);

Исходники

Исходники, описанные в этой статье, можно скачать здесь. Для простоты чтения Set/Get функции в них отсутствуют.

PS: При написании этой статьи я нашел 2 небольших ошибки в коде. Почаще пишите статьи на Хабр!

Автор: DenKon

Источник

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


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