饥荒分为单机版和联机版,一般来说,内容相同的MOD,联机版相对单机版来说,总是要多一些对网络数据的处理,总归是要复杂一些。越复杂的MOD,从单机移植到联机就越困难。
本文主要讲述单机和联机的差异,以及从单机转入联机时,应该注意的东西。
主要资料来自Klei官方论坛的长篇教程:[Guide] Getting started with modding DST (and some general tips for DS as well)
原作者是rezecib,当然,也结合了自己做MOD积累的经验。
相同的部分
相通之处和细节差异
- 调用美术资源的方式,都是使用assets表和Asset函数。不过在联机版里,如果你要使用自定义的美术资源,还需要在在modinfo里添加语句all_clients_require_mod = true,这条语句会告诉系统要求所有加入房间的人都下载这个MOD。
- 大部分在单机里可用的MOD API 如AddPrefabPostInit,AddComponentsPostInit等,用法都是不变的。有一点需要特别注意的是,AddSimPostInit,这个函数在单机版里常常被用于在游戏开始之后对玩家进行修改(比如说在游戏开始之后,在玩家附近生成一个专属宠物),这个函数在联机版里不能这样用了,因为当它被执行的时候,还没有玩家生成,所以你应该根据情况,选择新的方法了。如果你想修改UI,用AddClassPostConstruct对playerhud或contraols这两个widget进行修改。
- 不要再使用AddSimPostInit来修改UI了,在单机时这样做可能仅仅是不太合适,但在联机里很可能会造成游戏崩溃。如果要修改UI,请使用上面提到的AddClassPostConstruct。
- 添加新动作,单机和联机有些许的不同,具体请参考我这个博客里的另一篇关于如何添加新动作的教程。
让你的MOD在两个游戏中都能运行
一般来说,不推荐在两个游戏中运行同一个MOD,这不方便维护,我建议针对两个游戏发布各自的版本。不过如果你非要这么做的话,也是可以的。方法是使用TheSim:GetGameID()来区分两个游戏要执行的不同的代码块。
在联机版中,TheSim:GetGameID()的返回值是"DST",可以用TheSim:GetGameID() == "DST"这个语句来判断当前运行MOD的游戏是单机版还是联机版。
此外,你还需要在modinfo添加mod api的版本号。单机的api版本为6,联机则为10。写法就是如下:api_version = 6;api_version_dst = 10 。
新的东西
变化
GetWorld()和GetPlayer()
在单机版中存在的两个重要而且常用的全局函数——GetWorld()获取世界和GetPlayer()获取玩家,在联机版中,被改为了全局变量TheWorld和ThePlayer,在联机版中,直接调用这两个变量就可以,不再需要运行函数了。需要注意的是联机版是多人游戏,直接用ThePlayer可能会对其他玩家造成误操作。所以你在改单机为联机时,不可直接将GetPlayer()改为ThePlayer就完事了,而是要仔细分析每个GetPlayer()的使用情景来决定怎样替换。
TheNet
--
联机版新增了一个网络类:TheNet,用于处理各种与网络相关的内容,常用的函数有:
TheNet:GetIsServer() -- 判断是否是主机(创建游戏者)
TheNet:GetIsClient() -- 判断是否是客机(加入游戏者)
TheNet:IsDedicated() -- 判断是否是服务器
TheNet:Announce(message) -- 发送服务器公告,典型例子是XX死于XXX
TheNet:Say(message, whisper) -- 在聊天框里显示信息,如果whisper的值为true,则这个消息只会被附近的人看到
另外,有些人喜欢用TheWorld.ismastersim来代替TheNet:GetIsServer() (还有"not TheWorld.ismastersim" 代替TheNet:GetIsClient()),这在大多数时候没有错,不过碰上某些时候,TheWorld还没创建的情况,就会产生报错,因此还是建议使用TheNet:GetIsServer()。
TheNet类当然也有其它的一些函数,不过最常用的还是上面介绍的那几个,如果想要了解更多,可以查阅游戏目录\data\sripts\screen下的playerstatusscreen.lua找到其他的一些函数,不过这些函数对MOD而言并没有什么卵用(TheNet类不是用lua写的,所以找不到函数的定义,只能看到使用它们的例子)。
modinfo
联机版的modinfo,相比单机版,添加了一些新的内容。
api_version = 10 --api版本,联机版目前为10,如果不填10,则会被提示mod已经过期。
api_version_dst = 10 -- 这个是可选的,如果你想要让这个MOD同时在单机和联机上都能运行,则需要添加这句和下面的那句,并把上面那句的赋值改为6,如果只打算在联机版运行,可不写此句和下面一句。
dst_compatible = true --此值为true则表明MOD兼容联机版
all_clients_require_mod = true--让客机知道自己是否需要订阅这个MOD并下载下来,通常,有自定义美术资源的,都需要令此项为true。
client_only_mod = false--这一项和上一项正好相反,如果此项为true,则这是一个客机MOD,可以在游戏mod界面启动。否则,需要在开启房间的界面启动。
server_filter_tags = {"character", "rog", "reign of giants"}--服务器过滤标签表,主要用于玩家搜索房间。一个房间如果开启了这个mod,就会加入这个mod标签表里的标签。
prefab
联机版的prefab,相比单机版,要多一些额外的关于网络处理的部分。
- AddNetwork
inst:AddNetwork()
当你添加上面这条语句时,就会让这个prefab能被所有人看见(如果它本身是可见的)。否则,只能被创建这个物品的电脑看到。比如说,几何种植显示的那些方格,其实也是prefab,但没有添加上面这条语句,所以只能被种植者本人看到,不会被其他玩家看到。
- 主机判断
if not TheWorld.ismastersim then
return inst
end
这个if判断块在几乎所有的prefab中都十分重要,代码的含义是,如果这段代码运行在客机上,那么就到这里就结束prefab的初始化了。
因为在联机里,客机上存在的组件是极少的。包括物品栏组件inventoryitem,人物三围hunger,sanity,health在内的众多组件都是不存在的,所以如果不上面的那段代码,客机就会运行执行后面的代码,但由于相应的组件不存在,就一定会造成客机游戏崩溃,这也是单机转联机的人普遍会遇到的第一个崩溃问题。相应的,Tag,以及其他会被所有玩家看到的内容,比如说动画表现(AnimState),形态(Transform),人物说话的文字(talker)等,就必须加在上面这段代码之前,否则客机就看不到相应的界面表现了。
另外,对联机版的人物,初始化函数分成了两部分,一部分是common_postinit,另一部分是master_postinit。前者是会在主机和客机上都执行的代码,后者则只在主机上执行。上面的if判断已经内置在了MakePlayerCharacter函数中,所以无需再添加。
- SetPristine
inst.entity:SetPristine()
这条语句的意思是这条语句执行之前的内容都是运行在主机和客机上的,然后再往下的内容就不要再动用网络了。理论上可以省一点网络带宽,然而这东西很难感觉得出来,也很难做测试,权当没啥卵用吧,一般紧邻着上面的if判断写。写在之前还是之后无所谓。
prefab新内容的实例代码释义
下面给出联机版长矛的初始化函数的代码,参照着上面讲解的内容,很容易就能理解你需要为一个联机版的prefab额外添加哪些东西。
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddNetwork() --添加联网组件,要想让这个prefab被其他人看到,就必须添加这句,相反的,如果不希望被看到,比如说建筑几何学的网格,就不要加这句。
MakeInventoryPhysics(inst)
inst.AnimState:SetBank("spear")
inst.AnimState:SetBuild("spear")
inst.AnimState:PlayAnimation("idle")
if not TheWorld.ismastersim then return inst end --这个if判断区分了客机和主机
inst.entity:SetPristine()--节约一点网络带宽
inst:AddComponent("weapon")
inst.components.weapon:SetDamage(TUNING.SPEAR_DAMAGE)
inst:AddComponent("finiteuses")
inst.components.finiteuses:SetMaxUses(TUNING.SPEAR_USES)
inst.components.finiteuses:SetUses(TUNING.SPEAR_USES)
inst.components.finiteuses:SetOnFinished(inst.Remove)
inst:AddComponent("inspectable")
inst:AddComponent("inventoryitem")
inst:AddComponent("equippable")
inst.components.equippable:SetOnEquip(onequip)
inst.components.equippable:SetOnUnequip(onunequip)
MakeHauntableLaunch(inst)
end
主客机数据交互
如果你想要做复杂的,深入游戏核心的联机版MOD,主客机的数据交互几乎是不可能绕过去的。如果处理得不好,造成主客机数据不同步,轻则无法顺利实现自己想要的功能,重则直接崩掉服务器(有一段时间,精灵公主联机版不稳定,经常崩服务器,就是因为没有处理好这个问题。如果你想要做复杂的MOD,又想保证一定的可靠性,那么就必须要深入理解这一段的知识。
基本机制
联机版的主机,就和单机版一样,有着几乎所有的prefab,component,stategraph和brain。但在客机上,prefab依然存在,但它们只会运行其中一小部分代码(在TheWorld.ismastersim的判断块之前的代码)。这一小部分代码,只会给prefab添加少量与界面交互有关的组件,比如动画、图片或者文字表现,或者是直接关系者游戏操作的,比如:talker,transparentonsanity,playercontroller, 和playeractionpicker。brain和stategraph都只存在于主机上。相应的,为了使得大多数组件也能在客机上流程地运转,游戏在客机上使用了replica和classified(为什么不直接使用组件?因为这会造成数据的歧义性。比如说一个人物,吃了食物之后,会加饱食度。吃东西这个行为是发生在客机上的,如果客机有Hunger组件,那么调用客机的hunger组件,增加饱食度,那么在客机上,这个人物的饱食就变化了,但主机上的饱食并没有变化,两者的数据就不一致了。为了解决这个问题,就需要把数据都放在主机上,客机只负责接收来自主机的更新数据。)
主客机之间的数据交换,则是通过一个双重系统——remote procedure call(RPC)和netvar来操作。客机通过发送RPC码向主机发出要求。主机则改变netvar的值来向客机传递数据(大多数被储存在了classified中)
Replica
Replica是component的副件,与component不同,不管是主机还客机,replica都是必定会存在的,replica的主要用途就是帮助客机玩家流畅地完成原本component要完成的操作,但这些操作通常都只是游戏界面的变化,比如说播放动画,显示文字之类的,较少涉及到与主机的数据交换(数据交换的工作,主要由classified完成)。在主机中调用component的函数,如果在replica中存在同名函数,也会被同时调用。利用这个特性,可以在同名函数的方法体中。对主机,执行更新客机数据的代码,对客机,执行动画之类的操作(如果有必要的话)。(对主客机执行不同的代码,可以用上面提到的TheWorld.ismastersim这个变量来区分,或者用TheNet:IsServer()这个函数。)
如果你想为自己自定义的新组件设置replica的话,只需要两步便可完成。
1、在components文件夹下,新建一个文件,文件名为"组件名_replica.lua",文件里的定义格式同一般的组件,内容则自行决定。
2、在modmain中,添加一行代码AddReplicableComponent("组件名")
这样一来,你便有了一个自定义的replica。
具体的实例,请参看我的联机版samansha,里面有一个sa_car的replica。如果觉得看不懂,可以在评论区回复,我会考虑专门写一个教学用的实例mod。
Netvar&Classified
Classified和netvar的联系非常紧密,要解释Classified就需要解释netvar,这里就一并讲了。
Netvar,正如其名,是网络变量的意思。Netvar是官方设计用来从主机向客机传递数据的一组数据类型,它们的定义和用法,在netvar.lua里有官方的说明。官方也写过一个教程并配上了相应的示例MOD。不过我觉得官方写得并不够清晰,让我当初学习的时候走了不少弯路。这里按照我自己的理解来写一下,希望能帮到各位读者。
- 声明网络变量
对于有C或java之类强类型语言基础的读者来说,都很清楚声明变量时必须要声明数据类型。netvar也一样。netvar有多种数据类型以供不同规模的数据传输需要,比如net_bool-1位(true,false),net_tinybyte-3位(0-7)等等,具体可以在netvar中查到。在实际使用中,为了节约带宽,我们应当尽量选小数据类型来用。声明一个网络变量的格式为:ReferenceName = NetvarType(entity.GUID, "UniqueName", "DirtyEvent"
。ReferenceName是引用名,NetvarType就是网络变量的数据类型,entity.GUID就是这个网络变量要依附的entity的GUID,一般来说,这个网络变量是谁的,就依附谁。比如说你把这个网络变量写在了一个人物的初始化函数里,那么这个entiy就是inst。UniqueName是唯一名,只需要保证你的网络变量不与其它网络变量重名就行,DirtyEvent是当这个网络变量的数据发生改变时,会触发的事件,一般称为这个网络变量的dirty事件。Dirty事件会在主机和客机上都触发,客机可以利用这一个事件来改变HUD的状态,对做自定义的UI非常有用。
一个示例:inst.level = net_smallbyte(inst.GUID,"MyLevel","leveldirty")
另外,网络变量必须要在主机和客机中都有声明,也就是说,如果你想给人物添加一个网络变量,必须要写在common_init函数里。写在其它prefab里,则要在TheWorld.ismastersim的判断语句块之前写。如果要写在组件里,则必须要保证组件也在客机上存在(否则你应该写在replica里)。
- 使用网络变量
网络变量不可直接赋值,也不可直接读取,需要调用函数来完成。有三个函数可以用,分别是set,value和set_local,具体用法如下,
netvar:set(x)--只能在主机端调用这个函数,会自动同步客机的数据(在一个新的同步周期开始时)。如果这个函数确实改变了netvar的值,会在主机和客机上都触发相应的dirty事件。
netvar:value()--可以在主机和客机上调用这个函数,读取当前网络变量的值。
netvar:set_local(x)--可以在主机或客机上调用,改变相应的值但不触发数据同步或dirty事件。但当主机下一次调用set函数时,无论变量的值是否发生了改变,都会同步一次数据。
同样的,我的联机版samansha也包含了网络变量,仍然是那个sa_car组件及其replica。我主要用到的是dirty事件,对于数据传输,因为没有使用需求,就没有写。不过我会考虑以后加上一个示例的MOD。
Netvar就讲解这么多。下面简单来说一下Classified。Classified,实质上是一个prefab。这个prefab,根据使用的需求,打包了一些联系比较紧密的网络变量,并为他们设置一些共用的函数,以方便被调用(主要是被replica调用)。Classified有好几个,和人物联系紧密的是player_classified,其中包含了关联着饥饿、精神、血量等变量的网络变量。这里不详细展开。对于Classified,主要的用途是,当我们想让客机获取诸如饥饿之类的属性,进行一些与主机无关的操作时,去调取player_classified里的关联hunger的网络变量。但实际上这个应用的范围很小,因为要用到数据,又与主机无关的,一般就剩下控制HUD表现了。大多数时候,我们想要的是,在客机上,呼叫主机执行某一段代码。比如说呼叫主机让hunger减少50点。这个就属于客机向主机发起沟通的范畴了,而这就需要使用RPC。
RPC
RPC是Remote Procedure Call的缩写,意思就是远程程序调用。这里不需要了解其内在机理。只需要知道它是客机用来向主机发送执行代码信号的工具就行了。重点在于学习如何使用它。
RPC是不能随意发送整一段的代码给主机的,它只能发送一条简短的RPC消息,然后主机根据这条消息,寻找相应的RPC处理器,如果找得到的话,就会执行处理器预先设置的代码块。所以,我们想要使用RPC,就需要先(在主机)用AddModRPCHandler函数添加一个RPC处理器,然后在需要的时候,执行SendModRPCToServer来发送一条RPC消息给主机。
我们通过一个例子来学习:
local function GrowGiant(player)
player.Transform:SetScale(2,2,2)
end
AddModRPCHandler(modname, RPCname, GrowGiant)--添加RPC处理器,这个语句可以写在任何会被主机执行到的地方。三个参数,第一个为命名空间的名字,建议写mod的名字;第二个参数为RPC的名字,必须是唯一的,如果有同名的,就会根据先后顺序,被后面的覆盖。这两个参数都必须是字符串。第三个参数则是要执行的函数,这个函数的第一个参数固定为玩家的引用,这里的玩家,指的是执行下面的Send
local function SendGrowGiantRPC()
SendModRPCToServer(MOD_RPC[modname]["RPCname"])--[[向主机发送RPC消息,第一个参数为MOD_RPC[modname]["RPCname"],MOD_RPC是不能修改的,后面的modname,RPCname就和上面的意思是一样的。另外,这个函数可以传入更多的参数,只要写在第一个参数后面就行。这些参数讲会被上面的AddModRPCHandler里的执行函数接收到。在某些时候会非常有用。
end
GLOBAL.TheInput:AddKeyDownHandler(118, SendGrowGiantRPC)--按v键执行上面定义的SendGrowGiantRPC函数。除了这里通过按键来触发外,我们还可以根据情况,灵活调用SendModRPCToServer来达到不同的目的。
同样的,我的联机版samansha的sa_car组件,也展示了改如何使用RPC,也是可以参考的对象。
网友评论