本篇教程将通过编写一个简单的自动采集插件来介绍剑三的“事件(Event)
”这个概念。
自动采集插件的功能就是自动采集身边的矿和草药,当人物停下来的时候,如果身边有可以采集到的矿和草,插件就是自动开始采集。
为了实现自动采集,我们的插件需要做这些工作:
1 获得身边(视野内)的采集物信息。
2 判断这些采集物是否可以采集(是否是矿/ 草?距离是否够近?)。
3 如果满足条件,则开始采集。
由于要实现自动,所以以上步骤必须不断的进行。
在开始之前,我们先来建立插件的文件结构。先在插件目录Interface 下面建立自动采集插件的目录AutoGather ,然后在AutoGather 下面建立3 个文本文件:info.ini AutoGather.lua AutoGather.ini 。
看到AutoGather.ini 了吧,这就是剑3 的窗体文件了,我们这次就要用到它啦。别被窗体这两个字吓到了,在本例中,窗体其实是来打酱油的,你基本可以无视它。
Info.ini 文件的结构我在上一篇教程中已经介绍过了,这里就不再赘述了,直接放上该文件内容:
复制代码
AutoGather.lua
和AutoGather.ini
暂时保持空白。
在文章的开头我说过,要实现自动,就要不断的循环一些动作。学过编程的同学应该知道如何实现不断的重复某些动作的方法吧?对了,就是死循环。在嵌入式编程中,一般都会看到主函数的最后有一个while(1){} ,各种需要重复执行的代码就放在里面,Windows 编程的消息循环应该也是死循环吧(我没学过win 编程,不太懂)。
但是,如果你在剑三中执行一个死循环会发上什么事情呢?你可以自己试试看,在cube 中执行一个死循环 while true do end ,点执行之后看到效果了没?是的,游戏死掉了。
为何会死掉呢?我介绍一下剑3 的Lua 执行机制你就能明白了。剑3 中Lua 脚本的执行并不是并行的,也就是说,脚本的执行并不是多任务的。剑3 的Lua 引擎是基于“帧”的方式执行脚本的,简单的说,要执行的代码是放在“帧”里面的,这一帧的代码执行完毕后才会执行下一帧,剑三的客户端一般每秒钟会执行10 帧左右(这取决与你的插件数量和CPU 速度)。
这回明白了吧?你如果把死循环代码放到一个帧里执行,那么这一帧就永远不会执行完毕,所以游戏就卡死在这一帧了。所以,大家就要注意了,剑三的Lua 脚本编写有一个原则:代码的执行流程必须是有限的并且是可以预测的,而且流程要尽量的少。这样,你的插件才不会拖慢游戏的速度。
插件装多了游戏会变慢也是这个原因。这里我吐槽一下金山的服务器,实际上,剑三的服务端程序也是基于这种帧的执行机制的(其实从剑1 开始就是这样),但是服务端和客户端不一样,它的帧速是严格的16 帧/ 秒,客户端有个函数能读取到服务端的逻辑帧( GetLogicFrameCount() ) 。正常情况下,服务器的脚本执行是没有问题的,但是到了阵营攻防的时候……尤其是双方几百人打到一起的时候,服务器就需要运算大量的数据,这时候,1/16 秒执行完一帧就有点费劲了……为了保持16 帧/ 秒,这时候服务器就会开始丢东西,于是我们就会发现[ 郭炜炜] 释放了技能[ 郭炜炜之怒] ,我们被全地图锁足……以上只是我猜的,猜错了也别喷我哦。
明白了这个帧的机制,我想很多同学就懂了:只要使我们的脚本在每一帧都执行一遍,不就可以实现无限循环了吗。于是,下面我就开始介绍它的实现方法啦。
先介绍一下OnFrameBreathe() ,其实是一个窗体事件函数,如果你在一个脚本中打开了一个窗体,并且这个窗体是可呼吸的,那么剑3 的引擎每一帧都会调用这个脚本的OnFrameBreathe() 函数。简单点说,如果在AutoGather 的脚本中定义了AutoGather.OnFrameBreathe() 函数,并且用Wnd.OpenWindow 打开了AutoGather 窗体,那么每一帧AutoGather.OnFrameBreathe() 函数都会执行一次。请注意:OnFrameBreathe()的调用频率并不是固定的,它取决于你的cpu速度以及其他因素,一般来说是每秒10次左右,但绝不是想当然的16次/秒。
为了使用OnFrameBreathe() ,我们必须构建一个窗体。别被窗体这个词吓到,其实你不需要有任何剑3 的窗体控制的知识。只需要编辑AutoGather.ini 加入如下内容就可以了:
复制代码
第一行AutoGather
是窗口名
下面的._WndType=WndFrame 表示这是一个窗体
._Parent=Lowest 这一行表明这个窗体是在最底层的
下面那一堆xxx=0 表示窗口大小为0 。也就是说,这是一个在最底层的、不可见的隐形窗口(因为我们只需要用它呼吸不需要让他露脸)。
ScriptFile=Interface\AutoGather\AutoGather.lua 这个要指向插件lua 脚本的路径(实际上 这行不写也没事)
IsCustomDragable=0 ; 禁止自定义界面拖动(shift+u 那个)
DragAreaWidth=0 ; 可拖动宽度范围0
DragAreaHeight=0 ; 可拖动高度范围0
DummyWnd=1
DisableBringToTop=1 ; 禁止移动到上层
DisableBreath=0 ; 允许呼吸
BreatheWhenHide=1 ; 在窗体隐藏后继续呼吸
把那堆东西写进AutoGather.ini 保存之后,这个窗体就创建好了,之后我们要在AutoGather.lua 里面打开它。
打开AutoGather.lua 写入这一行(注意:这一条语句最好放在lua 文件的末尾,也就是你定义的函数的后面)
复制代码
Wnd.OpenWindow
里面的两个参数应该一看就明白了吧,第一个参数是窗体文件路径,第二个参数是窗体名,也就是AutoGather.ini
的第一行那个名字。
之后就可以定义OnFrameBreathe 函数,我们在Wnd.OpenWindow 的前面定义OnFrameBreathe 函数:
复制代码
为了测试是否能正常呼吸,我们在这个函数里加入测试语句:
复制代码
于是现在AutoGather.lua
的内容是这样的:
复制代码
进入游戏后如果看到聊天栏每秒刷一行字:“我在呼吸哦”,就表明你成功了。
之后就删掉那3 行测试语句,然后继续下一步吧。
为了能采集草/ 矿也就是doodad ,我们首先需要有一个视野内doodad 的列表,但是很不幸,早期的剑三并没有提供一个能直接获得doodad 列表的函数,所以那个时候收集doodad 列表就要用到事件。当然,现在我们有了更便捷的方法,但是为了介绍事件,我先来讲解一下这个以前的笨方法。
事件这个概念肯定大家都懂,剑三在发生某些事的时候会产生一个事件,如果RegisterEvent 注册过这个事件,程序就会去调用你定义过的事件处理函数,如果这个事件带有参数的话,游戏会用arg0~arg9 这几个全局变量传递参数。
这里我们要用到2 个事件:DOODAD_ENTER_SCENE 和DOODAD_LEAVE_SCENE ,顾名思义,他们分别是doodad 进入视野和doodad 离开视野,他们使用arg0 传递doodad 的ID 。利用这2 个事件,我们定义一个列表,在doodad 进入视野以后把它加进去,在doodad 离开视野以后再删掉,就能取得视野内的doodad 列表了。
为了实现它,先定义一个表来存放doodad 列表,我们在AutoGather.lua 开头加入:
复制代码
之后用RegisterEvent
注册两个事件:
复制代码
RegisterEvent
的第一个参数是要注册的事件,第二个参数是事件发生时要调用的函数,这里放的是用function()
直接定义的简单函数,如果你的函数是在脚本中定义好的,那么第二个参数直接放函数名就可以了,记住后面不要加上()
。
例子:RegisterEvent("DOODAD_ENTER_SCENE",AutoGather.TestFunc)
于是现在AutoGather.lua 的内容是这样的:
复制代码
列表有了,就可以开始往OnFrameBreathe() 里面写采集代码啦,不过在这之前,先先给它加个开关:
复制代码
这段代码我就不解释了,看不懂就先去学好lua
吧……
之后我们继续在OnFrameBreathe 里面加料,在采集之前,显然要保证人物在站立状态并且不在读条,所以我们要加入判断,如果人物不是站立状态或者人物在读条就返回:
复制代码
下面就是采集代码了,这个代码我也不解释,这些相关函数的原型和用法都能在\ui\script\doodad.lua
中找到。
复制代码
于是最终完成的AutoGather.lua
是这样的:
复制代码
附上最终完成的插件:
AutoGather.rar (1.16 KB, 下载次数: 211)
PS: 记得我前面说过这是个笨方法吗?其实现在剑三提供了获取视野内doodad 的函数GetNearbyDoodadList() ,只需要AutoGather.DooList = GetNearbyDoodadList() 就可以获取doodad 列表。有兴趣的同学可以自己改一下,怎么改我就不说了。
另外,其实可以很容易的在这个插件的基础之上实现自动庖丁、自动采集任务物品、只采集特定物品,这些相关的代码都能在doodad.lua 里面找到,如果你能把这些功能写出来,那么你就已经是一个合格的插件作者了。
自动采集插件的功能就是自动采集身边的矿和草药,当人物停下来的时候,如果身边有可以采集到的矿和草,插件就是自动开始采集。
为了实现自动采集,我们的插件需要做这些工作:
1 获得身边(视野内)的采集物信息。
2 判断这些采集物是否可以采集(是否是矿/ 草?距离是否够近?)。
3 如果满足条件,则开始采集。
由于要实现自动,所以以上步骤必须不断的进行。
在开始之前,我们先来建立插件的文件结构。先在插件目录Interface 下面建立自动采集插件的目录AutoGather ,然后在AutoGather 下面建立3 个文本文件:info.ini AutoGather.lua AutoGather.ini 。
看到AutoGather.ini 了吧,这就是剑3 的窗体文件了,我们这次就要用到它啦。别被窗体这两个字吓到了,在本例中,窗体其实是来打酱油的,你基本可以无视它。
Info.ini 文件的结构我在上一篇教程中已经介绍过了,这里就不再赘述了,直接放上该文件内容:
- [AutoGather]
- name=自动采集
- desc=自动采集 by myself
- default=1
- version=0.5
- lua_0=Interface\AutoGather\AutoGather.lua
在文章的开头我说过,要实现自动,就要不断的循环一些动作。学过编程的同学应该知道如何实现不断的重复某些动作的方法吧?对了,就是死循环。在嵌入式编程中,一般都会看到主函数的最后有一个while(1){} ,各种需要重复执行的代码就放在里面,Windows 编程的消息循环应该也是死循环吧(我没学过win 编程,不太懂)。
但是,如果你在剑三中执行一个死循环会发上什么事情呢?你可以自己试试看,在cube 中执行一个死循环 while true do end ,点执行之后看到效果了没?是的,游戏死掉了。
为何会死掉呢?我介绍一下剑3 的Lua 执行机制你就能明白了。剑3 中Lua 脚本的执行并不是并行的,也就是说,脚本的执行并不是多任务的。剑3 的Lua 引擎是基于“帧”的方式执行脚本的,简单的说,要执行的代码是放在“帧”里面的,这一帧的代码执行完毕后才会执行下一帧,剑三的客户端一般每秒钟会执行10 帧左右(这取决与你的插件数量和CPU 速度)。
这回明白了吧?你如果把死循环代码放到一个帧里执行,那么这一帧就永远不会执行完毕,所以游戏就卡死在这一帧了。所以,大家就要注意了,剑三的Lua 脚本编写有一个原则:代码的执行流程必须是有限的并且是可以预测的,而且流程要尽量的少。这样,你的插件才不会拖慢游戏的速度。
插件装多了游戏会变慢也是这个原因。这里我吐槽一下金山的服务器,实际上,剑三的服务端程序也是基于这种帧的执行机制的(其实从剑1 开始就是这样),但是服务端和客户端不一样,它的帧速是严格的16 帧/ 秒,客户端有个函数能读取到服务端的逻辑帧( GetLogicFrameCount() ) 。正常情况下,服务器的脚本执行是没有问题的,但是到了阵营攻防的时候……尤其是双方几百人打到一起的时候,服务器就需要运算大量的数据,这时候,1/16 秒执行完一帧就有点费劲了……为了保持16 帧/ 秒,这时候服务器就会开始丢东西,于是我们就会发现[ 郭炜炜] 释放了技能[ 郭炜炜之怒] ,我们被全地图锁足……以上只是我猜的,猜错了也别喷我哦。
明白了这个帧的机制,我想很多同学就懂了:只要使我们的脚本在每一帧都执行一遍,不就可以实现无限循环了吗。于是,下面我就开始介绍它的实现方法啦。
先介绍一下OnFrameBreathe() ,其实是一个窗体事件函数,如果你在一个脚本中打开了一个窗体,并且这个窗体是可呼吸的,那么剑3 的引擎每一帧都会调用这个脚本的OnFrameBreathe() 函数。简单点说,如果在AutoGather 的脚本中定义了AutoGather.OnFrameBreathe() 函数,并且用Wnd.OpenWindow 打开了AutoGather 窗体,那么每一帧AutoGather.OnFrameBreathe() 函数都会执行一次。请注意:OnFrameBreathe()的调用频率并不是固定的,它取决于你的cpu速度以及其他因素,一般来说是每秒10次左右,但绝不是想当然的16次/秒。
为了使用OnFrameBreathe() ,我们必须构建一个窗体。别被窗体这个词吓到,其实你不需要有任何剑3 的窗体控制的知识。只需要编辑AutoGather.ini 加入如下内容就可以了:
- [AutoGather]
- ._WndType=WndFrame
- ._Parent=Lowest
- Left=0
- Top=0
- Width=0
- Height=0
- DragAreaLeft=0
- DragAreaTop=0
- DragAreaRight=0
- DragAreaBottom=0
- AnimateStartPosX=0
- AnimateStartPosY=0
- AnimateEndPosX=0
- AnimateEndPosY=0
- AnimateTimeSpace=0
- AnimateMoveSpeed=0
- ScriptFile=Interface\AutoGather\AutoGather.lua
- IsCustomDragable=0
- DragAreaWidth=0
- DragAreaHeight=0
- DummyWnd=1
- DisableBringToTop=1
- DisableBreath=0
- BreatheWhenHide=1
下面的._WndType=WndFrame 表示这是一个窗体
._Parent=Lowest 这一行表明这个窗体是在最底层的
下面那一堆xxx=0 表示窗口大小为0 。也就是说,这是一个在最底层的、不可见的隐形窗口(因为我们只需要用它呼吸不需要让他露脸)。
ScriptFile=Interface\AutoGather\AutoGather.lua 这个要指向插件lua 脚本的路径(实际上 这行不写也没事)
IsCustomDragable=0 ; 禁止自定义界面拖动(shift+u 那个)
DragAreaWidth=0 ; 可拖动宽度范围0
DragAreaHeight=0 ; 可拖动高度范围0
DummyWnd=1
DisableBringToTop=1 ; 禁止移动到上层
DisableBreath=0 ; 允许呼吸
BreatheWhenHide=1 ; 在窗体隐藏后继续呼吸
把那堆东西写进AutoGather.ini 保存之后,这个窗体就创建好了,之后我们要在AutoGather.lua 里面打开它。
打开AutoGather.lua 写入这一行(注意:这一条语句最好放在lua 文件的末尾,也就是你定义的函数的后面)
- Wnd.OpenWindow ("Interface/AutoGather/AutoGather.ini","AutoGather")
之后就可以定义OnFrameBreathe 函数,我们在Wnd.OpenWindow 的前面定义OnFrameBreathe 函数:
- function AutoGather.OnFrameBreathe()
- end
- if GetLogicFrameCount()%16==0 then
- OutputMessage("MSG_SYS","我在呼吸哦\n")
- end
- AutoGather={}
- function AutoGather.OnFrameBreathe()
- if GetLogicFrameCount()%16==0 then
- OutputMessage("MSG_SYS","我在呼吸哦\n")
- end
- end
- Wnd.OpenWindow ("Interface/AutoGather/AutoGather.ini","AutoGather")
之后就删掉那3 行测试语句,然后继续下一步吧。
小知识:我们在游戏中看到的东西,除了固定的地图之外,只有三类,分别是Player 、Npc 、Doodad 。顾名思义,Player 是玩家,Npc 是Npc 。但是Doodad 呢?doodad 这个词的字面意思是小摆设。基本上游戏中那些不会动的小物件都是doodad ,包括了各种采集物,某些桌椅板凳,甚至主城里房子上那块牌子。我们今天要采集的草和矿,就都是doodad
为了能采集草/ 矿也就是doodad ,我们首先需要有一个视野内doodad 的列表,但是很不幸,早期的剑三并没有提供一个能直接获得doodad 列表的函数,所以那个时候收集doodad 列表就要用到事件。当然,现在我们有了更便捷的方法,但是为了介绍事件,我先来讲解一下这个以前的笨方法。
事件这个概念肯定大家都懂,剑三在发生某些事的时候会产生一个事件,如果RegisterEvent 注册过这个事件,程序就会去调用你定义过的事件处理函数,如果这个事件带有参数的话,游戏会用arg0~arg9 这几个全局变量传递参数。
这里我们要用到2 个事件:DOODAD_ENTER_SCENE 和DOODAD_LEAVE_SCENE ,顾名思义,他们分别是doodad 进入视野和doodad 离开视野,他们使用arg0 传递doodad 的ID 。利用这2 个事件,我们定义一个列表,在doodad 进入视野以后把它加进去,在doodad 离开视野以后再删掉,就能取得视野内的doodad 列表了。
为了实现它,先定义一个表来存放doodad 列表,我们在AutoGather.lua 开头加入:
- AutoGather.DooList={}
- RegisterEvent("DOODAD_ENTER_SCENE",function() table.insert(AutoGather.DooList,arg0) end)
- RegisterEvent("DOODAD_LEAVE_SCENE",function()table.remove(AutoGather.DooList,arg0) end)
于是现在AutoGather.lua 的内容是这样的:
- AutoGather={}
- AutoGather.DooList={}
- function AutoGather.OnFrameBreathe()
- if GetLogicFrameCount()%32==0 then
- OutputMessage("MSG_SYS","我在呼吸哦\n")
- end
- end
- RegisterEvent("DOODAD_ENTER_SCENE",function() table.insert(AutoGather.DooList,arg0) end)
- RegisterEvent("DOODAD_LEAVE_SCENE",function()table.remove(AutoGather.DooList,arg0) end)
- Wnd.OpenWindow ("Interface/AutoGather/AutoGather.ini","AutoGather")
列表有了,就可以开始往OnFrameBreathe() 里面写采集代码啦,不过在这之前,先先给它加个开关:
- AutoGather.bOn = false
- function AutoGather.OnFrameBreathe()
- if not AutoGather.bOn then
- return
- end
- end
- Hotkey.AddBinding("AutoGather", "切换开启状态", "自动采集",
- function()
- if AutoGather.bOn then
- AutoGather.bOn = fales
- OutputMessage("MSG_SYS","自动采集关闭\n")
- else
- AutoGather.bOn = true
- OutputMessage("MSG_SYS","自动采集开启\n")
- end
- end,
- nil)
之后我们继续在OnFrameBreathe 里面加料,在采集之前,显然要保证人物在站立状态并且不在读条,所以我们要加入判断,如果人物不是站立状态或者人物在读条就返回:
- local player = GetClientPlayer()
- if not player then
- return
- end
- if player.nMoveState ~= MOVE_STATE.ON_STAND or player.GetOTActionState() ~= 0 then
- return
- end
- for _,dwID in pairs(AutoGather.DooList) do
- local doodad = GetDoodad(dwID)
- if doodad and doodad.CanDialog(player) then
- if doodad and doodad.nKind == DOODAD_KIND.CRAFT_TARGET then
- InteractDoodad(dwID)
- end
- end
- end
- AutoGather={}
- AutoGather.bOn = false
- AutoGather.DooList={}
- function AutoGather.OnFrameBreathe()
- if not AutoGather.bOn then
- return
- end
- local player = GetClientPlayer()
- if not player then
- return
- end
- if player.nMoveState ~= MOVE_STATE.ON_STAND or player.GetOTActionState() ~= 0 then
- return
- end
-
- for _,dwID in pairs(AutoGather.DooList) do
- local doodad = GetDoodad(dwID)
- if doodad and doodad.CanDialog(player) then
- if doodad and doodad.nKind == DOODAD_KIND.CRAFT_TARGET then
- InteractDoodad(dwID)
- end
- end
- end
-
- end
- RegisterEvent("DOODAD_ENTER_SCENE", function() table.insert(AutoGather.DooList,arg0) end)
- RegisterEvent("DOODAD_LEAVE_SCENE",function() table.remove(AutoGather.DooList,arg0) end)
- Wnd.OpenWindow("Interface/AutoGather/AutoGather.ini","AutoGather")
- Hotkey.AddBinding("AutoGather", "切换开启状态", "自动采集",
- function()
- if AutoGather.bOn then
- AutoGather.bOn = fales
- OutputMessage("MSG_SYS","自动采集关闭\n")
- else
- AutoGather.bOn = true
- OutputMessage("MSG_SYS","自动采集开启\n")
- end
- end,
- nil)

PS: 记得我前面说过这是个笨方法吗?其实现在剑三提供了获取视野内doodad 的函数GetNearbyDoodadList() ,只需要AutoGather.DooList = GetNearbyDoodadList() 就可以获取doodad 列表。有兴趣的同学可以自己改一下,怎么改我就不说了。
另外,其实可以很容易的在这个插件的基础之上实现自动庖丁、自动采集任务物品、只采集特定物品,这些相关的代码都能在doodad.lua 里面找到,如果你能把这些功能写出来,那么你就已经是一个合格的插件作者了。