Insert a Spine Generic Runtime into a C ++ project

Hello!

Recently, we faced the task of adding skeletal animation to the project. On the advice of colleagues, we turned our attention to Spine .
After it became clear that the capabilities of the editor satisfy our needs ( there is a review of the animation editor here ), we began to embed Spine in our C ++ engine.

Source code


The "general" source codes for C in Rtime are here . Compilation sources give 3 errors - 3 functions must be implemented during integration. They will be described below.

Data format


The exported data ( here you can download the graphical editor and several exported animations for etst) consist of an animation json-file, texture (atlas) and atlas description file.

Integration Atlas


Let's start by loading the texture atlas. To do this, we will write a small wrapper class that will load and unload texture atlases in Spine format.

// объявление класса  
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);
}

The spAtlas_create function from the SpineAtlas constructor calls the _spAtlasPage_createTexture function , which must be overridden when Spine is integrated into the engine. Here we also define the _spAtlasPage_disposeTexture function paired with it .

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);
}

The function textures :: LoadTexture loads the texture from the file at the specified path. render :: ReleaseTexture - platform- dependent unloading of texture from memory.

Integration, animation


The simplest wrapper for Spine animation is as follows.

// объявление класса
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);
	}
}
// отрисовка, render::BindTexture и render::DrawVertexArray - платформозависимая отрисовка
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;
		render::BindTexture(texture);
		render::DrawVertexArray(vertices, 6);
	}
}
// заполнение одной вершины в формате 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();
}

The spSkeletonJson_readSkeletonDataFile function from the SpineAnimaion constructor calls the _spUtil_readFile function . This is the last of the three functions that must be implemented in the code for integrating Spine. She uses malloc in Spine style.
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;
}

Additional features


When loading an animation file, you can specify a global scale ( SpineAnimation :: SpineAnimation ).
spSkeletonJson* skeletonJson = spSkeletonJson_create(mAtlas->GetAtlas());
skeletonJson->scale = scale;

Skinning is implemented as follows ( SpineAnimation :: Play ):
if (!skinName.empty())
{
	spSkeleton_setSkinByName(mSkeleton, skinName.c_str()); 
	spSkeleton_setSlotsToSetupPose(mSkeleton);
}

When playing, you can set the speed of the animation, as well as mirror it horizontally and / or vertically ( SpineAnimaiton :: Update ):
if (IsPlaying())
{
	mSkeleton->flipX = mFlipX;
	mSkeleton->flipY = mFlipY;
	spSkeleton_update(mSkeleton, timeElapsed * mSpeed / 1000);		
	...
}

The selected animation can be launched along the desired path. The animation position is taken into account when filling in vertexes during rendering ( SpineAnimaiton :: Render )
FillSlotVertices(vertices], mPosition.x, mPosition.y, slot, regionAttachment);


Source code


The sources described in this article can be downloaded here . For ease of reading, Set / Get functions are missing.

PS: When writing this article, I found 2 small errors in the code. Often write articles on Habr!

Also popular now: