现在做游戏的较多用脚本 ,可以热加载代码。
脚本系统的目的是方便c++和lua之间的相互调用,实现c++和lua的 交互。
脚本系统设计:
(1)脚本系统创建时初始化虚拟机和库,析构时销毁。
(2)脚本加载:
主体入口脚本NPCEntry.lua:
含所有npc的脚本的表,含聊天接口函数。选择调用接口控制脚本QC.lua的接口,或者npc脚本的接口。
接口控制脚本QC.lua:
含接任务和交任务的接口(会使用任务表NPCQuest),检查接任务条件,来选择接任务还是返回聊天响应;检查交任务的条件,来选择交任务还是返回聊天信息。
各个npc脚本:
各个npc脚本以npc的id来命名,加载时存储在主体入口脚本的NPCEntry.lua的npc的表。
(3)配置加载:
NPCQuest表是配置在配置表,在脚本系统初始化时热加载
(4)函数调用:
(4-1)c++调用lua:把脚本参数存储到自定义列表,调用lua函数时压入函数和自定义参数到运行时栈。
(4-2)lua调用c++:使用tolua++导出需要调用的类的接口和成员变量。
脚本结构设计:
(1)主体入口文件NPCEntry.lua
(1-1)表npcTable 会按npcId 索引对应的npc lua文件的代码
(1-2)点击npc 函数 clickNPC 会返回对应的该npcId的所有的可接任务和可提交任务组成的字符串
(1-3)与npc聊天函数 talkNPC 会调用接任务或交任务接口,并返回其对话的字符串
(2)任务控制文件QC.lua 含任务接受(或对话接口)、任务提交(或对话)接口。(3)npc 脚本 npc1.lua (npc2.lua ......). 含指定npcId的默认对话(可接可交任务)组成的字符串的接口。可拓展npc与玩家交互接口。
1、脚本系统定义
为了可以传入自定义类型的变量和原子变量到一个列表,再一次性压栈(其实函数的调用就是压栈弹栈的过程,c++和lua交互的过程也是如此,就是通过栈来实现变量的访问)。
(1)脚本系统的初始化和关闭
脚本系统的析构函数里需要关掉虚拟机
if(m_pLua)
{
lua_close(m_pLua);
m_pLua = NULL;
}
在构造函数里打开虚拟机
m_pLua = lua_open();
luaL_openlibs(m_pLua);
初始化脚本系统:
1)加载npc脚本和npc数据到lua虚拟机2)把tolua++导出的接口导入到lua虚拟机
bool ScriptSystemLoad::init()
{
if (!initNPCScript())//加载npc脚本和npc数据到lua虚拟机
{
printf("initNPCScript error\n");
return false;
}
bool res = loadLuaServerInterface();//加载tolua++导出的接口到lua虚拟机
if (!res)
{
printf("loadLuaServerInterface error\n");
return false;
}
return true;
}
(2)调用脚本函数
步骤如下:
1)调用函数需要传入函数指针和参数,先压入需要调用的函数,然后是压入各个参数。最终执行是使用交互栈底的lua函数,而参数则是函数以上的指定个数的交互栈数据。
2)返回函数的执行结果,在交互栈的栈顶(结果可能多个)。
3) 调用过后就需要清栈。这是个编程习惯也是符合语言设计理念的(想下c++函数调用也是遵守函数调用约定,调用过后就清交互栈的)。
c++调用lua函数使用到的lua c的api 是:
LUA_API int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc);(参数:lua 虚拟机对象 函数参数个数 返回值个数 错误处理函数)
执行lua函数调用(参数:函数名、需要压入的参数、需要返回的结果)
bool ScriptSystem::exec(const char* fname, const ScriptValueList* args, ScriptValueList *results )
{
//清除堆栈(获取堆栈的数据个数,弹出堆栈的数据)
#define cleanStack() do {\
int stackNum = lua_gettop(m_pLua);\
if (stackNum)\
{\
lua_pop(m_pLua,stackNum);\
}\
}while(0)
int i;
int args_count = 0;
if (args)args_count = args->count;
lua_getglobal(m_pLua,fname);//获取需要调用的lua函数
//压入参数列表
for (i = 0;i < args_count;i++)
{
ScriptValue scriptValue = args->values[i];//根据不同参数类型来压入参数
if (ScriptValue::vNumber == scriptValue.type)
{
lua_pushnumber(m_pLua,scriptValue.data.d);
}
else if (ScriptValue::vInterger == scriptValue.type)
{
lua_pushinteger(m_pLua,scriptValue.data.i);
}
else if (ScriptValue::vString == scriptValue.type)
{
lua_pushstring(m_pLua,scriptValue.data.str);
}
else if (ScriptValue::vBool == scriptValue.type)
{
lua_pushboolean(m_pLua,scriptValue.data.i);
}
else if (ScriptValue::vPointer == scriptValue.type)
{
lua_pushlightuserdata(m_pLua,scriptValue.data.ptr);
}
else if (ScriptValue::vBaseObject == scriptValue.type)
{
tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CBaseObject");
}
else if (ScriptValue::vEntity == scriptValue.type)
{
tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CEntity");
}
else if (ScriptValue::vActor == scriptValue.type)
{
tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CDoer");
}
else if (ScriptValue::vPlayer == scriptValue.type)
{
tolua_pushusertype(m_pLua,scriptValue.data.ptr,"CPlayer");
}
else
{
lua_pushnil(m_pLua);//压入空指针
}
}
if (!results)//不需要返回值
{
int err = lua_pcall(m_pLua,args_count,0,0);//调用lua函数
if (err)
{
const char* result = lua_tostring(m_pLua, -1);
logError("Script Err:lua_pcall result:%s,fname %s\n",result,fname);
cleanStack();
return false;
}
}
else//需要一个返回值
{
int err = lua_pcall(m_pLua,args_count,1,0);
const char* result = lua_tostring(m_pLua, -1);//取出栈顶的执行结果
if (err)//lua函数调用错误
{
logError("Script Err:lua_pcall result:%s,fname %s\n",result,fname);
cleanStack();
return false;
}
if (result)
{
logDebug("lua_pcall result:%s,fname %s\n",result,fname);
results->push(result);//加入返回结果
}
}
cleanStack();//清除堆栈
return true;
}
(3)脚本变量列表
定义一些接口用于传入变量到列表。在脚本系统执行c++调用lua接口时传入的参数列表和获取返回结果列表。
class ScriptValueList : public CBaseObject
{
public:
static const int MaxValueCount = 8;//脚本值列表最多值数量
int count;
ScriptValue values[MaxValueCount];
public:
ScriptValueList(){ count = 0; memset(values, 0, sizeof(values)); }
~ScriptValueList(){ clear(); }
//添加一个整数值到列表
bool push(int v);
//添加一个浮点值到列表
bool push(double v);
//添加一个布尔值到列表
bool push(bool b);
//添加一个字符串值到列表
bool push(const char* str);
//添加一个指针值到列表
bool push(void* ptr);
//添加一个CBaseObject值到列表
bool push(CBaseObject* ptr);
//添加一个CEntity值到列表
bool push(CEntity* ptr);
//添加一个CDoer值到列表
bool push(CDoer* ptr);
//添加一个CPlayer值到列表
bool push(CPlayer* ptr);
//添加一个脚本值到列表
bool push(const ScriptValue &v);
//清空列表数据
inline void clear()
{
for (int i= count-1; i>-1; --i)
{
values[i].clear();
}
count = 0;
}
};
(4)变量
参数列表的需要的自定义的类型变量。
重载等号操作符来对不同类型的赋值到对应的成员中。
class ScriptValue
{
public:
//脚本值类型
enum ValueType
{
vNumber = 0,
vInterger = 1,
vString = 2,
vBool = 3,
vPointer = 4,
vBaseObject = 5,
vEntity = 6,
vActor = 7,
vPlayer = 8,
};
~ScriptValue() { clear(); }
inline void operator = (const int v)