您的位置:首页 > 编程语言 > Lua

Dorothy Lua开发建议

2016-03-23 15:36 441 查看

1.禁用全局变量

可以在zbstudio中使用Analyze(Shift+F7)功能检查全局变量的使用。但是可以通过模块做数据的共享,但是只能共享基本数据类型包括string,number,boolean,table,没有俘获userdata类型变量的function等,共享的table中也不能存有userdata。共享数据的模块写法如下:

--fileSharedModule.lua
localnode=CCNode()--userdata
localdata=
{
sharedNumber=998,--ok
sharedString="ABC",--ok
add=function(a,b)--ok
returna+b
end,

--[[
getModuleNode=function()--wrong
returnnode--userdata
end,

sharedNode=node,--wrong
]]
}

returndata

--filemain.lua
localSharedModule=require("SharedModule")

print(SharedModule.sharedNumber)
print(SharedModule.sharedString)
print(SharedModule.add(1,2))


禁用全局变量理由

因为全局变量的储存空间其实是一个可以被在任意位置替换掉的table,而非一个真正全局有效的空间范围。如下所示:

--filemoduleA.lua
--indefaultglobaltableenvironment
myGlobalValueA=998--setvariableindefaultglobaltable

--filemoduleB.lua
Dorothy()--thisfunctionalterdefaultglobaltabletoDorothy`sglobaltable(usedsetfenv(2,{})inside)
require("moduleA")
myGlobalValueB="pig"--setvariableinDorothy`sglobaltable
print(myGlobalValueA)--willoutputnil

--filemain.lua
--indefaultglobaltableenvironmentagain
require("moduleB")
print(myGlobalValueB)--willoutputnil


此外,如果自由使用全局变量,全局变量和局部变量可能的冲突问题,可能会导致比较隐蔽的程序错误,并且不容易排查(js也有类似问题)。

共享数据写法说明

模块return返回的内容,当使用require("module")时,会把这些内容缓存在package.loaded表中,可以使用(fork,vinpairs(package.loaded)doprint(k,v)end)查看其内容。

当返回内容为userdata时,Dorothy的Luauserdata对象会引用对应的C++对象,所以缓存userdata对象会导致C++对象无法得到释放,由于C++对象系统与Lua独立的引用系统,可能会导致C++系统中产生一系列的内存泄露。

所以Lua中若要缓存userdata时则需谨慎处理。在userdata不再被使用后,需要手动清理掉对它的引用,防止内存泄露。

2.禁止模块返回创建的对象

用返回对象的创建函数来取代直接返回新创建的对象。示例如下:

--fileMyGameScene.lua
localfunctionMyGameScene()
localscene=CCScene()
--codestocreatemygamescene
returnscene
end

returnMyGameScene--ok

--filemain.lua
localMyGameScene=require("MyGameScene")
localscene=MyGameScene()
CCDirector:run(scene)--ok

错误示例:
--fileMyGameScene.lua
localscene=CCScene()
--codestocreatemygamescene
returnscene--returninstance,wrong

--filemain.lua
localscene=require("MyGameScene")--wrong
CCDirector:run(scene)
--memoryleakwillhappensafterCCDirector:replaceScene(anotherScene)


禁止模块返回创建的对象的理由

同用模块共享数据禁止共享userdata的理由,避免userdata中的C++对象被一起缓存。

3.正确地添加对象属性和方法

Lua中是使用table数据结构来做定义和操作的对象的,比如新建一个person对象,并添加相关的属性和方法:

localperson={}--tableobject
person.name="Pig"
person.walk=function(self)
print(self.name.."iswalking")
end

person:walk()


Dorothy-Lua中的对象是userdata,但是也可以像table一样添加属性和方法:


localnode=CCNode()
node.name="Pig"
node.walk=function(self)
print(self.name.."iswalking")
end

node:walk()




给Dorothy中的对象添加属性和方法有一个重要的规则就是,不应与对象自带的属性或是方法重名。比如下面的例子里:

localnode=CCNode()

node:addChild(CCNode())--invokeC++bindingmethod'addChild'fromCCNode

node.addChild=function(self,node)--assignactiontoalterfunction
print("override")
end
node:addChild(CCNode())--invokethenewmethodandprintoverride

node.anchor=function(self,anchor)--raiseerror,'anchor'isaCCNodeproperty,
--onlyacceptvalueofoVec2type,
--andsetterfunctioninvokedherewhichcauseserror
print("can`toverride")
end


上面的anchor是CCNode的一个oVec2类型的属性,它的赋值行为,会触发内部setter函数执行数据类型验证,然后调用底层C++对象的操作。上面的addChild是CCNode中一个由C++绑定而来的方法,它的赋值行为将会覆盖该node对象在Lua中调用该C++方法的接口,并且把这个接口覆盖成自定的一个lua函数,但是底层C++中的addChild方法并不会因此被实际替换掉。所以根据上述情况,给Dorothy中的一个对象添加属性和方法,如果不是特殊需要,添加的属性不应与自带的属性和方法名称重名。

4.维护对自定义对象的引用

自定义对象包括自己用table创建的新对象,或是给Dorothy对象添加属性而来的对象。在Lua中,没有被引用的对象会被清理掉是很自然的事。但是对于Dorothy的对象,这个概念稍有点复杂。比如下面的代码:


Dorothy()

localfunctionf()
localnode=CCNode()--localluaitemnotreferenced
node.tag=998--settagpropertyofCCNodeclass
node.flag=233--addnewfieldtonodeobject
localscene=CCScene()
scene:addChild(node)--nodeisreferencedinC++system
CCDirector:run(scene)
end

f()
collectgarbage()--clearunreferencedlocalluaitems

print(CCDirector.currentScene.children[1].tag)--gettheproperty,print998
print(CCDirector.currentScene.children[1].flag)--lostnewfield,printnil


会发现其中的node对象作为Lua对象,没有在Lua中被引用时会被销毁,包括对象上添加的属性。但是node作为游戏底层的C++对象,会被保持引用而一直存在,当再次获取这个对象的时候,会被创建为新的Lua对象而重新在Lua中被使用。就是说,一个Dorothy对象包含C++部分和Lua部分,如果我们要新建一个有自定义方法和属性的Dorothy对象,为了避免自定义的方法和属性被销毁,我们需要在Lua代码中保持对这个对象的引用。比如把上述代码改成这样:


Dorothy()

localfunctionf()
localnode=CCNode()
node.tag=998--settagpropertyofCCNodeclass
node.flag=233--addnewfieldtonodeobject
CCDirector.node=node--luaitemreferencedbyglobalobjectCCDirector
localscene=CCScene()
scene:addChild(node)--nodeisreferencedinC++system
CCDirector:run(scene)
end

f()
collectgarbage()--clearunreferencedlocalluaitems

print(CCDirector.currentScene.children[1].tag)--print998
print(CCDirector.currentScene.children[1].flag)--print233


5.使用消息系统做模块间的通讯

为了减少模块之间的耦合度,非组合关系的模块使用消息系统做通讯。示例:

--fileCandle.lua
...
localfunctionCandle()
localcandle=CCNode()
candle.color=ccColor3(0x000000)
...
candle.light=function(self)
self.color=ccColor3(0xffffff)
end
returncandle
end

returnCandle

--fileCake.lua
localCandle=require("Candle")
...
localfunctionCake()
localcake=CCNode()
...
localcandle=Candle()
cake.candle=candle--usecomposition
cake:addChild(candle)

cake:gslot("Cake.Light",function()--registerlistenerforlightevent
cake.candle:light()
end)
returncake
end

returnCake

--fileGameUI.lua
...
localbutton=oButton("Clicktolightacake",function()
emit("Cake.Light")--sendlightevent
end)
menu:addChild(button)
...

--filemain.lua
...
localCake=require("Cake")
localGameUI=require("GameUI")

localcake=Cake()
scene:addChild(cake)

localui=GameUI()
scene:addChild(ui)


用消息系统做模块通讯的说明

上面示例中的Candle类和Cake类是组合关系的类,所以Cake模块可以直接依赖于Candle模块。但是GameUI类和Cake类只是很弱的关联关系,所以他们之间更适于使用消息系统来做通讯,而避免建立直接的依赖或是对象引用的关系。Cake给外部调用的接口通过使用oListener()监听消息的方式来提供。需要调用Cake的接口时,通过oEvent:send()来发送消息触发Cake的接口功能。使用消息系统的好处是,可以极大的降低模块之间的耦合程度,把模块做得非常内聚,系统更容易扩展,模块代码也更容易进行复用,也方便进行模块的测试。

消息系统的使用,用类似C#委托的方式也可以实现,但用委托的缺点是,当模块之间嵌套的层级比较多的时候,我们会需要把事件逐级地进行传递。特别是在UI的模块容易遇到这种情况,比如下面这个例子:

--fileMenu.lua
...
menu.menuClicked=nil

localbutton=Button()
button.clicked=function()
ifmenu.menuClickedthen
menu.menuClicked()--buttonclickeventtriggermenuclickevent
end
end
menu:addChild(button)
returnmenu
...

--filePanel.lua
...
panel.panelClicked=nil

localmenu=Menu()
menu.menuClicked=function()
ifpanel.panelClickedthen
panel.panelClicked()--menuclickeventtriggerpanelclickevent
end
end
panel:addChild(menu)
returnpanel
...

--filemain.lua
...
localpanel=Panel()
panel.panelClicked=function()
print("panelclicked!")--butsometimeswhatwereallywantisjustapanelclickeventwhenweclickonabuttonfromthepanel
end
scene:addChild(panel)
CCDirector:run(scene)
...


因为模块的嵌套和每个模块内聚性的需要,我们只好逐级给每个模块定义自己范围的事件,然后通过外部的连接代码把事件传递出来。如果直接写跨层级的事件传递,可能就会破坏模块的封装性。硬要跨模块层级去使用事件的话,就得向上级模块暴露底层模块的细节,比如像这样:

--filemain.lua
...
localpanel=Panel()
panel.menu.button.clicked=function()--exposesubmoduledetails
print("panelclicked!")
end
scene:addChild(panel)
CCDirector:run(scene)
...


代码耦合度也提高了,没有达到很好的封装效果。

而实际上,当我们在写游戏的时候,往往很少复用同一段业务逻辑的代码,很多创建型的代码在整个游戏中也只是一次性执行的代码。同一个游戏的各种UI界面往往也没有固定的模式。如果费很大的力气去总结和抽象出多种场景适用的界面模块来复用,效果可能会很差。倒不如准备几套不同交互方式的界面基础代码模版,然后写新界面的时候,复制粘贴模版代码作为基础,然后再开始进行改造就行了。因为脚本代码本身就是一种游戏数据,本来就该设计为应对最多变的业务逻辑来使用。本来就是用来应对变化的东西,应对不变的模式化的内容就不适合脚本来做了。所以我认为把脚本当写框架的编程语言来用并不能发挥它的最大的作用,需要写成框架的逻辑一般更应该放到底层的语言来写。

所以,在Lua中使用消息系统的终极目标,就是为了不用去做刻意的框架设计和封装,同时也能降低模块的耦合度。

之前UI界面用消息系统来写的话,就要这样写:

--fileMenu.lua
...
localbutton=Button()
button.clicked=function()
emit("EventToPrint")--justfocusonbusiness
end
menu:addChild(button)
returnmenu
...

--filePanel.lua
...
localmenu=Menu()
panel:addChild(menu)
returnpanel
...

--filemain.lua
...
localpanel=Panel()
scene:addChild(panel)
scene:slot("EventToPrint",function()
print("panelclicked!")
end)
CCDirector:run(scene)
...


我们把注意的焦点完全只放在代码的业务逻辑上。在这段代码中,点击按钮就是为了调用一段打印文字的代码。所以直接使用消息系统把两个模块中的逻辑连接起来就简单完事,然后其他设计方面的工作就不用特别去在意了。接着可以在Panel.lua模块的最开始写一个说明性的注释,可以写上该模块将要监听和发送的消息定义,供该模块的其他使用者进行了解和使用。

6.尽量使用require加载模块

Lua的几个代码加载的函数的关系是这样的:


localresult=dofile(filename)
等价于
localfunc=loadfile(filename)
localresult=func()

localmodule=require(moduleName)
等价于
localmoduleFunc=loadfile(moduleName)
localmodule=moduleFunc()
package.loaded[moduleName]=moduleortrue




用loadfile每次都会重新加载Lua代码,用require会在加载执行后将结果做缓存,或是记录下该模块已经被加载过的信息。所以为了避免模块代码被重复加载应该使用require,只有当需要手动控制代码加载细节的时候才使用loadfile或是dofile。

7.延迟加载和及时清理模块

当我们的模块依赖关系很强的时候,可能会在每个模块的开始使用require把其它模块加载进来,就像是这样:

--filemoduleA.lua
...

--filemoduleB.lua
localmoduleA=require("moduleA")
...

--filemoduleC.lua
localmoduleA=require("moduleA")
...

--filemoduleD.lua
localmoduleB=require("moduleB")
localmoduleC=require("moduleC")
...

--filemain.lua
localmoduleD=require("moduleD")
...


在这样较强的依赖关系下,在main运行的开始,执行
localmoduleD
=require("moduleD")
这一行代码的同时,所有在依赖链上的模块就要同时也完成加载,然后后续的逻辑代码才能执行。这样带来的问题是当项目代码量很大的时候,较强的模块依赖关系可能会导致大量的代码在一瞬间同时全部被加载。而事实上项目中的很多模块代码可能要随着程序的运行,在后期才被执行和调用。这样的预先加载除了满足模块的依赖关系以外没有实际的作用。

一种解决办法是把模块的引入代码放到代码逻辑中,比如这样来组织代码,由普通写法:

--filemoduleX.lua
localmoduleA=require("moduleA")

localfunctionmoduleX()
...
moduleA:invoke()
...
end


改写成

--filemoduleX.lua

localfunctionmoduleX()
...
localmoduleA=require("moduleA")
moduleA:invoke()
...
end


但是这样的写法会让代码的组织稍微变得更复杂。另一个升级版的办法是在设计时尽量降低模块之间的依赖度,然后手动控制模块的加载,比如这样:

--filemoduleA.lua
localfunctionmoduleA()
...
node:gslot("EventA",function()
...
end)
...
returnnode
end
returnmoduleA

--filemoduleB.lua
localfunctionmoduleB()
...
node.func=function(self)
emit("EventA")
emit("EventC",998)
end
...
returnnode
end
returnmoduleB

--filemoduleC.lua
localfunctionmoduleC()
...
node:gslot("EventC",function(n)
...
end)
...
returnnode
end
returnmoduleC

--filemain.lua
localmodules=
{
"moduleA",
"moduleB",
"moduleC",
}

run(function()--controlhowmodulesloadinaroutine
localscene=CCScene()
fori=1,#modulesdo
localmodule=require(modules[i])
coroutine.yield()--provideonegameupdatetoloadmodule
localnode=module()
scene:addChild(node)
coroutine.yield()--provideonegameupdatetocreatenode
end
end)
...


上面的代码使用消息系统拆解了模块之间依赖,使模块之间不再需要互相require,并在main中经过多个游戏更新逐步加载和执行模块内容。用协程做逐步加载,将Lua代码的加载时间分摊到多个游戏帧中。

适时地清理已加载的游戏模块也是必要的,比如做了一个闯关的RPG游戏,每进入一个关卡的时候可以加载该关卡的模块,当过关以后,为之前关卡加载的代码就没用了。要清理这些无效的模块,唯一要手动处理的部分是模块使用require加载以后在package.loaded表里缓存的内容。package.loaded是一个全局空间所以不会自动释放。简单用package.loaded[ModuleName]=nil就可以了。

原文地址:http://www.luvfight.me/dorothy-lua-programming-tip/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: