饥荒源码
位置:游戏安装目录/data/databundles/scripts.zip
饥荒中lua版本: 5.1
饥荒中的require
和lua 中是一致的,经过个人实验,在mod中增加和饥荒源码中一致的模块,并require,返回的是饥荒自带的模块,而非我mod中的
scripts目录
- behavious: 行为,给brains目录里定义的类使用的
- brains: 大脑,可以理解为AI,定义各种生物的默认行为的,比如猪人闲逛,看见东西就吃,看见蜘蛛就打等
- cameras: 摄像头,引擎上的东西,不用管它
- components: 各种各样的组件,如果想写大型mod,最好把这里面每个组件都看一遍,学习一下klei怎么定义组件的
- languages: 各国语言翻译
- map: 游戏中的地形生成的定义。比如马赛克地形,月岛地形,都是在这里面定义的
- prefabs: 预制体,饥荒里的所有实体都是预制体,比如人物,花,鸟,鱼,树等等
- scenarios: 情景,游戏中的彩蛋
- screens: 场景,游戏里每个界面都是一个场景,比如创建世界,生成世界,加载世界,进入游戏的界面都是场景
- stategraphs: 状态图(区别于状态机,饥荒把行为逻辑抽到了行为树(behavious)里,stategraph就只管理动画逻辑),定义不同状态播放什么动作,比如人物站立时的动画,猎犬攻击的动画,同样还能定义不同状态下的event(事件),handler(处理程序)
- tools/utils: 各种工具
- widgets: 小窗口,比如显示血量,季节的那个表盘就是 widget
存档与日志
客户端日志保存路径:
C:\Users\Administrator\Documents\Klei\DoNotStarveTogether\client_log.txt
服务器日志保存路径:
C:\Users\Administrator\Documents\Klei\DoNotStarveTogether\用户id目录(全数字)\Cluster_x(x为数字,对应创建的哪个世界)\server_log.txt
注意,如果没开洞穴,那就不会有服务器日志,全都保存在客户端日志中
API
scripts/modutil.lua
官方提供的可在自定义mod直接调用的API,定义在源码scripts/modutil.lua
中,但需要增加GLOBAL.
作为前缀才能访问.不过有些是不需要的,具体哪些需要的可查看源码scripts/mods.lua
中env变量,如下:
1 | function CreateEnvironment(modname, isworldgen, isfrontend) |
另外如果是在自定义mod文件目录/modmain.lua
文件中,则均不需要增加GLOBAL.
前缀
scripts/mainfunctions.lua
这里非local修复的变量或方法都可直接使用,比如经常使用的CreateEntity(name)
方法(大部分代码使用时不传参数),具体原理暂不清楚.
全局对象
可查看源码scripts/strict.lua
中:
1 | -- 凡是调用次的方法的变量都是全局的 |
有一半全局对象都是在源码scripts目录/main.lua
中声明的.
按键KEY
1 | GLOBAL.TheInput:AddKeyHandler(function(key, down) -- 监听键盘事件 |
具体每个按键的key值,可在源码scripts/constants.lua
中查看(搜索KEY_
可快速定位)
自定义mod文件目录
1 | anim/:自定义动画文件,可不建立 |
资源
资源种类
图片
存放位置: modname/images/
图片需要png格式,然后转换成tex和xml文件,也就是每一个png文件都有两个tex和xml文件
动画
存放位置: modname/anim/
动画需要scml文件,通过spriter软件制作,然后放入modname/exported
文件下(此处略过了细节,具体请百度),通过使用饥荒提供的Dont Starve Together Mod Tools
软件中的autocompiler.exe,双击启动后会自动转换为压缩包.且该压缩包已放入modname/anim
文件夹下.
压缩包包含以下文件:
- anim.bin: 动画相关
- atlas-0.tex(可选): 图片相关,实际如果图片很多,可能还有atlas-1.tex…
- build.bin(可选): 图片对应的xml文件
atlas-0.tex
和build.bin
可不包括是因为如果对应资源已存在的话,只需要动画相关即可.比如: 饥荒官方的动画文件player_xx.zip,都只有anim.bin
文件,其它文件都已经被包含在wilson.zip/wilson_mount.zip
或其它资源包中,wilson.zip
含有正常人物的基本贴图,wilson_mount.zip
是骑牛时候的基本人物贴图.
反编译
通过上述文件反编译时需要注意,如果反编译时.tex文件和build.bin文件不全,会导致丢失缺失的图片资源,对应的通道也会消失.
通道: Spriter打开scml文件后,右上角Palette栏中,文件夹的名称就是通道名称,其文件夹下包括对应的正视图,侧视图,后视图,放置地面上的图等等
资源加载
饥荒的资源加载(图片和动画),分为两种:
modmain文件中指定Assets
1
2
3
4
5
6
7Assets = {
--科技栏的
Asset("ATLAS", "images/tab/doraemon_tab.xml"),
Asset("IMAGE", "images/tab/doraemon_tab.tex"),--可不写,只写加载xml文件即可
--人物动画
Asset("ANIM", "anim/doraemon_fly.zip"),--动画
}创建prefab时传入的assets
1
2
3
4
5
6
7local assets ={
Asset("ANIM", "anim/bamboo_dragonfly.zip"),--动画
Asset("ATLAS", "images/inventoryimages/bamboo_dragonfly.xml"),--物品栏贴图
Asset("IMAGE", "images/inventoryimages/bamboo_dragonfly.tex"),
Asset("SOUND", "sound/common.fsb"),--旋风扇声音
}
return Prefab(prefab, fn, assets)
modmain中的assets
一般这里必须要有mod的对应图标,另外如果有额外的人物自定义动画也请放在这里,有些mod(神话主题mod)放在了特效的prefab中,但我不清楚其中的原理
Prefab中的assets
一般来说和该prefab有关的资源都放在这里
modinfo
1 | -- 名称 |
modmain
import,require,modimport
进阶讲解
装备栏
1 | --饥荒源码: |
动画
图层: spriter左边图片名称
通道: spriter右边图片文件夹名称
inst.AnimState.show(“ARM_carry”)/hide(“ARM_normal”): 能显示或隐藏所有以”ARM_carry/ARM_carry_XXX”或”ARM_normal/ARM_normal_XXX”命名的图层(X:0-9,XXX最好以000开始依次排序,我没测试过乱序的情况),注意如果有多个重复图层,会一起被隐藏.比如:
1 | ARM_carry |
另外关于动画通道名称建议和原版保持一致,比如覆盖swap_body
通道,那你的动画文件名称动画通道也叫swap_body
,且通道下的名称都以swap_body-数字
格式命名
视图
- 正视图–>DOWN
- 右侧视图–>SIDE
- 左侧视图不需要提供,在游戏中是右侧视图的水平翻转
- 后视图–>UP,后视图在游戏中会增加水平翻转,所以设计的时候需要注意
武器动画
地上: 动画名称可随便命名,在该武器构建函数中直接播放该动画,动画文件名称记得和bank保持一致
手上: 动画名称必须是BUILD
,其所属动画entity的名称并不重要,也可以和地上动画放在同一个动画entity下.
1 | function equip() |
衣服动画
衣服/护符的贴图较多:
1 | -- swap_body通道下: |
一般的,贴图0~8是必须的,其余并不是必须的
帽子动画
其中要说明的是: 图片中红字说明以’特殊’开头的图片都不是必须的,尤其swap_hat-5~swap_hat-7
均为我自己mod需要,在制作自定义动画时我使用到了这几个文件
1 | --以下代码抄自联机武器衣服护甲模板mod |
地上动画补充说明
地上动画记得中心点要放在图片中央,另外地上动画透明背景应该要和图像本身大小差不多,不可以过多.
如果在游戏中捡起动作很难出现,请重新设置下图片中心点,再打包一次.另外也有大佬建议:可以反编译动画,修改可选择区域.
Action与StateGraph,componentactions
State主要用于播放动画,Action即为动作效果,二者并发执行(先进入state,在state中(客户端)调用PerformPreviewBufferedAction或(服务器)调用PerformBufferedAction后执行动作fn,如果开启了延迟补偿则前者会生效,否则只有后者也就是服务器调用的那个方法生效)
Action的主要属性
- priority: 优先级,如果动作收集器收到了多个可以触发的动作,优先度最高的会被允许触发
- str: 文字描述(玩家能看到的)
- mount_valid: 该动作是否允许骑乘时触发
- encumbered_valid: 该动作是否允许背重物时触发
- instant: 立刻,即是否为无视距离,true:无视距离立刻触发,false:需先走到最小触发距离再触发
- distance: 最小触发距离
- rmb: 是否为只能鼠标右键触发,true:只能鼠标右键
- fn(act): 动作函数,act类型为BufferedAction
Action的类别
详见源码文件
componentactions.lua
- SCENE: 点击一个物品栏上的实体或者世界上实体(需要拥有对应绑定的组件).例如:收集动作
- USEITEM: 拿起拥有对应绑定组件的实体的场景,例如:缝纫包
- POINT: 装备一个手持物品,或将一个物品拿起来.这是唯一一个可以对着地面而不是一个具体的物品作为变量的动作.例如:陷阱,种植,橙色法杖的闪现
- EQUIPPED: 装备拥有对应绑定组件物品后的场景,例如:装备一个武器后的技能,火把
- INVENTORY: 右键物品栏某项物品,且该物品拥有对应绑定组件的场景.例如:吃东西,装备物品
State的主要属性
1 | State{ |
状态的跳转
1 | inst.sg:GoToState(statename,params) -- 跳转到某一状态 |
状态的延迟补偿
- 如果开启了延迟补偿
- 如果关闭了延迟补偿
触发动作 -> 客机发送RPC(即:执行inst:PerformPreviewBufferedAction()) -> 服务器收到,并触发state,执行动画->动作fn函数,且会同步到客户端上
componentactions
Action与state构成了一个完整的动作,但如何触发动作?
这时候需要ComponentAction来完成.
1 | local fn = function(...) |
另外关于fn函数或者上述代码中fn函数中的testfn函数都要考虑到一点: 客户端.客户端执行这行代码时是拿不到一些组件的,所以通常建议modder在此处通过标签来实现是否可以触发动作的判断.
网络(单机与联机的差异)
AddNetwork
1 | inst:AddNetwork() |
当你添加上面这条语句时,就会让这个prefab能被所有人看见(如果它本身是可见的)。否则,只能被创建这个物品的电脑看到。比如说,几何种植显示的那些方格,其实也是prefab,但没有添加上面这条语句,所以只能被种植者本人看到,不会被其他玩家看到。
主机判断和SetPristine
1 | --标识该行代码之后不需要网络 |
上面这个if判断块在几乎所有的prefab中都十分重要,代码的含义是,如果这段代码运行在客机上,那么就到这里就结束prefab的初始化了。
因为在联机里,客机上存在的组件是极少的。包括物品栏组件inventoryitem,人物三围hunger,sanity,health在内的众多组件都是不存在的,所以如果不上面的那段代码,客机就会运行执行后面的代码,但由于相应的组件不存在,就一定会造成客机游戏崩溃,这也是单机转联机的人普遍会遇到的第一个崩溃问题。相应的,Tag,以及其他会被所有玩家看到的内容,比如说动画表现(AnimState),形态(Transform),人物说话的文字(talker)等,就必须加在上面这段代码之前,否则客机就看不到相应的界面表现了。
另外,对联机版的人物,初始化函数分成了两部分,一部分是common_postinit,另一部分是master_postinit。前者是会在主机和客机上都执行的代码,后者则只在主机上执行。上面的if判断已经内置在了MakePlayerCharacter函数中,所以无需再添加。
主客机数据交互
如果你想要做复杂的,深入游戏核心的联机版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。
Netvar&Classified
Classified和netvar的联系非常紧密,要解释Classified就需要解释netvar,这里就一并讲了。
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,具体用法如下:
1
2
3netvar:set(x)--只能在主机端调用这个函数,会自动同步客机的数据(在一个新的同步周期开始时)。如果这个函数确实改变了netvar的值,会在主机和客机上都触发相应的dirty事件。
netvar:value()--可以在主机和客机上调用这个函数,读取当前网络变量的值。
netvar:set_local(x)--可以在主机或客机上调用,改变相应的值但不触发数据同步或dirty事件。但当主机下一次调用set函数时,无论变量的值是否发生了改变,都会同步一次数据(即dirty事件)。
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消息给主机。
我们通过一个例子来学习:
1 | local function GrowGiant(player) |
学习资料
本文章参考了以下文章(无先后顺序),在此非常感谢大佬们,也请各位modders多多关注下以下大佬: