Lua在程序中的数据定义和过程定义的界定原则的研究
引言
作为宿主语言的衍生,Lua无论从数据对象的填充,还是处理过程的定制,都提供了很好的支持。甚至我们可以将全部的宿主语言都搬到Lua里来写。在这样大 的灵活度下,如何界定什么样的函数需要导出到Lua,如何对数据对象定义,或者说使用Lua的基本思路是什么,时常让刚学会Lua的人迷惑。本文使用一个 实际例子来讲述一个C++系统和Lua结合的演变过程、思路,并比较各个方案之间的优劣,提供一个使用Lua的参考思路。
找出需要定制的地方
在我们的游戏引擎中有一个 renderroom 系统,就是用来实时渲染一个对象到贴图的轻量系统。整个系统的静态结构就在这里略了,重点在于RenderRoom的定制。当你需要渲染一个角色的时候,需要确定
摄象机的位置、朝向
场景模型、特效
光
最开始的时候也就考虑到了这3点。因为我们那个时候需要渲染生物只有2种需求,渲染头和渲染全身,因此在C++里定义了以下类型
struct RenderType
{
enum T
{
Head,
Body,
};
};
并写了一个处理函数:
void refreshTexture(RenderType::T type)
{
switch( type)
{
case RenderType::Head:
设置摄象机等参数;
break;
case RenderType::Head:
设置摄象机等参数;
break;
}
}
但是在这之后发现,渲染人型生物和渲染爬行类生物的时候, Head的参数根本就不同。
为了迅速解决问题,添加了新的Head类型。
这当然是个非常临时的方法,所以不久之后我们就碰到生物体型过多,参数类型过多的问题。于是“可配置”的需求就变的强烈了。这个时候开始考虑利用Lua作为数据定义文件。我尝试了以下几种方式。
一、在Lua中定义数据文件结构,将数据对象返回给CPP进行解析。
// Lua
local data={
cameraPos = {x,y,z} // 这是一个原生结构,如果你将 Vector3 之类的结构导出到Lua了,这里就可以省不少事情
cameraDir = {x,y,z} // 这是一个原生结构,如果你将 Vector3 之类的结构导出到Lua了,这里就可以省不少事情
scene = "RenderRoomScene1.scene"
};
parseRenderRoomData(data); ---在C++中定义的函数. 导出到Lua
data=nil;
// CPP
parseRenderRoomData(lua_State* L)
{
// 解析表;
// 使用 lua_next 取得各个元素,按照Lua中定义的数据格式进行解析
};
这个方案是不好的,原因在于 parseRenderRoomData 对数据的解析过程受制于 data的定义,而data中的数据经常变化,甚至结构也经常变化,所以需要在 parseRenderRoomData 函数中编写大量的异常处理代码防止数据文件损坏的时候程序不崩溃,或者说引起Lua的栈不平衡.
这个方案唯一的优点可能就是好理解吧:Lua提供数据对象,CPP解析并创建实例对象.
二、CPP中定义数据文件结构,创建对象, Lua填充数据, 将数据对象返回给CPP进行解析。
这个方案的代码静态结构大致是这样:
//CPP
struct RenderRoomData
{
// 这是一个原生结构,如果你将 Vector3 之类的结构导出到Lua了,这里就可以省不少事情
float cameraPos_x;
float cameraPos_y;
float cameraPos_z;
float cameraDir_x;
float cameraDir_y;
float cameraDir_z;
std::string sceneFile;
};
RenderRoomData* createRenderRoomDate()
{
return new RenderRoomData;
}
parseRenderRoomData(lua_State* L)
{
// 同上,但是是按照 RenderRoomData 的定义进行解析.
}
// Lua
local data= createRenderRoomDate()
data.cameraPos_x;
data.cameraPos_y;
data.cameraPos_z;
data.cameraDir_x;
data.cameraDir_y;
data.cameraDir_z;
data.sceneFile = "RenderRoomScene1.scene"
parseRenderRoomData(data);
data=nil;
这个方案从核心上来说和方案一一致,就是Lua提供数据. 但是在数据协议方面则做了非常大的改进,因为是使用CPP定义的对象,所以解析的过程就可以确定下来.这样可以将错误处理代码省略.但这只是理由之一,最 大的理由在于Lua文件一般是由编辑器或策划来负责维护,现在数据格式由程序定义好,则会大大减少数据文件出错几率.并且数据定义这样的事情也就在一开始 变动频繁,当稳定后基本上是不变的.考虑到变动的成本,方案二远胜于方案一.
在数据定义方面,我会选择方案二,因为实际情况是如果你采用方案一,写错误处理的时间会根据数据文件的复杂程度急剧增长,最终从各个方面都会劣于方案一。 但是如果是在系统设计的初期,几乎没有任何错误处理,所以方案一也不失为一种方便的原型模型。这个需要参考最终代码的质量标准。
(我哭死了,写了1个多小时的内容被我自己覆盖了...以后再也不用txt写东西了,用vs)
新的演化
如果需求能一次到位,写程序也就不用这样累了,所以"需求总是变化的"这句真理永远适用。RenderRoom的核心思路是为了将一个对象渲染到贴图,在一些引擎里这个系统叫"化身". 根据使用场合的不同,我们会希望贴图有的需要背景使用透明色,比如头像,有的又不希望它透明,比如装备栏里的人物角色,并且在一些时候你还想定制Fog,Bloom等参数. 但是这样的配置数据也就只有有限的几种,而像摄象机位置、朝向这样的信息是需要每怪物单独设置的,这样RenderRoomData里就需要包含2种不同类型的数据: 每对象数据 和 每类型数据, 两者的数量级比例大致是1000:10. 一般对于这样的数据拆分的常见手段是使用ID或者说句柄. 在这里首先定义一个新的数据结构:
struct RenderParameters
{
// 渲染相关的参数定义
};
不在这里定义ID的理由,随后说明.
对于这个新结构的使用方法,有以下几种方案:
一、数据定义冗余策略.
// CPP
在 struct RenderRoomData 中增加一个成员变量
struct RenderRoomData
{
...
RenderParameters renderParameters;
};
// Lua
local data= createRenderRoomDate()
...
data.renderParameters.propory1 = a;
data.renderParameters.propory2 = b;
data.renderParameters.propory3 = c;
...
对于数据冗余策略,ID是没有必要的,所以在RenderParameters的原型定义里没有定义ID。
二、使用ID检索策略
// CPP
在 struct RenderRoomData 中增加一个成员变量
struct RenderParameters
{
unsigned int typeID;
// 渲染相关的参数定义
};
在 struct RenderRoomData 中增加一个成员变量
struct RenderRoomData
{
...
unsigned int renderParametersID;
};
现在你可以使用任何喜欢的检索方式对RenderParameters对象进行创建和检索,比如:
1、使用在之前讨论数据定义的方式中提到的方案二,在CPP中创建,Lua中填充,并在CPP中检索他们。
2、利用Lua的全局对象,对其进行填充和检索。
3、定义Lua函数,利用ID将冗余数据自动填充。这个方法是方案一和方案二的结合。
//Lua
--相信看到这里,各位看官都有自己的想法了,我就不叨叨了
三、定义Lua函数对象
使用这个方法有先决条件:如果你将 refreshTexture 中调用的具体干活的函数导出了的话,就可以使用这个方法。暂时将这些功能函数称其为“干活函数”。
分析我们定义RenderParameters的最初目的就可以得知,实际上我们就是在为干活函数搜集参数,并从Lua中传递到CPP中给它们使用。因此如果我们能在Lua中直接调用干活函数的话,就可以使用更加简单的方式来处理“RenderRoom的定制”这个终极目标。
// CPP
// 实现一个能调用Lua函数的函数,如
template<typename Par1,typename Par2, typename Par3>
bool callLuaFunction(const char* functionName, Par1 param1, Par2 param2, Par3 param3);
这个函数需要能把 Lua对象向 ParX 类型的对象进行转化,实现方式很多,各位自行拿捏.
定义参数的理由随后说明.
// Lua
--定义一个全局表
RenderRoomBulider = {}
--添加具体Builder
RenderRoomBulider["头像"] = function(cameraPos, cameraDir, viewport) -- 视口对象.因为Fog , Bloom等参数是每视口设置的.
SetCameraPos(cameraPos);
SetCameraPos(cameraPos);
CreateSceneStage("RenderRoomScene1.scene");
SetFogDistance(...);
SetFogColor(...);
SetBloom();
end;
RenderRoomBulider["装备栏"] = function(cameraPos, cameraDir, viewport)
SetCameraPos(cameraPos);
SetCameraPos(cameraPos);
CreateSceneStage("RenderRoomScene1.scene");
SetFogDistance(...);
SetFogColor(...);
SetBloom();
end;
....
最后在RenderRoom初始化的地方写如下代码:
std::string builderTypeName = "RenderRoomBulider[头像]";
Vector3 cameraPos = 具体怪物的CameraPos;
Vector3 cameraDir = 具体怪物的CameraDir;
Viewport* viewport = RenderRoom关联的视口;
callLuaFunction( builderTypeName.c_str(), cameraPos, cameraDir, viewport);
最后的问题在于具体怪物的Camera属性记录在哪呢, 我相信每个游戏都有一个怪物数据表吧,就记录在那里吧.
Camera属性是每怪物的,RenderParameter是每应用场景的,我们通过怪物表获取Camera属性,通过Lua获取RenderParameter属性,最终将RenderRoom的定制从程序中释放出来了.
小结
Lua的特点在于灵活,具体表现就是没有任何限制,但是就象你可以拿 C风格的强制类型转换在C++里写任意对象的转换一样, 总是有原则存在其中的,这些原则可以帮助我们写出健壮的、易维护的代码,对于脚本语言更加如此. 这些原则可能来自于程序经理要求,项目规范,个人习惯等等方面,但是最重要的一条就是,要统一原则,并贯彻下去.