Cocos2d-x之LUA脚本引擎深入分析

发表于2015-09-02
评论0 1k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594
 首 先,我们要知道LUA是个什么东西,至于官方怎么说可以百度去查,但我想告诉你的是LUA就是一种可以在不必修改C++代码的情况下实现逻辑处理的手段。 稍微讲的再明白一点,就是你用指定语法写一些逻辑处理函数然后保存成文本格式,这个文件称为脚本文件,可以被游戏执行。经过若干年的发展,现在在LUA中 写逻辑,除了调用注册到LUA的静态C函数外,也已经可以方便的访问到C++工程中的类的成员函数。这是游戏开发史上最重要的技术之一。其改变了很多设计 方案,使游戏变的灵活强大而极具扩展性。
 
         在Cocos2d-x中,有两个类来完成对于LUA脚本文件的处理。
 
1. CCLuaEngine:LUA脚本引擎
 
2. CCScriptEngineManager:脚本引擎管理器。
 
 
CCLuaEngine类的基类是一个接口类,叫做CCScriptEngineProtocol,它规定了所有LUA引擎的功能函数,它和 CCScriptEngineManager都存放在libcocos2d下的script_support目录中的 CCScriptSupport.h/cpp中。
 
首先我们来看一下CCScriptEngineProtocol:
 
[cpp]  
class CC_DLL CCScriptEngineProtocol : public CCObject  
{  
public:  
    //取得LUA的全局指针,所有的LUA函数都需要使用这个指针来做为参数进行调用。  
    virtual lua_State* getLuaState(void) = 0;  
      
    //通过LUA脚本ID移除对应的CCObject  
    virtual void removeCCObjectByID(int nLuaID) = 0;  
      
    //通过函数索引值移除对应的LUA函数。  
    virtual void removeLuaHandler(int nHandler) = 0;  
      
    //将一个目录中的LUA文件加入到LUA资源容器中。  
    virtual void addSearchPath(const char* path) = 0;  
      
    //执行一段LUA代码  
    virtual int executeString(const char* codes) = 0;  
      
    //执行一个LUA脚本文件。  
    virtual int executeScriptFile(const char* filename) = 0;  
      
    //调用一个全局函数。  
    virtual int executeGlobalFunction(const char* functionName) = 0;  
      
//通过句柄调用函数多种形态。  
//通过句柄调用函数,参数二为参数数量。  
virtual int executeFunctionByHandler(int nHandler, int numArgs = 0) = 0;  
//通过句柄调用函数,参数二为整数数据。  
virtual int executeFunctionWithIntegerData(int nHandler, int data) = 0;  
//通过句柄调用函数,参数二为浮点数据。  
virtual int executeFunctionWithFloatData(int nHandler, float data) = 0;  
//通过句柄调用函数,参数二为布尔型数据。  
virtual int executeFunctionWithBooleanData(int nHandler, bool data) = 0;  
//通过句柄调用函数,参数二为CCObject指针数据和其类型名称。  
virtual int executeFunctionWithCCObject(int nHandler, CCObject* pObject, const char* typeName) = 0;      
  
//将一个整数数值压栈做为参数。  
virtual int pushIntegerToLuaStack(int data) = 0;  
//将一个浮点数值压栈做为参数。  
virtual int pushFloatToLuaStack(int data) = 0;  
//将一个布尔数值压栈做为参数。  
virtual int pushBooleanToLuaStack(int data) = 0;  
//将一个CCObject指针和类型名压栈做为参数。  
    virtual int pushCCObjectToLuaStack(CCObject* pObject, const char* typeName) = 0;  
      
    // 执行单点触屏事件  
virtual int executeTouchEvent(int nHandler, int eventType, CCTouch *pTouch) = 0;  
//执行多点触屏事件。  
    virtual int executeTouchesEvent(int nHandler, int eventType, CCSet *pTouches) = 0;  
    // 执行一个回调函数。  
    virtual int executeSchedule(int nHandler, float dt) = 0;  
};  
 
这个接口类的功能函数的具体实现,我们要参看CCLuaEngine类。
现在我们打开CCLuaEngine.h:
[cpp]  
//加入lua的头文件,约定其中代码使用C风格  
extern "C" {  
#include "lua.h"  
}  
  
//相应的头文件。  
#include "ccTypes.h"  
#include "cocoa/CCObject.h"  
#include "touch_dispatcher/CCTouch.h"  
#include "cocoa/CCSet.h"  
#include "base_nodes/CCNode.h"  
#include "script_support/CCScriptSupport.h"  
  
//使用Cocos2d命名空间  
NS_CC_BEGIN  
  
// 由CCScriptEngineProtocol派生的实际功能类。  
  
class CCLuaEngine : public CCScriptEngineProtocol  
{  
public:  
    //析构  
    ~CCLuaEngine();  
      
    //取得LUA的全局指针,所有的LUA函数都需要使用这个指针来做为参数进行调用。  
    virtual lua_State* getLuaState(void) {  
        return m_state;  
    }  
      
    …此处省略若干字。  
      
    // 加入一个多线程加载LUA脚本的实时回调函数,此函数用于ANDROID  
    virtual void addLuaLoader(lua_CFunction func);  
    //取得当前单件实例指针  
    static CCLuaEngine* engine();  
      
private:  
    //构造,单例,你懂的。  
    CCLuaEngine(void)  
    : m_state(NULL)  
    {  
    }  
    //初始化函数。  
bool init(void);  
//将一个句柄压栈  
    bool pushFunctionByHandler(int nHandler);  
    //唯一的LUA指针  
    lua_State* m_state;  
};  
  
NS_CC_END  
 
分析其CPP实现:
[cpp] 
//本类的头文件。  
#include "CCLuaEngine.h"  
//这里用到了tolua++库,tolua++库是一个专门处理LUA脚本的第三方库,可以很好的完成LUA访问C++类及成员函数的功能。如果没有tolua++,这块要处理起来可是麻烦死了。  
#include "tolua++.h"  
  
//加入lua库的相应头文件。  
extern "C" {  
#include "lualib.h"  
#include "lauxlib.h"  
#include "tolua_fix.h"  
}  
  
//加入Cocos2d-x所用的相应头文件。  
#include "cocos2d.h"  
#include "LuaCocos2d.h"  
#include "cocoa/CCArray.h"  
#include "CCScheduler.h"  
  
//如果是ANDROID平台,加上对多线程加载LUA脚本的支持,使用相应的头文件。  
  
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)  
#include "Cocos2dxLuaLoader.h"  
#endif  
  
//开始Cocos2d-x命名空间。  
NS_CC_BEGIN  
  
//析构。  
CCLuaEngine::~CCLuaEngine()  
{  
    //结束对LUA指针的使用,关闭LUA。  
    lua_close(m_state);  
}  
  
//初始始。  
bool CCLuaEngine::init(void)  
{  
    //开始对LUA的使用,创建一个LUA指针。  
m_state = lua_open();  
//打开相应的库。  
luaL_openlibs(m_state);  
//打开使用tolua封装的访问Cocos2d-x的库。  
    tolua_Cocos2d_open(m_state);  
tolua_prepare_ccobject_table(m_state);  
//如果是ANDROID平台,也加上对LUA进行多线程加载的库支持。  
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)  
    addLuaLoader(loader_Android);  
#endif  
    return true;  
}  
  
//取得单例指针。  
CCLuaEngine* CCLuaEngine::engine()  
{  
    CCLuaEngine* pEngine = new CCLuaEngine();  
    pEngine->init();  
    pEngine->autorelease();  
    return pEngine;  
}  
  
//通过LUA脚本ID移除对应的CCObject   
void CCLuaEngine::removeCCObjectByID(int nLuaID)  
{  
    tolua_remove_ccobject_by_refid(m_state, nLuaID);  
}  
//<span style="font-family: Arial, Helvetica, sans-serif;">通过函数索引值移除对应的LUA函数。</span>  
void CCLuaEngine::removeLuaHandler(int nHandler)  
{  
    tolua_remove_function_by_refid(m_state, nHandler);  
}  
//将一个目录中的LUA文件加入到LUA资源容器中。  
void CCLuaEngine::addSearchPath(const char* path)  
{  
    //取得全局表package  
lua_getglobal(m_state, "package");                                
//取得其中的path字段,压入栈顶。  
lua_getfield(m_state, -1, "path");              
//取得当前的目录字符串。  
const char* cur_path =  lua_tostring(m_state, -1);  
//参数出栈,恢复堆栈。  
lua_pop(m_state, 1);                                              
//将新路径字符串加入到路径串列中,压入栈顶。  
lua_pushfstring(m_state, "%s;%s/?.lua", cur_path, path);  
//设置path字段值路径  
lua_setfield(m_state, -2, "path");        
//参数出栈,恢复堆栈。  
 lua_pop(m_state, 1);                                              
}  
//执行一段LUA代码  
int CCLuaEngine::executeString(const char *codes)  
{  
    //执行一段LUA代码。返回值存放到nRet中。  
int nRet =    luaL_dostring(m_state, codes);  
//进行下拉圾收集。  
    lua_gc(m_state, LUA_GCCOLLECT, 0);  
    //如果出错,打印日志。  
    if (nRet != 0)  
    {  
        CCLOG("[LUA ERROR] %s", lua_tostring(m_state, -1));  
        lua_pop(m_state, 1);  
        return nRet;  
    }  
    return 0;  
}  
//执行一个LUA脚本文件。  
  
int CCLuaEngine::executeScriptFile(const char* filename)  
{  
    //执行一个LUA脚本文件。返回值存放到nRet中。  
    int nRet = luaL_dofile(m_state, filename);  
//    lua_gc(m_state, LUA_GCCOLLECT, 0);  
    //如果出错,打印日志。  
    if (nRet != 0)  
    {  
        CCLOG("[LUA ERROR] %s", lua_tostring(m_state, -1));  
        lua_pop(m_state, 1);  
        return nRet;  
    }  
    return 0;  
}  
//调用一个全局函数。  
int    CCLuaEngine::executeGlobalFunction(const char* functionName)  
{  
    //将全局函数放在栈顶  
lua_getglobal(m_state, functionName);  /* query function by name, stack: function */  
//判断是否是函数。  
    if (!lua_isfunction(m_state, -1))  
    {  
        CCLOG("[LUA ERROR] name '%s' does not represent a Lua function", functionName);  
        lua_pop(m_state, 1);  
        return 0;  
    }  
    //调用函数。  
    int error = lua_pcall(m_state, 0, 1, 0);         /* call function, stack: ret */  
//    lua_gc(m_state, LUA_GCCOLLECT, 0);  
  
    if (error)  
    {  
        CCLOG("[LUA ERROR] %s", lua_tostring(m_state, - 1));  
        lua_pop(m_state, 1); // clean error message  
        return 0;  
    }  
  
    // get return value  
    //如果取得的第一个参数不是数字,返回错误。  
    if (!lua_isnumber(m_state, -1))  
    {  
        lua_pop(m_state, 1);  
        return 0;  
    }  
    //取得数字的参数存放在ret中。  
int ret = lua_tointeger(m_state, -1);  
//参数出栈,恢复堆栈。  
    lua_pop(m_state, 1);                                            /* stack: - */  
    return ret;  
}  
//通过句柄调用函数多种形态。  
//通过句柄调用函数,参数二为参数数量。  
int CCLuaEngine::executeFunctionByHandler(int nHandler, int numArgs)  
{  
    if (pushFunctionByHandler(nHandler))  
    {  
        if (numArgs > 0)  
        {  
            lua_insert(m_state, -(numArgs + 1));                        /* stack: ... func arg1 arg2 ... */  
        }  
  
        int error = 0;  
        // try  
        // {  
            error = lua_pcall(m_state, numArgs, 1, 0);                  /* stack: ... ret */  
        // }  
        // catch (exception& e)  
        // {  
        //     CCLOG("[LUA ERROR] lua_pcall(%d) catch C++ exception: %s", nHandler, e.what());  
        //     lua_settop(m_state, 0);  
        //     return 0;  
        // }  
        // catch (...)  
        // {  
        //     CCLOG("[LUA ERROR] lua_pcall(%d) catch C++ unknown exception.", nHandler);  
        //     lua_settop(m_state, 0);  
        //     return 0;  
        // }  
        if (error)  
        {  
            CCLOG("[LUA ERROR] %s", lua_tostring(m_state, - 1));  
            lua_settop(m_state, 0);  
            return 0;  
        }  
  
        // get return value  
        int ret = 0;  
        //如果返回参数是数字转为整数。  
        if (lua_isnumber(m_state, -1))  
        {  
            ret = lua_tointeger(m_state, -1);  
        }//如果是布尔型转为true或false  
        else if (lua_isboolean(m_state, -1))  
        {  
            ret = lua_toboolean(m_state, -1);  
        }  
    //参数出栈,恢复堆栈。  
        lua_pop(m_state, 1);  
        return ret;  
    }  
    else  
    {  
        return 0;  
    }  
}  
  
//通过句柄调用函数,参数二为整数数据。  
int CCLuaEngine::executeFunctionWithIntegerData(int nHandler, int data)  
{  
    lua_pushinteger(m_state, data);  
    return executeFunctionByHandler(nHandler, 1);  
}  
//通过句柄调用函数,参数二为浮点数据。  
int CCLuaEngine::executeFunctionWithFloatData(int nHandler, float data)  
{  
    lua_pushnumber(m_state, data);  
    return executeFunctionByHandler(nHandler, 1);  
}  
//通过句柄调用函数,参数二为布尔型数据。  
int CCLuaEngine::executeFunctionWithBooleanData(int nHandler, bool data)  
{  
    lua_pushboolean(m_state, data);  
    return executeFunctionByHandler(nHandler, 1);  
}  
//通过句柄调用函数,参数二为CCObject指针数据和其类型名称。  
int CCLuaEngine::executeFunctionWithCCObject(int nHandler, CCObject* pObject, const char* typeName)  
{  
    tolua_pushusertype_ccobject(m_state, pObject->m_uID, &pObject->m_nLuaID, pObject, typeName);  
    return executeFunctionByHandler(nHandler, 1);  
}  
//将一个整数数值压栈做为参数。  
int CCLuaEngine::pushIntegerToLuaStack(int data)  
{  
    //将整数值压入堆栈  
lua_pushinteger(m_state, data);  
//返回参数的数量。  
    return lua_gettop(m_state);  
}  
//将一个浮点数值压栈做为参数。  
int CCLuaEngine::pushFloatToLuaStack(int data)  
{  
    //将数字值压入堆栈  
lua_pushnumber(m_state, data);  
//返回参数的数量。  
    return lua_gettop(m_state);  
}  
//将一个布尔数值压栈做为参数。  
int CCLuaEngine::pushBooleanToLuaStack(int data)  
{  
    //将boolean值压入堆栈  
lua_pushboolean(m_state, data);  
//返回参数的数量。  
    return lua_gettop(m_state);  
}  
//将一个CCObject指针和类型名压栈做为参数。  
int CCLuaEngine::pushCCObjectToLuaStack(CCObject* pObject, const char* typeName)  
{  
    tolua_pushusertype_ccobject(m_state, pObject->m_uID, &pObject->m_nLuaID, pObject, typeName);  
    return lua_gettop(m_state);  
}  
  
// 执行单点触屏事件  
int CCLuaEngine::executeTouchEvent(int nHandler, int eventType, CCTouch *pTouch)  
{  
CCPoint pt = CCDirector::sharedDirector()->convertToGL(pTouch->getLocationInView());  
//将参数压栈后调用函数。  
    lua_pushinteger(m_state, eventType);  
    lua_pushnumber(m_state, pt.x);  
    lua_pushnumber(m_state, pt.y);  
    return executeFunctionByHandler(nHandler, 3);  
}  
  
//执行多点触屏事件。  
int CCLuaEngine::executeTouchesEvent(int nHandler, int eventType, CCSet *pTouches)  
{  
//将类型参数压栈后调用函数。  
lua_pushinteger(m_state, eventType);  
//创建一个表  
    lua_newtable(m_state);  
//将多个触点信息参数放入表中。  
    CCDirector* pDirector = CCDirector::sharedDirector();  
    CCSetIterator it = pTouches->begin();  
    CCTouch* pTouch;  
    int n = 1;  
    while (it != pTouches->end())  
    {  
        pTouch = (CCTouch*)*it;  
        CCPoint pt = pDirector->convertToGL(pTouch->getLocationInView());  
        //将位置x压入堆栈  
        lua_pushnumber(m_state, pt.x);  
        //将栈顶的数值放入到表中对应索引n的数值中  
        lua_rawseti(m_state, -2, n++);  
        //将位置x压入堆栈  
        lua_pushnumber(m_state, pt.y);  
        //将栈顶的数值放入到表中对应索引n的数值中  
        lua_rawseti(m_state, -2, n++);  
        ++it;  
    }  
    //以表做为第二参数压栈,调用函数。  
    return executeFunctionByHandler(nHandler, 2);  
}  
//通过句柄调用函数,参数二为CCObject指针数据和其类型名称。  
int CCLuaEngine::executeSchedule(int nHandler, float dt)  
{  
    return executeFunctionWithFloatData(nHandler, dt);  
}  
// 加入一个多线程加载LUA脚本的实时回调函数,此函数用于ANDROID  
void CCLuaEngine::addLuaLoader(lua_CFunction func)  
{  
    if (!func) return;  
  
//取得全局表  
lua_getglobal(m_state, "package");                       
//取得全局表中的“loaders”表  
    lua_getfield(m_state, -1, "loaders");                   
//将设定的函数和参数压栈  
lua_pushcfunction(m_state, func);                       
//将参数压栈  
    for (int i = lua_objlen(m_state, -2) + 1; i > 2; --i)  
{  
    //取得原"loaders"表第i-1个参数  
        lua_rawgeti(m_state, -2, i - 1);                                                                                
        //将取出的值放到新"loaders"表中第i个数值  
        lua_rawseti(m_state, -3, i);                        
}  
//将函数设为新"loaders"表的第2个参数  
    lua_rawseti(m_state, -2, 2);                             
//把“loaders” 表放到全局表中  
    lua_setfield(m_state, -2, "loaders");                    
    //参数出栈,恢复堆栈。  
    lua_pop(m_state, 1);  
}  
//将一个句柄压栈  
bool CCLuaEngine::pushFunctionByHandler(int nHandler)  
{  
    //找出注册函数表的第nHandler个数值  
lua_rawgeti(m_state, LUA_REGISTRYINDEX, nHandler);  /* stack: ... func */  
//判断是否是函数。  
    if (!lua_isfunction(m_state, -1))  
    {  
        CCLOG("[LUA ERROR] function refid '%d' does not reference a Lua function", nHandler);  
        lua_pop(m_state, 1);  
        return false;  
    }  
    return true;  
}  
 
 
      然后我们来看一下CCScriptEngineManager,这个类被称为脚本引擎管理器,其实很简单,只是用来设定当前项目的唯一正在使用的脚本引擎。也许Cocos2d-x打算用它管理多种类型脚本引擎,比如python,js等。
 
[cpp] 
class CC_DLL CCScriptEngineManager  
{  
public:  
    //析构  
    ~CCScriptEngineManager(void);  
    //取得单例指针  
    CCScriptEngineProtocol* getScriptEngine(void) {  
        return m_pScriptEngine;  
    }  
    //设置使用的LUA管理器  
void setScriptEngine(CCScriptEngineProtocol *pScriptEngine);  
//移除使用的LUA管理器。  
    void removeScriptEngine(void);  
    //取得单例指针  
static CCScriptEngineManager* sharedManager(void);  
//销毁单例  
    static void purgeSharedManager(void);  
  
private:  
    //构造,单例,你懂的。  
    CCScriptEngineManager(void)  
    : m_pScriptEngine(NULL)  
    {  
    }  
    //使用的LUA脚本引擎  
    CCScriptEngineProtocol *m_pScriptEngine;  
};  
其对应的CPP实现:  
//全局唯一的  
static CCScriptEngineManager* s_pSharedScriptEngineManager = NULL;  
//析构  
CCScriptEngineManager::~CCScriptEngineManager(void)  
{  
    removeScriptEngine();  
}  
//设置使用的LUA管理器  
void CCScriptEngineManager::setScriptEngine(CCScriptEngineProtocol *pScriptEngine)  
{  
    removeScriptEngine();  
    m_pScriptEngine = pScriptEngine;  
    m_pScriptEngine->retain();  
}  
//移除使用的LUA管理器。  
void CCScriptEngineManager::removeScriptEngine(void)  
{  
    if (m_pScriptEngine)  
    {  
        m_pScriptEngine->release();  
        m_pScriptEngine = NULL;  
    }  
}  
  
//取得单例指针  
CCScriptEngineManager* CCScriptEngineManager::sharedManager(void)  
{  
    if (!s_pSharedScriptEngineManager)  
    {  
        s_pSharedScriptEngineManager = new CCScriptEngineManager();  
    }  
    return s_pSharedScriptEngineManager;  
}  
//销毁单例  
void CCScriptEngineManager::purgeSharedManager(void)  
{  
    if (s_pSharedScriptEngineManager)  
    {  
        delete s_pSharedScriptEngineManager;  
        s_pSharedScriptEngineManager = NULL;  
    }  
}  
 
         现在我们来实际操作一下。
         打开HelloLua工程中的AppDelegate.cpp:
在AppDelegate::applicationDidFinishLaunching()函数中看这几行代码:
[cpp] 
 //取得LUA脚本引擎  
CCScriptEngineProtocol* pEngine = CCLuaEngine::engine();  
//设置脚本引擎管理器使用新创建的LUA脚本引擎  
    CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);  
//如果是ANDROID平台,获hello.lua内存到字符串然后执行字符串  
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)  
    CCString* pstrFileContent = CCString::createWithContentsOfFile("hello.lua");  
    if (pstrFileContent)  
    {  
        pEngine->executeString(pstrFileContent->getCString());  
    }  
#else  
    //如果不是ANDROID平台,取得hello.lua文件全路径并执行文件。  
    std::string path = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("hello.lua");  
    pEngine->addSearchPath(path.substr(0, path.find_last_of("/")).c_str());  
    pEngine->executeScriptFile(path.c_str());  
#endif   
 
 
         就这样,hello.lua中的脚本就可以被执行了。
 
         现在我们将HelloLua工程目录拷出一份来,将目录和工程命名为StudyLua,并在程序运行目录中加入相关资源图片。之后我们打开hello.lua:
 
[html]  
-- 设置内存回收  
collectgarbage("setpause", 100)  
collectgarbage("setstepmul", 5000)  
  
  
-- 取得窗口大小  
local winSize = CCDirector:sharedDirector():getWinSize()  
-- 将Hello背景图加入  
  
local function createLayerHello()  
    local layerHello = CCLayer:create()  
  
    -- 加入背景图  
    local bg = CCSprite:create("Hello.png")  
    bg:setPosition(winSize.width / 2 , winSize.height / 2)  
    layerHello:addChild(bg)  
      
    -- 创建HelloWorld  
    local label = CCLabelTTF:create("Hello Cocos2d-x", "Arial", 50)  
    label:setPosition(winSize.width / 2 ,60)  
    label:setVisible(true)  
    layerHello:addChild(label)  
       
    return layerHello  
end  
  
--将关闭按钮菜单加入  
local function createExitBtn()  
  
    local layerMenu = CCLayer:create()  
  
    --局部函数,用于退出  
    local function menuCallbackExit()  
        CCDirector:sharedDirector():endToLua()  
    end  
  
    -- 创建退出按钮  
    local menuPopupItem = CCMenuItemImage:create("CloseNormal.png", "CloseSelected.png")  
    -- 放在居上角附近  
    menuPopupItem:setPosition(winSize.width - 50, winSize.height - 50)  
    -- 注册退出函数  
    menuPopupItem:registerScriptHandler(menuCallbackExit)  
    -- 由菜单按钮项创建菜单  
    local   menuClose = CCMenu:createWithItem(menuPopupItem)  
    menuClose:setPosition(0, 0)  
    menuClose:setVisible(true)  
    -- 将菜单加入层中  
    layerMenu:addChild(menuClose)  
  
    return layerMenu  
end  
  
--创建场景  
local sceneGame = CCScene:create()  
--将Hello背景图加入  
sceneGame:addChild(createLayerHello())  
--将关闭按钮菜单加入  
sceneGame:addChild(createExitBtn())  
--运行场景  
CCDirector:sharedDirector():runWithScene(sceneGame)  
运行一下:

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台