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

[Cocos2d-x相关教程来源于红孩儿的游戏编程之路CSDN博客地址http://blog.csdn.net/honghaier

红孩儿Cocos2d-X学习园地QQ3群:205100149,47870848
 


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


 

 另:本章所用Cocos2d-x版本为:

Cocos2d-2.0.2

http://cn.cocos2d-x.org/download

         大家好,又是一周过去了,这一周忙的有点焦头烂额,除了工作照例每天加班到九点外,工具箱又做了大幅改进,新的论坛游戏兔子game2z也上线了,Cocos2d-x的学习时间被压缩的很少了,现在是凌晨一点零六分,看着妻子睡熟的样子,我也只能告诉自已,坚持到底。

 

         好了,不说废话,本周奉上一篇初级入门教程博文,Cocos2d-x中的LUA引导与入门。

做为惯例,一切都是以HelloWorld的样例为准。我们今天学习用LUA来完成一版HelloWorld。

 

         大家既使没有看过我的“HelloWorld 深入分析”一文,想必也无数次运行过Cocos2d-x里的HelloCpp工程,对于运行的结果画面熟烂于心。我们回想一下,这个画面里有什么。嗯,一个背景图精灵,一个文字标签,一个关闭按钮。OK,咱们就做这么个东西。

 

         首先,我们要知道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:


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:

//加入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实现:

//本类的头文件。
#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);
}
//通过函数索引值移除对应的LUA函数。
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等。


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()函数中看这几行代码:

 //取得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:


-- 设置内存回收
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)

运行一下:



我只能说,一切好极了,下课!


  • 14
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火云洞红孩儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值