提示:spine大版本不向下兼容,如4.1版本打不开3.8的动画文件。
一、准备工作
- 新建cocos2dx-lua项目(我测试的项目版本是3.15和3.17)。
- spine版本选择下载 https://github.com/EsotericSoftware/spine-runtimes/releases。
二、升级spine底层代码
1. 删除原有spine代码

2. 导入新的spine代码
① spine代码里只需要这两个

② 拷贝上述两个文件里的代码文件全部拷贝到 frameworks\cocos2d-x\cocos\editor-support\spine 目录下面(v3是cocos 3.x版本,v4是4.0版本的)。


3. 回到Visual Studio工程重新导入spine代码到项目中


三、修改源文件代码
① lua_cocos2dx_spine_auto.cpp
#include "scripting/lua-bindings/auto/lua_cocos2dx_spine_auto.hpp"
#include "editor-support/spine/spine.h"
#include "editor-support/spine/spine-cocos2dx.h"
#include "scripting/lua-bindings/manual/tolua_fix.h"
#include "scripting/lua-bindings/manual/LuaBasicConversions.h"
#include "scripting/lua-bindings/manual/spine/LuaSkeletonAnimation.h"
#include "editor-support/spine/Animation.h"
#include "editor-support/spine/AnimationState.h"
#include "scripting/lua-bindings/manual/cocos2d/LuaScriptHandlerMgr.h"
#include "scripting/lua-bindings/manual/CCLuaValue.h"
int lua_cocos2dx_spine_SkeletonRenderer_setSkin(lua_State* tolua_S)函数里有两个地方是
...
bool ret = cobj->setSkin(arg0);
tolua_pushboolean(tolua_S,(bool)ret)
...
新版的spine里setSkin没有返回bool 而是void 所以这里两行代码写成
cobj->setSkin(arg0);
就可以了
② lua_cocos2dx_spine_manual.cpp
/****************************************************************************
Copyright (c) 2013-2016 Chukong Technologies Inc.
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
http://www.cocos2d-x.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
#include "scripting/lua-bindings/manual/spine/lua_cocos2dx_spine_manual.hpp"
#include "scripting/lua-bindings/auto/lua_cocos2dx_spine_auto.hpp"
#include "scripting/lua-bindings/manual/tolua_fix.h"
#include "scripting/lua-bindings/manual/LuaBasicConversions.h"
#include "scripting/lua-bindings/manual/cocos2d/LuaScriptHandlerMgr.h"
#include "scripting/lua-bindings/manual/CCLuaValue.h"
#include "editor-support/spine/spine.h"
#include "editor-support/spine/spine-cocos2dx.h"
#include "scripting/lua-bindings/manual/spine/LuaSkeletonAnimation.h"
#include "scripting/lua-bindings/manual/CCLuaEngine.h"
using namespace spine;
// setBlendFunc
template<class T>
static int tolua_cocos2dx_setBlendFunc(lua_State* tolua_S,const char* className)
{
if (NULL == tolua_S || NULL == className || strlen(className) == 0)
return 0;
int argc = 0;
T* self = nullptr;
#if COCOS2D_DEBUG >= 1
tolua_Error tolua_err;
if (!tolua_isusertype(tolua_S,1,className,0,&tolua_err)) goto tolua_lerror;
#endif
self = static_cast<T*>(tolua_tousertype(tolua_S,1,0));
argc = lua_gettop(tolua_S) - 1;
if (2 == argc)
{
CCLOG("setBlendFunc of %s will deprecate two int parameter form,please pass a table like {src = xx, dst = xx} as a parameter", className);
GLenum src, dst;
if (!luaval_to_int32(tolua_S, 2, (int32_t*)&src, StringUtils::format("%s%s",className,":setBlendFunc").c_str()))
return 0;
if (!luaval_to_int32(tolua_S, 3, (int32_t*)&dst,StringUtils::format("%s%s",className,":setBlendFunc").c_str()))
return 0;
BlendFunc blendFunc = {src, dst};
self->setBlendFunc(blendFunc);
return 0;
}
luaL_error(tolua_S, "'setBlendFunc' has wrong number of arguments: %d, was expecting %d\n", argc, 2);
return 0;
#if COCOS2D_DEBUG >= 1
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'setBlendFunc'.",&tolua_err);
return 0;
#endif
}
static int lua_cocos2dx_CCSkeletonAnimation_createWithFile(lua_State* L)
{
if (nullptr == L)
return 0 ;
int argc = 0;
#if COCOS2D_DEBUG >= 1
tolua_Error tolua_err;
if (!tolua_isusertable(L,1,"sp.SkeletonAnimation",0,&tolua_err)) goto tolua_lerror;
#endif
argc = lua_gettop(L) - 1;
if (2 == argc)
{
#if COCOS2D_DEBUG >= 1
if (!tolua_isstring(L, 2, 0, &tolua_err) ||
!tolua_isstring(L, 3 ,0, &tolua_err))
{
goto tolua_lerror;
}
#endif
const char* skeletonDataFile = tolua_tostring(L, 2, "");
const char* atlasFile = tolua_tostring(L, 3, "");
auto tolua_ret = LuaSkeletonAnimation::createWithFile(skeletonDataFile, atlasFile);
int nID = (tolua_ret) ? (int)tolua_ret->_ID : -1;
int* pLuaID = (tolua_ret) ? &tolua_ret->_luaID : NULL;
toluafix_pushusertype_ccobject(L, nID, pLuaID, (void*)tolua_ret,"sp.SkeletonAnimation");
return 1;
} else if (3 == argc)
{
#if COCOS2D_DEBUG >= 1
if (!tolua_isstring(L, 2, 0, &tolua_err) ||
!tolua_isstring(L, 3 ,0, &tolua_err) ||
!tolua_isnumber(L, 4 ,0, &tolua_err))
{
goto tolua_lerror;
}
#endif
const char* skeletonDataFile = tolua_tostring(L, 2, "");
const char* atlasFile = tolua_tostring(L, 3, "");
LUA_NUMBER scale = tolua_tonumber(L, 4, 1);
auto tolua_ret = LuaSkeletonAnimation::createWithFile(skeletonDataFile, atlasFile, scale);
int nID = (tolua_ret) ? (int)tolua_ret->_ID : -1;
int* pLuaID = (tolua_ret) ? &tolua_ret->_luaID : NULL;
toluafix_pushusertype_ccobject(L, nID, pLuaID, (void*)tolua_ret,"sp.SkeletonAnimation");
return 1;
}
luaL_error(L, "'createWithFile' function of SkeletonAnimation has wrong number of arguments: %d, was expecting %d\n", argc, 2);
#if COCOS2D_DEBUG >= 1
tolua_lerror:
tolua_error(L,"#ferror in function 'createWithFile'.",&tolua_err);
#endif
return 0;
}
int executeSpineEvent(LuaSkeletonAnimation* skeletonAnimation, int handler, EventType eventType, TrackEntry* entry, spEvent* event = nullptr )
{
if (nullptr == skeletonAnimation || 0 == handler)
return 0;
LuaStack* stack = LuaEngine::getInstance()->getLuaStack();
if (nullptr == stack)
return 0;
lua_State* L = LuaEngine::getInstance()->getLuaStack()->getLuaState();
if (nullptr == L)
return 0;
int ret = 0;
spine::String animationName = (entry && entry->getAnimation()) ? entry->getAnimation()->getName() : "";
std::string eventTypeName = "";
switch (eventType) {
case EventType::EventType_Start:
{
eventTypeName = "start";
}
break;
case EventType::EventType_Interrupt:
{
eventTypeName = "interrupt";
}
break;
case EventType::EventType_End:
{
eventTypeName = "end";
}
break;
case EventType::EventType_Dispose:
{
eventTypeName = "dispose";
}
break;
case EventType::EventType_Complete:
{
eventTypeName = "complete";
}
break;
case EventType::EventType_Event:
{
eventTypeName = "event";
}
break;
default:
break;
}
LuaValueDict spineEvent;
spineEvent.insert(spineEvent.end(), LuaValueDict::value_type("type", LuaValue::stringValue(eventTypeName)));
spineEvent.insert(spineEvent.end(), LuaValueDict::value_type("trackIndex", LuaValue::intValue(entry->getTrackIndex())));
spineEvent.insert(spineEvent.end(), LuaValueDict::value_type("animation", LuaValue::stringValue(animationName.buffer())));
spineEvent.insert(spineEvent.end(), LuaValueDict::value_type("loopCount", LuaValue::intValue(std::floor(entry->getTrackTime() / entry->getAnimationEnd()))));
if (nullptr != event)
{
LuaValueDict eventData;
eventData.insert(eventData.end(), LuaValueDict::value_type("name", LuaValue::stringValue(event->getData().getName().buffer())));
eventData.insert(eventData.end(), LuaValueDict::value_type("intValue", LuaValue::intValue(event->getIntValue())));
eventData.insert(eventData.end(), LuaValueDict::value_type("floatValue", LuaValue::floatValue(event->getFloatValue())));
eventData.insert(eventData.end(), LuaValueDict::value_type("stringValue", LuaValue::stringValue(event->getStringValue().buffer())));
spineEvent.insert(spineEvent.end(), LuaValueDict::value_type("eventData", LuaValue::dictValue(eventData)));
}
stack->pushLuaValueDict(spineEvent);
ret = stack->executeFunctionByHandler(handler, 1);
return ret;
}
int tolua_Cocos2d_CCSkeletonAnimation_registerSpineEventHandler00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
tolua_Error tolua_err;
if (
!tolua_isusertype(tolua_S,1,"sp.SkeletonAnimation",0,&tolua_err) ||
!toluafix_isfunction(tolua_S,2,"LUA_FUNCTION",0,&tolua_err) ||
!tolua_isnumber(tolua_S, 3, 0, &tolua_err) ||
!tolua_isnoobj(tolua_S,4,&tolua_err)
)
goto tolua_lerror;
else
#endif
{
LuaSkeletonAnimation* self = (LuaSkeletonAnimation*) tolua_tousertype(tolua_S,1,0);
if (NULL != self ) {
int handler = ( toluafix_ref_function(tolua_S,2,0));
EventType eventType = static_cast<EventType>((int)tolua_tonumber(tolua_S, 3, 0));
switch (eventType) {
case EventType::EventType_Start:
{
self->setStartListener([=](TrackEntry* entry){
executeSpineEvent(self, handler, eventType, entry);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_START);
}
break;
case EventType::EventType_Interrupt:
{
self->setInterruptListener([=](TrackEntry* entry){
executeSpineEvent(self, handler, eventType, entry);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_INTERRUPT);
}
break;
case EventType::EventType_End:
{
self->setEndListener([=](TrackEntry* entry){
executeSpineEvent(self, handler, eventType, entry);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_END);
}
break;
case EventType::EventType_Dispose:
{
self->setDisposeListener([=](TrackEntry* entry){
executeSpineEvent(self, handler, eventType, entry);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_DISPOSE);
}
break;
case EventType::EventType_Complete:
{
self->setCompleteListener([=](TrackEntry* entry){
executeSpineEvent(self, handler, eventType, entry);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_COMPLETE);
}
break;
case EventType::EventType_Event:
{
self->setEventListener([=](TrackEntry* entry, spEvent* event){
executeSpineEvent(self, handler, eventType, entry, event);
});
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_EVENT);
}
break;
default:
break;
}
}
}
return 0;
#ifndef TOLUA_RELEASE
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'registerSpineEventHandler'.",&tolua_err);
return 0;
#endif
}
int tolua_Cocos2d_CCSkeletonAnimation_unregisterSpineEventHandler00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
tolua_Error tolua_err;
if (
!tolua_isusertype(tolua_S,1,"sp.SkeletonAnimation",0,&tolua_err) ||
!tolua_isnumber(tolua_S, 2, 0, &tolua_err) ||
!tolua_isnoobj(tolua_S,3,&tolua_err)
)
goto tolua_lerror;
else
#endif
{
LuaSkeletonAnimation* self = (LuaSkeletonAnimation*) tolua_tousertype(tolua_S,1,0);
if (NULL != self ) {
EventType eventType = static_cast<EventType>((int)tolua_tonumber(tolua_S, 2, 0));
ScriptHandlerMgr::HandlerType handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_START;
switch (eventType) {
case EventType::EventType_Start:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_START;
self->setStartListener(nullptr);
break;
case EventType::EventType_Interrupt:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_INTERRUPT;
break;
case EventType::EventType_End:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_END;
self->setEndListener(nullptr);
break;
case EventType::EventType_Dispose:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_DISPOSE;
break;
case EventType::EventType_Complete:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_COMPLETE;
self->setCompleteListener(nullptr);
break;
case EventType::EventType_Event:
handlerType = ScriptHandlerMgr::HandlerType::EVENT_SPINE_ANIMATION_EVENT;
self->setEventListener(nullptr);
break;
default:
break;
}
ScriptHandlerMgr::getInstance()->removeObjectHandler((void*)self, handlerType);
}
}
return 0;
#ifndef TOLUA_RELEASE
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'unregisterScriptHandler'.",&tolua_err);
return 0;
#endif
}
extern int lua_cocos2dx_spine_SkeletonRenderer_setBlendFunc(lua_State* tolua_S);
CC_DEPRECATED_ATTRIBUTE static int tolua_spine_SkeletoneAnimation_setBlendFunc(lua_State* tolua_S)
{
int argc = lua_gettop(tolua_S) - 1;
if (argc == 2)
{
return tolua_cocos2dx_setBlendFunc<spine::SkeletonAnimation>(tolua_S,"sp.SkeletonAnimation");
}
return lua_cocos2dx_spine_SkeletonRenderer_setBlendFunc(tolua_S);
}
static int lua_cocos2dx_spine_SkeletonAnimation_addAnimation(lua_State* tolua_S)
{
int argc = 0;
spine::SkeletonAnimation* cobj = nullptr;
bool ok = true;
#if COCOS2D_DEBUG >= 1
tolua_Error tolua_err;
#endif
#if COCOS2D_DEBUG >= 1
if (!tolua_isusertype(tolua_S,1,"sp.SkeletonAnimation",0,&tolua_err)) goto tolua_lerror;
#endif
cobj = (spine::SkeletonAnimation*)tolua_tousertype(tolua_S,1,0);
#if COCOS2D_DEBUG >= 1
if (!cobj)
{
tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_spine_SkeletonAnimation_addAnimation'", nullptr);
return 0;
}
#endif
argc = lua_gettop(tolua_S)-1;
if (argc == 3)
{
int arg0;
const char* arg1;
bool arg2;
ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "sp.SkeletonAnimation:addAnimation");
std::string arg1_tmp; ok &= luaval_to_std_string(tolua_S, 3, &arg1_tmp, "sp.SkeletonAnimation:addAnimation"); arg1 = arg1_tmp.c_str();
ok &= luaval_to_boolean(tolua_S, 4,&arg2, "sp.SkeletonAnimation:addAnimation");
if(!ok)
return 0;
cobj->addAnimation(arg0, arg1, arg2);
lua_settop(tolua_S, 1);
return 1;
}
if (argc == 4)
{
int arg0;
const char* arg1;
bool arg2;
double arg3;
ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "sp.SkeletonAnimation:addAnimation");
std::string arg1_tmp; ok &= luaval_to_std_string(tolua_S, 3, &arg1_tmp, "sp.SkeletonAnimation:addAnimation"); arg1 = arg1_tmp.c_str();
ok &= luaval_to_boolean(tolua_S, 4,&arg2, "sp.SkeletonAnimation:addAnimation");
ok &= luaval_to_number(tolua_S, 5,&arg3, "sp.SkeletonAnimation:addAnimation");
if(!ok)
return 0;
cobj->addAnimation(arg0, arg1, arg2, arg3);
lua_settop(tolua_S, 1);
return 1;
}
luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "addAnimation",argc, 3);
return 0;
#if COCOS2D_DEBUG >= 1
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_spine_SkeletonAnimation_addAnimation'.",&tolua_err);
#endif
return 0;
}
static int lua_cocos2dx_spine_SkeletonAnimation_setAnimation(lua_State* tolua_S)
{
int argc = 0;
spine::SkeletonAnimation* cobj = nullptr;
bool ok = true;
#if COCOS2D_DEBUG >= 1
tolua_Error tolua_err;
#endif
#if COCOS2D_DEBUG >= 1
if (!tolua_isusertype(tolua_S,1,"sp.SkeletonAnimation",0,&tolua_err)) goto tolua_lerror;
#endif
cobj = (spine::SkeletonAnimation*)tolua_tousertype(tolua_S,1,0);
#if COCOS2D_DEBUG >= 1
if (!cobj)
{
tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_spine_SkeletonAnimation_setAnimation'", nullptr);
return 0;
}
#endif
argc = lua_gettop(tolua_S)-1;
if (argc == 3)
{
int arg0;
const char* arg1;
bool arg2;
ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "sp.SkeletonAnimation:setAnimation");
std::string arg1_tmp; ok &= luaval_to_std_string(tolua_S, 3, &arg1_tmp, "sp.SkeletonAnimation:setAnimation"); arg1 = arg1_tmp.c_str();
ok &= luaval_to_boolean(tolua_S, 4,&arg2, "sp.SkeletonAnimation:setAnimation");
if(!ok)
return 0;
cobj->setAnimation(arg0, arg1, arg2);
lua_settop(tolua_S, 1);
return 1;
}
luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "setAnimation",argc, 3);
return 0;
#if COCOS2D_DEBUG >= 1
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_spine_SkeletonAnimation_setAnimation'.",&tolua_err);
#endif
return 0;
}
static void extendCCSkeletonAnimation(lua_State* L)
{
lua_pushstring(L, "sp.SkeletonAnimation");
lua_rawget(L, LUA_REGISTRYINDEX);
if (lua_istable(L,-1))
{
tolua_function(L, "create", lua_cocos2dx_CCSkeletonAnimation_createWithFile);
tolua_function(L, "registerSpineEventHandler", tolua_Cocos2d_CCSkeletonAnimation_registerSpineEventHandler00);
tolua_function(L, "unregisterSpineEventHandler", tolua_Cocos2d_CCSkeletonAnimation_unregisterSpineEventHandler00);
tolua_function(L, "setBlendFunc", tolua_spine_SkeletoneAnimation_setBlendFunc);
tolua_function(L, "addAnimation", lua_cocos2dx_spine_SkeletonAnimation_addAnimation);
tolua_function(L, "setAnimation", lua_cocos2dx_spine_SkeletonAnimation_setAnimation);
}
lua_pop(L, 1);
/*Because sp.SkeletonAnimation:create creat a LuaSkeletonAnimation object,so we need use LuaSkeletonAnimation typename for g_luaType*/
std::string typeName = typeid(LuaSkeletonAnimation).name();
g_luaType[typeName] = "sp.SkeletonAnimation";
g_typeCast["SkeletonAnimation"] = "sp.SkeletonAnimation";
}
int register_all_cocos2dx_spine_manual(lua_State* L)
{
if (nullptr == L)
return 0;
extendCCSkeletonAnimation(L);
return 0;
}
int register_spine_module(lua_State* L)
{
lua_getglobal(L, "_G");
if (lua_istable(L,-1))//stack:...,_G,
{
register_all_cocos2dx_spine(L);
register_all_cocos2dx_spine_manual(L);
}
lua_pop(L, 1);
return 1;
}
③ LuaSkeletonAnimation.cpp
LuaSkeletonAnimation* LuaSkeletonAnimation::createWithFile (const char* skeletonDataFile, const char* atlasFile, float scale)
{
LuaSkeletonAnimation* node = new (std::nothrow) LuaSkeletonAnimation();
//spAtlas* atlas = spAtlas_createFromFile(atlasFile, nullptr);//删除这句代码用下面的代码替换,新版的没有spAtlas_createFromFile这个方法,而是采用了构造函数来创建
spAtlas* atlas = new spAtlas(atlasFile, nullptr);
node->initWithJsonFile(skeletonDataFile, atlas, scale);
node->autorelease();
return node;
}
四、适配新旧类型.
- 在sine/Animation.h文件后面加上:typedef spine::Animation spAnimation;
- 在sine/Atlas.h文件后面加上:typedef spine::Atlas spAtlas;
- 在sine/Event.h文件后面加上:typedef spine::Event spEvent;
- 在sine/Skeleton.h文件后面加上:typedef spine::Skeleton spEvent;
- 在sine/SkeletonData.h文件后面加上:typedef spine::SkeletonData spEvent;
- 在sine/VertexEffect.h文件后面加上:typedef spine::VertexEffect spEvent;(spine4.1版本没有这个文件)

- lua_cocos2dx_spine_auto.cpp和lua_cocos2dx_spine_manual.cpp 中把 “spTrackEntry”替换成“spine::TrackEntry” 、“spEventType”替换成“EventType”
五、编译测试
对于cocos2dx-lua 3.15和3.16与spine3.8和4.1版本均可运行。
本文档详细介绍了如何将cocos2dx-lua项目中的spine从旧版本升级到新版本,包括删除旧代码、导入新代码、修改源文件、适配类型以及编译测试的步骤。注意spine大版本不向下兼容,升级时需谨慎操作。
提示:spine大版本不向下兼容,如4.1版本打不开3.8的动画文件。

3703

被折叠的 条评论
为什么被折叠?



