Вставляем Spine Generic Runtime в проект на С++
Всем привет! Недавно перед нами встала задача добавления в проект скелетной анимации. По совету коллег мы обратили внимание на 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 небольших ошибки в коде. Почаще пишите статьи на Хабр!