网游uilua 逻辑或中哪些地方需要使用lua代码

编写高性能的 Lua 代码 - 文章 - 伯乐在线
& 编写高性能的 Lua 代码
Lua是一门以其性能著称的脚本语言,被广泛应用在很多方面,尤其是游戏。像《魔兽世界》的插件,手机游戏《大掌门》《神曲》《迷失之地》等都是用Lua来写的逻辑。
所以大部分时候我们不需要去考虑性能问题。Knuth有句名言:“过早优化是万恶之源”。其意思就是过早优化是不必要的,会浪费大量时间,而且容易导致代码混乱。
所以一个好的程序员在考虑优化性能前必须问自己两个问题:“我的程序真的需要优化吗?”。如果答案为是,那么再问自己:“优化哪个部分?”。
我们不能靠臆想和凭空猜测来决定优化哪个部分,代码的运行效率必须是可测量的。我们需要借助于分析器来测定性能的瓶颈,然后着手优化。优化后,我们仍然要借助于分析器来测量所做的优化是否真的有效。
我认为最好的方式是在首次编写的时候按照最佳实践去写出高性能的代码,而不是编写了一堆垃圾代码后,再考虑优化。相信工作后大家都会对事后的优化的繁琐都深有体会。
一旦你决定编写高性能的Lua代码,下文将会指出在Lua中哪些代码是可以优化的,哪些代码会是运行缓慢的,然后怎么去优化它们。
在代码运行前,Lua会把源码预编译成一种中间码,类似于Java的虚拟机。这种格式然后会通过C的解释器进行解释,整个过程其实就是通过一个while循环,里面有很多的switch...case语句,一个case对应一条指令来解析。
自Lua 5.0之后,Lua采用了一种类似于寄存器的虚拟机模式。Lua用来储存其寄存器。每一个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录。每一个函数的栈都可以储存至多250个寄存器,因为栈的长度是用8个比特表示的。
有了这么多的寄存器,Lua的预编译器能把所有的local变量储存在其中。这就使得Lua在获取local变量时其效率十分的高。
举个栗子: 假设a和b为local变量,a = a + b的预编译会产生一条指令:
;a是寄存器0 b是寄存器1
;a是寄存器0 b是寄存器1ADD 0 0 1
但是若a和b都没有声明为local变量,则预编译会产生如下指令:
0 0 1do add
GETGLOBAL&&&&0 0&&&&;get aGETGLOBAL&&&&1 1&&&&;get bADD&&&&&&&&&&0 0 1&&;do addSETGLOBAL&&&&0 0&&&&;set a
所以你懂的:在写Lua代码时,你应该尽量使用local变量。
以下是几个对比测试,你可以复制代码到你的编辑器中,进行测试。
a = os.clock()
for i = 1, do
local x = math.sin(i)
b = os.clock()
print(b-a) -- 1.113454
a = os.clock()for i = 1, do&&local x = math.sin(i)endb = os.clock()print(b-a) -- 1.113454
把math.sin赋给local变量sin:
a = os.clock()
local sin = math.sin
for i = 1, do
local x = sin(i)
b = os.clock()
print(b-a) --0.75951
a = os.clock()local sin = math.sinfor i = 1, do&&local x = sin(i)endb = os.clock()print(b-a) --0.75951
直接使用math.sin,耗时1.11秒;使用local变量sin来保存math.sin,耗时0.76秒。可以获得30%的效率提升!
关于表(table)
表在Lua中使用十分频繁,因为表几乎代替了Lua的所有容器。所以快速了解一下Lua底层是如何实现表,对我们编写Lua代码是有好处的。
Lua的表分为两个部分:数组(array)部分和哈希(hash)部分。数组部分包含所有从1到n的整数键,其他的所有键都储存在哈希部分中。
哈希部分其实就是一个哈希表,哈希表本质是一个数组,它利用哈希算法将键转化为数组下标,若下标有冲突(即同一个下标对应了两个不同的键),则它会将冲突的下标上创建一个,将不同的键串在这个链表上,这种解决冲突的方法叫做:链地址法。
当我们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希(rehash)。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于所有元素数目的2的乘方。
当创建一个空表时,数组和哈希部分的长度都将初始化为0,即不会为它们初始化任何数组。让我们来看下执行下面这段代码时在Lua中发生了什么:
local a = {}
for i=1,3 do
a[i] = true
local a = {}for i=1,3 do&&&&a[i] = trueend
最开始,Lua创建了一个空表a,在第一次迭代中,a[1] = true触发了一次rehash,Lua将数组部分的长度设置为2^0,即1,哈希部分仍为空。在第二次迭代中,a[2] = true再次触发了rehash,将数组部分长度设为2^1,即2。最后一次迭代,又触发了一次rehash,将数组部分长度设为2^2,即4。
下面这段代码:
a.x = 1; a.y = 2; a.z = 3
a = {}a.x = 1; a.y = 2; a.z = 3
与上一段代码类似,只是其触发了三次表中哈希部分的rehash而已。
只有三个元素的表,会执行三次rehash;然而有一百万个元素的表仅仅只会执行20次rehash而已,因为2^20 = 1048576 & 1000000。但是,如果你创建了非常多的长度很小的表(比如坐标点:point = {x=0,y=0}),这可能会造成巨大的影响。
如果你有很多非常多的很小的表需要创建时,你可以将其预先填充以避免rehash。比如:{true,true,true},Lua知道这个表有三个元素,所以Lua直接创建了三个元素长度的数组。类似的,{x=1, y=2, z=3},Lua会在其哈希部分中创建长度为4的数组。
以下代码执行时间为1.53秒:
a = os.clock()
for i = 1,2000000 do
local a = {}
a[1] = 1; a[2] = 2; a[3] = 3
b = os.clock()
print(b-a)
--1.528293
a = os.clock()for i = 1,2000000 do&&&&local a = {}&&&&a[1] = 1; a[2] = 2; a[3] = 3endb = os.clock()print(b-a)&&--1.528293
如果我们在创建表的时候就填充好它的大小,则只需要0.75秒,一倍的效率提升!
a = os.clock()
for i = 1,2000000 do
local a = {1,1,1}
a[1] = 1; a[2] = 2; a[3] = 3
b = os.clock()
print(b-a)
--0.746453
a = os.clock()for i = 1,2000000 do&&&&local a = {1,1,1}&&&&a[1] = 1; a[2] = 2; a[3] = 3endb = os.clock()print(b-a)&&--0.746453
所以,当需要创建非常多的小size的表时,应预先填充好表的大小。
关于字符串
与其他主流脚本语言不同的是,Lua在实现字符串类型有两方面不同。
第一,所有的字符串在Lua中都只储存一份拷贝。当新字符串出现时,Lua检查是否有其相同的拷贝,若没有则创建它,否则,指向这个拷贝。这可以使得字符串比较和表索引变得相当的快,因为比较字符串只需要检查引用是否一致即可;但是这也降低了创建字符串时的效率,因为Lua需要去查找比较一遍。
第二,所有的字符串变量,只保存字符串引用,而不保存它的buffer。这使得字符串的赋值变得十分高效。例如在Perl中,$x = $y,会将$y的buffer整个的复制到$x的buffer中,当字符串很长时,这个操作的代价将十分昂贵。而在Lua,同样的赋值,只复制引用,十分的高效。
但是只保存引用会降低在字符串连接时的速度。在Perl中,$s = $s . 'x'和$s .= 'x'的效率差距惊人。前者,将会获取整个$s的拷贝,并将’x’添加到它的末尾;而后者,将直接将’x’插入到$x的buffer末尾。
由于后者不需要进行拷贝,所以其效率和$s的长度无关,因为十分高效。
在Lua中,并不支持第二种更快的操作。以下代码将花费6.65秒:
a = os.clock()
local s = ''
for i = 1,300000 do
s = s .. 'a'
b = os.clock()
print(b-a)
--6.649481
a = os.clock()local s = ''for i = 1,300000 do&&&&s = s .. 'a'endb = os.clock()print(b-a)&&--6.649481
我们可以用table来模拟buffer,下面的代码只需花费0.72秒,9倍多的效率提升:
a = os.clock()
local s = ''
local t = {}
for i = 1,300000 do
t[#t + 1] = 'a'
s = table.concat( t, '')
b = os.clock()
print(b-a)
a = os.clock()local s = ''local t = {}for i = 1,300000 do&&&&t[#t + 1] = 'a'ends = table.concat( t, '')b = os.clock()print(b-a)&&--0.07178
所以:在大字符串连接中,我们应避免..。应用table来模拟buffer,然后concat得到最终字符串。
3R原则(the rules of 3R)是:减量化(reducing),再利用(reusing)和再循环(recycling)三种原则的简称。
3R原则本是循环经济和环保的原则,但是其同样适用于Lua。
有许多办法能够避免创建新对象和节约内存。例如:如果你的程序中使用了太多的表,你可以考虑换一种数据结构来表示。
举个栗子。 假设你的程序中有多边形这个类型,你用一个表来储存多边形的顶点:
polyline = {
{ x = 1.1, y = 2.9 },
{ x = 1.1, y = 3.7 },
{ x = 4.6, y = 5.2 },
polyline = {&&&&{ x = 1.1, y = 2.9 },&&&&{ x = 1.1, y = 3.7 },&&&&{ x = 4.6, y = 5.2 },&&&&...}
以上的数据结构十分自然,便于理解。但是每一个顶点都需要一个哈希部分来储存。如果放置在数组部分中,则会减少内存的占用:
polyline = {
{ 1.1, 2.9 },
{ 1.1, 3.7 },
{ 4.6, 5.2 },
polyline = {&&&&{ 1.1, 2.9 },&&&&{ 1.1, 3.7 },&&&&{ 4.6, 5.2 },&&&&...}
一百万个顶点时,内存将会由153.3MB减少到107.6MB,但是代价是代码的可读性降低了。
最变态的方法是:
polyline = {
x = {1.1, 1.1, 4.6, ...},
y = {2.9, 3.7, 5.2, ...}
polyline = {&&&&x = {1.1, 1.1, 4.6, ...},&&&&y = {2.9, 3.7, 5.2, ...}}
一百万个顶点,内存将只占用32MB,相当于原来的1/5。你需要在性能和代码可读性之间做出取舍。
在循环中,我们更需要注意实例的创建。
for i=1,n do
local t = {1,2,3,'hi'}
--执行逻辑,但t不更改
for i=1,n do&&&&local t = {1,2,3,'hi'}&&&&--执行逻辑,但t不更改&&&&...end
我们应该把在循环中不变的东西放到循环外来创建:
local t = {1,2,3,'hi'}
for i=1,n do
--执行逻辑,但t不更改
local t = {1,2,3,'hi'}for i=1,n do&&&&--执行逻辑,但t不更改&&&&...end
如果无法避免创建新对象,我们需要考虑重用旧对象。
考虑下面这段代码:
local t = {}
t[i] = os.time({year = i, month = 6, day = 14})
local t = {}for i = 1970, 2000 do&&&&t[i] = os.time({year = i, month = 6, day = 14})end
在每次循环迭代中,都会创建一个新表{year = i, month = 6, day = 14},但是只有year是变量。
下面这段代码重用了表:
local t = {}
local aux = {year = nil, month = 6, day = 14}
aux.year =
t[i] = os.time(aux)
local t = {}local aux = {year = nil, month = 6, day = 14}for i = 1970, 2000 do&&&&aux.year = i;&&&&t[i] = os.time(aux)end
另一种方式的重用,则是在于缓存之前计算的内容,以避免后续的重复计算。后续遇到相同的情况时,则可以直接查表取出。这种方式实际就是效率高的原因所在,其本质是用空间换时间。
Lua自带垃圾回收器,所以我们一般不需要考虑垃圾回收的问题。
了解Lua的垃圾回收能使得我们编程的自由度更大。
Lua的垃圾回收器是一个增量运行的机制。即回收分成许多小步骤(增量的)来进行。
频繁的垃圾回收可能会降低程序的运行效率。
我们可以通过Lua的collectgarbage函数来控制垃圾回收器。
collectgarbage函数提供了多项功能:停止垃圾回收,重启垃圾回收,强制执行一次回收循环,强制执行一步垃圾回收,获取Lua占用的内存,以及两个影响垃圾回收频率和步幅的参数。
对于批处理的Lua程序来说,停止垃圾回收collectgarbage("stop")会提高效率,因为批处理程序在结束时,内存将全部被释放。
对于垃圾回收器的步幅来说,实际上很难一概而论。更快幅度的垃圾回收会消耗更多CPU,但会释放更多内存,从而也降低了CPU的分页时间。只有小心的试验,我们才知道哪种方式更适合。
我们应该在写代码时,按照高标准去写,尽量避免在事后进行优化。
如果真的有性能问题,我们需要用工具量化效率,找到瓶颈,然后针对其优化。当然优化过后需要再次测量,查看是否优化成功。
在优化中,我们会面临很多选择:代码可读性和运行效率,CPU换内存,内存换CPU等等。需要根据实际情况进行不断试验,来找到最终的平衡点。
最后,有两个终极武器:
第一、使用,LuaJIT可以使你在不修改代码的情况下获得平均约5倍的加速。查看LuaJIT在。
第二、将瓶颈部分用C/C++来写。因为Lua和C的天生近亲关系,使得Lua和C可以混合编程。但是C和Lua之间的通讯会抵消掉一部分C带来的优势。
注意:这两者并不是兼容的,你用C改写的Lua代码越多,LuaJIT所带来的优化幅度就越小。
这篇文章是基于Lua语言的创造者Roberto Ierusalimschy在中的翻译改写而来。本文没有直译,做了许多删节,可以视为一份笔记。
感谢Roberto在Lua上的辛勤劳动和付出!
可能感兴趣的话题
好文! 补充一点c跟lua的通讯消耗有时候会十分的严重
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线内容团队正试图以我们微薄的力量,把优秀的原创文章和译文分享给读者,为“快餐”添加一些“营养”元素。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线开发者通道
CocoaChina 2013春季开发者大会:《大掌门》欧阳刘彬--基于Cocos2d-x引擎开发经验分享
《大掌门》欧阳刘彬分享的内容同样是与Cocos2D-X和跨平台开发有关,在演讲中他详细分享了为什么会选择Lua的原因。 欧阳刘彬:首先感谢CocoaChina的邀请,跟大家分享一下我们《大掌门》在游戏
本次大会更多相关报道,请访问
《大掌门》欧阳刘彬分享的内容同样是与Cocos2D-X和跨平台开发有关,在演讲中他详细分享了为什么会选择Lua。
欧阳刘彬:首先感谢CocoaChina的邀请,跟大家分享一下我们《大掌门》在游戏开发过程当中使用Cocos2D所开发的一些经验。刚才凌聪讲的内容 感觉已经是一个比较完整的、系统的东西了,我们本身在刚开始做的时候,我觉得他们那边应该是有一个比较强大开发团队在下面做了一些支撑的事情。我们做的一 些事情,其实大部分他刚才已经提到了,我们做的可能不像他们那么系统,但是也有一些东西是跟他很像的一个过程,我就我们这边的一些经验来跟大家再做一些分享。
我们为什么选Cocos2D-X,其实最大的原因就是跨平台。当时为什么选择Lua呢?大概我们从去年春天的时候开始做这个事情,我去年春天也来了 CocoaChina的一个会,当时我们选一个脚本,觉得比较合适的可能还是用Lua,那时候已经出来了一段时间了。它的一些特点其实刚才也已经提到过 了,我们觉得它,支持一些动态更新,它的容错性比别的好,我们找一个靠谱的C++程序员说实话还是比较难的,我们在开发速度上会比其他的语言要快一些。
我们的一些基本情况,其实我们用Cocos2D是比较老的一个版本了,我们大概在去年3、4月份就开始做,基于上面做了一些改进,后面因为在开发的过程当 中,我们觉得一个底层的框架已经选好了之后就尽量不要去动它,因为这个说实话是一个伤筋动骨的事情,但是我们会继续用Cocos2D的框架,我们会选择 2.0的版本,老的那一套东西一直在上面做。我们做的一些事情没有那么系统,但是也做了一些。首先我们在上面做了我们的一些事件管理,做了一些底层的UI 控件,做了我们的网络通信模块和加解密的模块。我们的一个特点就是说,我们所有的游戏逻辑都是用Lua去实现的,我们在Lua里面也去实现了一些MVC的 框架等等,但是对于一些性能要求比较高的东西,比如说加解密这种东西可能还会在C上实现,在Lua里面去用新的接口。
首先我要分享的就是我们版本更新的机制,其实刚才凌聪也提到了,我们为什么要用Lua,就说它能够走游戏内更新的方式更新版本,比如说我们拿App Store来举例,你去提交一个版本,我们以往的经验,首次提交的话可能需要一个月的时间,后续的更新审核可能5个工作日,也就一周过去了。这个提交从审 核里面如果一个月过了,当然大家相当高兴了。中间的过程当中其实经常会遇到一些非技术的原因把这个打回来,比如说你的截图,或者是一些信息写的不对,中间 来回打会可能也会耽误时间,这个审核周期太长了,我们觉得是一件让人不太好接受的事情,尤其是一些非技术原因。
再有一个问题就是,像我们的游戏版本,我们游戏的安装包现在说实话,从最开始的50兆到现在变成100兆已经比较大了。假设我们去实现的话,如果我发现游戏里面一个比较重要的Bug,我去修复这个 Bug,提交之后玩家再去更新,按照现有的机制,玩家就是下一个完整的安装包下来。国内其实像凌聪刚刚提到了,在Android市场有一定的新的机制,但 是我了解在Android的4.0上面才支持这种方式,在比较低的版本上还是不支持增量的更新。
在腾讯这边确实是做得比较好,它的QQ游戏大厅是支持增量 的更新的。我刚才提到的,就是说更新一个版本,我们加一个什么玩法有一个全新的包,这种带来的问题就是每次更新都要下一个完整的包,用户的体验是非常差 的,对于用户整个留存都会有影响。我们的解决方案就是,所有的游戏逻辑都去用Lua实现,我们实现一套游戏内更新的机制,每次游戏升级的时候,基本上只要 更新自己的Lua文件就行了,这样就可以及时、随时的更新,我们不需要等任何人的审核。还有就是快速,我们走增量更新的方式,只需要去下载我们改动过的一 些资源和代码。在这里也可以插一句,刚才也人问到苹果基于这个审核的条款,在我的理解,实际上苹果只是给自己设一个底线。
如果游戏只是对于一些正常的功能 逻辑的更新,走脚本的方式的话应该还是可以的,只不过你如果把游戏整个更新一遍,变成一个完全不同的游戏,那肯定是不行的,我认为苹果这种条款,可能是作 为它自己的一个底线放在那儿。
我们整个游戏更新的过程,其实也是类似的,我们的后台服务器分为了版本管理的服务器、资源服务器、游戏逻辑服务器。首先我们的游戏启动之后,第一次会带着 自己的版本向我们的服务器版本去问,就是我现在的版本号是一,最新的版本号是多少。如果没有更新的话,后续它直接往游戏逻辑服务器发起后续的通信需求,但 是如果有更新的话,我们的版本服务器会告诉他最新的版本已经到了2.0了,会返回一些需要更新的文件列表回来,然后我们手机端就会从资源服务器把这些所有 需要的文件全部下载下来,然后在自己这里实现一些资源的重载,整个脚本的一些Reload,接下来再往我们的服务器做通信的时候,就已经变成一个最新的版 本了。
我们如果实现了这种游戏自己更新的机制的话,我们就要考虑到一个问题,就是我们客户端上面的那些代码文件和一些资源文件怎么去放。我们一个简单的想法就是 说,我们的游戏逻辑都是拿Lua实现的,我们的代码可能就是直接放一个Lua文件在手机上面。它从V1更新到V2的话,我们就从资源服务器直接下载一份最 新的Lua文件把它覆盖掉。但是这样有一个问题,我这个PPT里面其实有一些忽略,在我们的后台有一些版本的管理系统,我们对于每一个版本都有资源、后台 代码和数值的配置都是有保存的,我们随时可以从后台界面上去做切换,把我们的游戏升级到某一个版本,我们也支持游戏多版本的并存。比如说我们开发人员在测 一个版本,但是玩家玩的是另外一个版本,我们自己测试好之后再切换上线。所以我们一套东西之后,你就会考虑到一个问题,就是说假设我们测试不够充分,一个 版本放出去之后,你可能突然发现一个重大的Bug,你可能要么给它进行修复,如果很快的找不到解决办法,你只能马上把版本回复回去。第一种方式就是说,如 果客户端只放一个版本的话,我的回滚希望更快,就是说代码在之前,唯一的代码在你手上实际上是有的,希望服务器返回某一个状态,你立刻自己就能够切回到 V1的版本上去,而不需要重新把那些代码再重新Download一遍。
还有一个办法就是每个版本都通过一个版本号,比如说这是我V1的版本,我增量后续更新的时候,可能会把V1先拷贝成V2,然后再去做增量更新。比如说V1 到V2有哪些文件变了,比如有两个文件,A.Lua或者是B.Lua,我可能会实现增量的更新,但是我的V1的B点Lua和V2的B点Lua实际上是没有 变的,它们会占用额外的存储空间,会有浪费。所以我们最后的方案,也结合我们后台的那套版本管理的系统,就是实现的方案是把我们所有的资源和我们的脚本, 包括我们的数值配置,我们的脚本都算是一种资源,我们做md5重命名,通过在我们的手机端、游戏后台、资源管理服务器都会有一个数据库去管理这种每一个版 本下面所有的资源的映射关系,就是原始的文件名和最后打包之后会存下来,每一个版本都会存下来。接下来真正游戏逻辑运行起来之后,需要通过一个自定义的 Lua Loader去查找这些文件。
这是我们打包的流程,首先会打包的时候把这些文件算一个码然后进行重命名,同时会生成一个资源的数据库,接下来把这些资源的数据库和脚本文件放到这个包里 面去。然后游戏逻辑里面,比如说我们的Lua代码去装载一个文件的话,语法就是Require一个A,就会去查找A.Lua这个文件。我们现在面临的问题 就是,这个A.Lua实际上已经不存在了,我们已经重命名了,我们需要有一个机制,让Lua再去Require这个A的时候要找到一个文件,所以我们需要 自己实现一个Lua& Loader,实际上Lua那个引擎里面是有相应的方法的,我们可以在那个引擎里面注册一个自己的Loader,在它被触发之后,首先根据我们要装载的文 件,比如说A,在我们的List里面去查这个文件真实的文件名是什么。比如说我们的客户端现在是V1的版本,它去V1的里面去查到的文件是这个,那么他可 能就会最终度曲的文件就是这个文件,再最终通过Lua的Loadbuffer这个函数转进来,再走后续的流程。如果是V2的版本,就找到另外一个文件,最 终回进入到这个Loadbuffer当中来。这样的话,我们在整个游戏逻辑里面有一个变量去标识当前客户端的版本是1还是2,但是实际上他们版本1和版本 2的代码可能都会在我们的手机上有,可以很快速的去切换。像我们一个没有变化的B点Lua,就以我们刚才提到的这种方式去实现的话,他们查的话是不同的, 但是找到的文件是同一个。
接下来大家就会想到,游戏逻辑如果去用脚本实现的话,那你的代码怎么加密?像JS一般有混淆器。但是这种方式总之还是能有人去反编译的,如果我们有一些比 较核心的业务逻辑放在前端的代码里面,我们不希望被别人很轻易的看到。其实我们如果在打包流程当中已经做了这些事情,我们已经有了自己自定义的Lua Loader的话,加解密这个事情就很容易做了,我们在打包的过程当中,在重命名之前做一次加密的操作,然后在我们的游戏逻辑运行起来之后,我们再把一个 文件的内容读出来之后,再去调用Loadbuffer再去做加密就OK了。
我们用这种方式去管理脚本和资源之后,这个文件就会有一个问题,就是说我们整个代码可能有70多个Lua文件,有200多个配置文件,这些文件如果以文本 的方式去加密之后,APK的安装包和IPK的安装包都是最基础的安装包,这些文件在加密之后你再去压缩没有压抑的,因为加密之后二进制流是没有规则的,你 用Zip去压是是没有任何意义的,带来的问题就是安装包体积会变大。还有一个需要考虑的就是说,我们这种增加一个加解密的环节,对于我们游戏运行起来的时 候会有什么性能的影响?所以我们做了一些改进。就是说其实这个想法也挺简单的,就是说你加密之前先把文件压缩了再加密,这个事情直接就让这个加密对于压缩 这个影响已经完全没有了,你先压缩再加密的话,本身这个文件就已经比较小了。在我们游戏运行的时候,把它先在我们的Lua Loader里面先解密,再解压,然后再调用Loadbuffer这个函数。
我们做一些对比,刚才提到了我们的安装包如果直接使用的话,这个图(PPT)有三列,第一列是我们的原始文件,我们的文件是一个Zip格式,在安装包里面 的大小可能就是1.5M,如果我们把配置文件和脚本去做加密处理的话,然后再放到我们的安装包里面去,它就变大到8M,就是整个安装包就大了不少。但是如 果我们后面再通过第三种方式,就是先压缩再加密这种方式的话,安装包的体积可能比以前的原始文件直接放进去稍微大一点点,基本上大不了多少。所以这样的 话,压缩、加密这种对于游戏安装包的影响就已经可以完全没有了。
我们对比了一下这些脚本通过处理之后,它在游戏运行时对于性能的一些影响。因为我们的游戏逻辑运行起来的时候最开始的启动阶段可能是Require一些少 量的脚本,真正进入游戏的时候,因为我们Lua内存一些框架的实现原因,我们是需要几乎把所有的Lua代码都要装载进来,所以你一次性的去装载700多个 Lua文件的话,实际上是有一些性能的问题,实际上会使你的装载时间变长,就是在这种加密的方式下。但是对于我这一列后面列的第三种方式,就是说压缩和加 密的方式下,实际上加载的时间又会降下来。所以可能大家就会困惑,为什么我只加密装载时间变长了,但是我加了一个压缩再加密,我运行的时候先解密再解压, 装载时间反而会降下来了呢?这张图就是说明了这个原因,大家可以对比一下。我的Lua文件装载实际上是分了好几个阶段,可能有先读取文件的内容,然后有解 密,有解压缩的过程,就是三个颜色代表了。对于原始文件,如果我们直接放到安装包的话,直接读原始文件就可以了,只要是能读出来,不需要解密和解压缩,总 共1千个文件,我们脚本和配置文件夹都放进来的话。如果只加密的话,读文件装载时间可能少一点点,这个可能是测试的一些偏差了,正常来说应该是差不多的, 可能还需要另外一半的时间去做解密的操作。但是如果文件压缩之后再加密的话,在我们运行的时候先把它读出来,读出来的时间因为本身压缩之后那个文件会很 小,就是它读出来之后,第一次装载内存的Buffer是比较小的,装载的时间其实比原始的文件还要小一点,接下来解密时间也会变得小很多,因为文件的大小 也小了很多,所以解密耗的时间也会比较少。最终解压缩的时间,我们测试的时间上是很少的。
前面提到的都是我们在iOS上做的一些性能的对比。到Android上我们就发现了另外一个问题,这个图里面蓝色的线就是iOS上所有的脚本和配置文件的 加载时间,基本上就是都很小,慢的也就是2秒,快的可能也就是不到1秒的时间。跑到Android上就变得很夸张,游戏可能进去之后开始有一点转圈,然后 过了快10秒钟才会把一些界面装载进来。不管是放原始文件还是放加密的文件还是压缩加密的这种文件,我们放到APP里面之后再去直接装载的话就发现性能有 很大的问题。我们看了一下为什么会有这种问题?我们就发现在Android上面你直接从SD卡读一个文件和直接从APK里面读一个文件实际上是有相当大的 差异的。其实对于单个文件来说,如果运行时只是装载一个文件,从SD卡上读耗的时间可能是一毫秒,从APK里面去读耗的时间是十毫秒。对于一个文件来说, 其实这个时间几乎没有什么关系。但是如果你一次装载1千个文件出来的话,这个10倍的差距就会放大到比较明显的一个程度。所以针对这种情况,我们在 Android上面就增加了一个环节,就是我们游戏安装完第一次启动的时候,需要有一个解开我们资源和配置文件的过程。像iOS上就不需要这样的过程,因 为iOS上本身安装包装进去之后就是一个展开的状态,不像APK,安装完了之后就是放在手机的RAM里面。
我们对于这种展开之后,你再去放到SD卡上后面去运行我们游戏的话可以看到,一个浅蓝色的线就是从Android上,如果从SD卡去加载我们的配置文件和 脚本的话,时间就跟iOS上的加载时间差不多了,比直接从APP里面加载要好很多,刚才提到的是我们的版本管理的一些经验。接下来就是我们用Lua在开发 商利用它的一些灵活性,利用一些丰富的接口,我们去做一些辅助我们定位问题和帮助我们加快开发速度的一些方法。在调用我们Lua的时候,在Lua引擎的文 件里面,会调一个Lua的函数,或者是执行一个Lua的文件,最终的调用方式都是通过Lua Pcall这个方法,它的文档里面其实第四个字段是一个Message Handle,这个Lua的Pcall接下来如果在执行Lua代码的时候如果出错了,如果我有设置这个Messager handler的话,我会通过这个去处置我的异常。我不知道现在引擎有没有这个问题,实际上以前在Pcall的时候是没有做这种错误处理的,第四个参数都 是直接填的,如果我们自己定位一个Message Handle,我们可以运行一个详细的错误的堆栈,我们开发的时候,可以选择把这个堆栈弹放出来,开发人员可能在模拟器上调试的时候,如果出现什么错误就 可以直接把这个错误弹出来了。在我们发布之后,我们游戏毕竟不能保证百分之百没有Bug,如果运行的时候出现一些错误,我们能够把这些逻辑信息捕获,上传 到我们的日志服务器里面去。Lua还有一个好处,一般来说一个挂掉了之后,可能只是影响当时的那一次调用,一般不会让你的游戏直接崩溃掉。
比如说我们实现了一个自己的错误的Handle,这个例子就是我们去改造CCLua文件里面一个执行Global的一个例子,红色的三项是我们自己加上去 的,如果你有1G的Handle,你首先要Psuh到State上去,在最终调用的时候,-1就是你马上要执行的那个Global的指针,-2就是我们自 己的Message Handle。在这个Message Handle里面的实现,实际上主要是用到了Lua的一个方法,他们可以取得运行时的信息,就是堆栈和一些调用的脚本名称和行号什么的。这是我们通过自己 定义的Hanlle可以捕获的一些错误异常,第一行是我们Lua错误的Message,下面是Lua错误的堆栈信息。我们通过这些东西很容易看到究竟是哪 个模块,哪个文件的哪一行出了什么问题,有堆栈的话也很容易看出,为什么它会在这些行出现这样的错误。
刚才凌聪也提到了Lua跟Java跟iOS去做交互的时候有一些比较恶心的东西,他们有的通过自己的方式实现,我们也有自己的一些小的技巧。一些常用的方 式,如果我们有一些Java和Object的接口的话,我们会通过C++导出给Lua用。这样的问题,尤其是一些像我们的iOS和Java需要有一次 C++的封装其实就有开发量。再有就是这种接口比较固定的还好,比如说加解密这种可能实现一次就OK了。后来你再去做SDK介入的时候你会发现每一个渠道 都不太一样,你很难做出一个比较通用的接口暴露给Lua,如果按照之前的这种Tolua++的方式去做的话就很麻烦。比如说我在Java这一层实现打开一 个网址的功能,可能先在Java里面要实现一个OPenUrl的函数,在C++这边也要实现一个OperUrl的函数,去调用Java的代码,再用 Tolua++这个导出,再去调用这个函数。JNI调用的代码说实话,我的感觉很恶心,很容易写出Bug,如果这个字段改成份了,需要修改的参数就很大, 需要改很多地方。Java这一层要改,C++和Lua都要改。我们的技巧就是对于一些性能要求不高的封口上,我们就安装一个doAction这个函数,在 Lua层,就去调用不管是C还是Object C还是Java的接口的时候,可能就是调这个接口。我们在C里面去实现一个调度,就是在C里面,我们这个Tring里面一般会有一个Action的字段, 我们在C这里面出现相应的处理,可能也都会在这个Tring里面去做,在Java这边,首先通过JNI的调用,调到一个Java的 PlatformTool里面,我们通过这种封装之后,在Lua这一层去大概一个网址,我可能在Lua的代码里面就会这么写,首先会把我的参数,比如说 Action是UIO,需要变成一个串,然后再传给这个doAction的方法。这样的好处就是说,我的交互,基本上第一次写好这个doAction之后 就不用变了,我依然要在OC或者是Java这里去实现我的业务代码,不需要考虑在C这一层怎么做中间的角度。调用方式也变得比较一致了,接口后面如果发生 一些变化的话,修改量也会比之前的方式小很多。
上面就是我们在游戏开发过程当中对于版本更新、代码加密、错误检查和接口封装方面的一些经验。很高兴今天能在这里跟大家分享这些内容,欢迎大家能有一些技 术上的交流。我这边广告准备得不充分,我们也是最近招各种各样的人,包括技术、策划和美术,我们都欢迎大家来加入我们,去年这个时候我们可能就是一个很成 长中的团队了,我们也面临着很多的机遇和挑战,大家有兴趣的话欢迎来联系我。
主持人:技术分会场的议程到现在就全部结束了,感谢各位的光临,下次大会再见!}

我要回帖

更多关于 lua 逻辑非 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信