先说个现象。很多刚接触饥荒Mod的朋友,从创意工坊下了几个Mod,丢进mods目录里,游戏里勾选一下就能跑。等有一天自己动了写Mod的念头,新建了个文件夹,反而懵了:到底该建哪些文件?哪些文件名是游戏认识的,哪些是自己随便建的?为什么别人给的Mod文件长得五花八门?
上一篇我们完成了基础环境的准备,这篇就专门解决目录结构这个绕不过去的问题。别小看这几行文件和文件夹,它是所有饥荒Mod的地基。地基打不好,后面写再多的lua脚本都白搭。我会把这套目录结构拆开讲清楚,顺带把游戏加载Mod的顺序、modinfo.lua和modmain.lua的职责边界一次说明白,最后附上我踩过的坑。这篇文章不只适合刚入坑的新手,也能帮那些写过几个小Mod但一直靠复制粘贴的老哥们把底层逻辑盘明白。
1. 先搞懂一件大事:游戏到底怎么识别你的Mod
1.1 一个Mod文件夹就是游戏眼里的“一个Mod”
不管是单机版还是联机版,Mod的宿主目录都是mods。游戏启动时,会扫描这个目录下的每一个子文件夹,并把那些“有资格被称为Mod”的文件夹列到游戏主界面的Mod管理列表里。
“有资格”三个字是关键。游戏判断一个文件夹是不是Mod,看的不是文件夹名字,而是这个文件夹里有没有一个叫modinfo.lua的文件。modinfo.lua就是Mod的身份证,里面记录了名字、作者、版本号、兼容性、图标等元数据。没有这个文件,就算你往里放了一百个脚本,游戏也只会当你不存在。
这个干预逻辑非常重要。你在创意工坊订阅Mod时,Steam会把Mod下载到mods/workshop-xxxxxx这样的文件夹里。workshop-前缀只是Steam的下载路径规则,游戏本身并不依赖这个前缀。你自己手动建Mod文件夹的时候,随便起个英文名就行,比如mymod、testmod,只要里面文件结构完整,游戏一视同仁。不过有一点要注意:路径里尽量不要有中文和空格,联机版跨平台时这种路名容易出幺蛾子。
1.2 单机版和联机版的目录位置不完全一样
这里先绕不开一个现实问题:饥荒分单机版(Don't Starve)和联机版(Don't Starve Together,简称DST)。两套游戏的Mod目录结构逻辑基本相同,但具体路径位置有差异。
以常见系统为例,单机版的Mod一般在游戏根目录下的mods里,或者在我的文档下Klei相关目录里;联机版的Mod则在Documents/Klei/DoNotStarveTogether/对应的客户端数据目录中。实际到底在哪个位置,取决于你的游戏安装方式、是否开了创意工坊、以及系统用户目录设置。最快的办法是:在Steam里浏览游戏本地文件,或者直接到创意工坊订阅一个任意Mod,然后看它被下载到哪里,你的Mod就放到哪里。
联机版还多了一套“服务器Mod”和“客户端Mod”的区分。你自己单机玩的时候,Mod默认都会生效。但到了联机,情况就变了:有的Mod必须所有人都装了才能正常进服,有的是只有房主装了就行,还有的只是改变个人视角和UI,谁装谁生效就行。这个区分主要写在modinfo.lua的参数里,后面会说,但它决定了你的Mod在整个多人生态里的角色,立项前就要想清楚。
2. modinfo.lua:Mod的身份证,先把它写明白
2.1 字段逐个拆解:哪个字段游戏真会读
modinfo.lua的格式说穿了就是一个返回Lua表的文件。游戏加载时会执行这个文件,读取里面的键值对。字段虽然多,但真正会被用到的其实就那几个。
name = "我的第一个Mod" description = "这个Mod用来演示目录结构" author = "YourName" version = "1.0.0" forumthread = "" api_version = 10 icon_atlas = "modicon.xml" icon = "modicon.tex" dont_starve_compatible = false dst_compatible = true reign_of_giants_compatible = false shipwrecked_compatible = false hamlet_compatible = false all_clients_require_mod = truename:显示在Mod列表里的名字,建议英文,中文也可以但可能遇到字体显示问题。description:Mod的简介,玩家在Mod管理列表里能看到。多国语言支持的话,可以配合本地化字段写多语言描述。author:显示作者,没有任何校验,想写什么写什么。version:Mod版本号,是个字符串,可以1.0.0也可以1.01,本质是给玩家和后续更新看的。api_version:这个要重点说。单机版老Mod多是6,联机版目前主流是10。如果你拿着一个很久没更新的Mod,进游戏提示版本不兼容,十有八九是这里的问题。对新写的Mod,直接用10。icon_atlas和icon:Mod列表左侧的小图标。注意这里不是直接放PNG,而是需要一个.tex和一个配套的.xml图集配置文件。图标文件需要放在Mod根目录下,文件名和字段对应的保持一致。- 兼容性字段:
dont_starve_compatible是单机版兼容开关,dst_compatible是联机版兼容开关,reign_of_giants_compatible、shipwrecked_compatible、hamlet_compatible分别是原版巨人国、船难、哈姆雷特DLC的兼容开关。这些字段的作用是告诉游戏“我这个Mod能不能在这个模式下加载”。
除了这些,还有几个联机特有的字段值得关注。all_clients_require_mod = true表示所有联机玩家都必须安装这个Mod才能进入服务器;client_only_mod = true则表示这个Mod只在客户端运行,不影响服务器逻辑。设计Mod时要想清楚自己的定位,瞎写会让玩家联机时摸不着头脑。
2.2 可以直接抄作业的modinfo.lua模板
我贴一个自己常用的模板,基本覆盖了单机、联机双兼容的常见配置,你直接把内容替换成语义合理的配置就行。
-- modinfo.lua name = "Example Mod" description = "An example mod for directory structure tutorial" author = "YourName" version = "1.0.0" forumthread = "" api_version = 10 -- 图标配置 icon_atlas = "modicon.xml" icon = "modicon.tex" -- 兼容性配置 dont_starve_compatible = false dst_compatible = true client_only_mod = false all_clients_require_mod = true -- DLC兼容性 reign_of_giants_compatible = false shipwrecked_compatible = false hamlet_compatible = false如果你想让Mod支持多语言描述,可以用Klei官方支持的那种带本地化表的方式。游戏会根据玩家语言读取对应的简介,没有匹配时回退为英文。这里不再展开,因为和目录结构的主线关系不大,但思路是:在modinfo.lua里写一个可以返回不同语言文本的函数。
新手最容易犯的错是:改完modinfo.lua,发现游戏里Mod列表还是老样子。原因通常有两个:一是文件保存时没有保持UTF-8编码,中文变乱码;二是游戏对modinfo有缓存,改了它之后需要重进游戏主菜单甚至重启游戏才能刷新,只在设置界面来回切没用。
3. modmain.lua:整个Mod的启动引擎
3.1 为什么逻辑必须从这个文件开始
游戏扫描并启用Mod以后,做的第一件事就是执行Mod目录下这个叫modmain.lua的文件。如果说modinfo.lua是身份证,那modmain.lua就是心脏。你所有向游戏注入逻辑、修改数值、监听事件的操作,都必须从这里起步。
但注意,modmain.lua本身不负责写所有逻辑,它是“入口”和“调度中心”。实际的功能代码,通常被拆分成多个脚本放到scripts目录下,然后在modmain.lua里用require引入。这样设计的好处是:一个Mod做大了以后,几百行甚至上千行代码挤在一个文件里,不仅看着痛苦,排错更是灾难。
比如,我把“给角色加新物品”的逻辑写在scripts/prefabs/my_item.lua里,在modmain里只需要一行:
require "prefabs/my_item"这个require路径是相对于Mod根目录的scripts文件夹来计算的,默认不带.lua后缀。换句话说,require "prefabs/my_item"对应的是scripts/prefabs/my_item.lua。
有一点我必须反复强调:饥荒的脚本执行环境分两个层级,一个是游戏全局环境GLOBAL,另一个是Mod自己沙盒化的局部环境。你在modmain.lua里直接写print("hello"),这个print是Mod局部的,不会污染全局。想访问游戏全局内容,比如全局配置表TUNING或玩家实例,就得用GLOBAL.TUNING这种方式。很多新手一上来就写:
TUNING.SOME_VALUE = 10 -- 错误!这里的TUNING是nil然后报错attempt to index a nil value,就是这个原因。正确写法是GLOBAL.TUNING.SOME_VALUE = 10,或者在文件顶部先local GLOBAL = GLOBAL,再从GLOBAL表里取。
3.2 最常用的功能挂载点,直接上代码
modmain.lua里怎么挂功能,我用三个最典型的场景演示:改角色数值、自定义合成配方、监听事件。
场景一:把某个角色的最大生命改成200。这个用AddPrefabPostInit,它会在某个prefab(可以理解为一个实体模板)初始化完成后执行回调,是饥荒Mod开发里出镜率最高的钩子。
-- modmain.lua AddPrefabPostInit("wilson", function(inst) if inst.components.health then inst.components.health:SetMaxHealth(200) end end)这个回调里拿到的inst就是游戏中该prefab的一个实例。判断一下inst.components.health是否存在,是防止某些情形下组件还没挂上导致崩溃。
场景二:给玩家加一个新合成配方。AddRecipe的签名比较长,简单用法如下:
AddRecipe( "my_item", -- 配方对应的物品名 { GLOBAL.Ingredient("twigs", 2), GLOBAL.Ingredient("flint", 1) }, GLOBAL.RECIPETABS.TOOLS, -- 在哪个合成栏 GLOBAL.TECH.SCIENCE_ONE, -- 需要几本科技 nil, nil, nil, nil, nil, -- 其他参数先留空 "my_mod_tag" -- 自定义tag,方便和原版区分 )这个配方的意思是:两根树枝加一块燧石,在工具栏里合成my_item,需要一级科技解锁。
场景三:监听游戏事件。比如角色死亡后想做点啥,可以监听player_death事件。世界事件、玩家状态变化等都可以通过类似方式监听,关键是把AddPrefabPostInit回调里拿到的实例作为事件源,用inst:ListenForEvent注册自己的处理函数。
3.3 写modmain.lua时的顺序感
虽然游戏对modmain.lua里的代码顺序没有硬性要求,但为了自己方便,我建议按这个顺序组织:
- 必要的变量、常量声明和require。
- 对全局配置(TUNING)的修改。
- 注册各类PostInit钩子。
- 注册配方、动作、科技等新内容。
- 数组和配置类代码收尾。
这样做的最大好处是,后面排查问题的时候,你能顺着代码从上到下快速定位是哪一类逻辑出了问题,不需要在一堆混杂的函数定义里翻来翻去。
4. scripts目录:所有脚本的家,但也别乱放
4.1 一套可以长期维护的脚本目录布局
很多新手喜欢把所有脚本都平铺在Mod根目录,或者一股脑丢进scripts里不分类。几个文件还行,等Mod功能多起来,找文件找到崩溃。我自己的习惯是,按脚本职责分子目录。下面是我常用的模板,你的Mod不必完全照抄,但可以参考这个分层逻辑。
MyMod/ ├── modinfo.lua ├── modmain.lua ├── scripts/ │ ├── prefabs/ -- 自定义实体,比如新物品、新生物 │ ├── components/ -- 自定义组件,扩展游戏行为 │ ├── actions/ -- 自定义玩家动作 │ ├── recipes/ -- 自定义配方 │ ├── widgets/ -- 客户端UI相关 │ └── tuning.lua -- 集中存放自定义数值 ├── images/ │ ├── inventoryimages/ -- 物品栏图标 │ └── ui/ -- 其他界面贴图 ├── anim/ -- 动画文件 ├── sound/ -- 音频文件 └── exported/ -- 各种工具导出的中间资源每个目录的定位很清楚:prefabs里放物品、生物这类实体的定义;components里放你自己的组件,用来给实体增加原版没有的能力;recipes里放合成配方;tuning.lua集中管理所有数值常量。为什么单独抽一个tuning.lua?因为后期调整平衡时,你只想改数值,不想在一堆逻辑代码里捞参数。
4.2 自定义物品的时候,prefab到底怎么写
既然提到了scripts/prefabs,顺手演示一个简单自建物品的完整流程。第一步,在scripts/prefabs/my_item.lua里定义物品:
-- scripts/prefabs/my_item.lua local assets = { Asset("ANIM", "anim/my_item.zip"), Asset("ATLAS", "images/inventoryimages/my_item.xml"), } local function fn() local inst = CreateEntity() inst.entity:AddTransform() inst.entity:AddAnimState() inst.entity:AddNetwork() -- 具体实体初始化逻辑 return inst end return Prefab("my_item", fn, assets)这个Prefab最终会通过require被modmain或其他逻辑调用。注意里面的动画资源路径anim/my_item.zip和贴图路径images/inventoryimages/my_item.xml,它们对应Mod根目录下真实的资源文件。这正是目录结构联动起来的地方——脚本引用资源,资源放在游戏能识别的固定位置,一以贯之。
4.3 require路径常见的坑:大小写和目录对不上
Lua的require是区分大小写的。你在modmain里写require "Prefabs/my_item",但实际目录是小写scripts/prefabs/my_item.lua,那就会报找不到文件。
还有一个隐藏问题:Windows本地开发时,文件系统大小写不敏感,所以即使路径大小写写错了,本地也能跑。但一旦把Mod发给别人或者放到Linux服务器上跑联机,问题立刻暴露,玩家那边就是加载失败。这让很多Windows上开发的新手非常头疼,因为明明本地一切正常。我现在写完Mod第一件事,就是把所有require路径的大小写和实际目录核对一遍,养成习惯能省一晚上排查时间。
另外要注意,scripts目录也不是必须存在的。如果你的Mod只有几十行逻辑,直接在modmain.lua里写完就行,完全不需要建scripts目录。目录结构是为了“可维护性”服务的,不是为了凑完整体面而存在的。硬是为一个十几行的小功能拆五个脚本文件,反而是徒增负担。
5. 资源目录:贴图、动画、音效各就各位
5.1 哪些资源目录是游戏认识的,哪些是约定俗成的
饥荒的资源文件有自己的一套格式,不能直接把美术好的PNG往文件夹里一丢就算完事。游戏真正能识别的是处理过的资源文件。
- 动画资源:放在
anim/目录,通常是.zip格式,里面是编译过的动画数据。游戏在prefab里通过Asset("ANIM", "anim/xx.zip")引用。 - 贴图资源:放在
images/目录下,本质上由.tex和.xml成对出现。.tex是处理后的图片,.xml记录图集里的裁剪坐标。物品栏图标一般特定放在images/inventoryimages/下,这是游戏默认查找物品图标的位置。 - 音频资源:放在
sound/目录,格式是.fsb或者游戏专用的音频包。一般在声明Asset("SOUND", "sound/xx")时使用。
这些目录名字和位置,部分是游戏硬编码,部分是社区惯用约定。如果你拿不准,最快的办法是下一个别人写好的Mod,拆开看它的资源放哪儿,把自己的资源对号入座。这和学习编程看优秀源码一个道理。
5.2 为什么你的贴图老是不显示
贴图不显示是新手最容易遇到的问题。绝大部分情况都出在“贴图文件没转换”上。
原版游戏的贴图格式是统一的Klei专用格式,直接用Photoshop另存为也不行。你需要专门的转换工具把PNG转成.tex和.xml文件,然后把这两个文件一起放进images/目录。另外,素材的尺寸、图集打包方式也会影响显示效果,但那是后话,先把转换流程跑通再谈美术细节。
这里提醒一下:不要直接修改游戏原版资源文件。一方面会被游戏完整性问题检测机制盯上,另一方面,你改的是别人的文件,一旦游戏更新,改动全部丢失。正确的做法是把原版文件作为参考,复制到自己的Mod目录里再修改。
6. 新手最常见的目录结构问题速查
6.1 典型问题清单,出现症状直接对照
我把这几年在饥荒Mod开发社区里看到的、以及自己踩过的目录结构相关坑整理成了一张表,基本能覆盖绝大多数目录结构引发的问题。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 游戏中Mod列表里看不到这个Mod | Mod没放进正确的mods目录,或文件夹里没有modinfo.lua | 确认目录位置,确认modinfo.lua存在且拼写正确 |
| Mod列表能看到,但勾选启用后没有反应 | modinfo.lua里语法错误,或api_version不兼容 | 查看日志文件,检查语法,确认api_version = 10 |
| 改了modmain.lua,游戏里却没变化 | 游戏缓存,或改错文件目录 | 完全重启游戏,不要只回主菜单;确认改的是被加载的那个modmain.lua |
启动游戏时报attempt to index a nil value | modmain里直接访问全局变量没带GLOBAL. | 改成GLOBAL.TUNING等写法,或文件顶部声明local GLOBAL = GLOBAL |
| require一个脚本时报找不到文件 | 路径写错、大小写不一致、文件不在scripts目录下 | 核对require路径和实际文件位置,注意大小写 |
| 联机时自己能看到新物品,队友没有 | Mod被设计成了客户端Mod,或队友没装 | 修改all_clients_require_mod或client_only_mod字段,重新分发 |
| 贴图不显示,只有代码在跑 | 贴图没转换成游戏用的.tex/.xml格式 | 用转换工具处理贴图,并按正确路径放入资源目录 |
6.2 怎么快速定位目录结构问题
定位问题的第一现场永远是日志文件。单机版和你本机的联机客户端日志,一般在Documents/Klei/目录下对应游戏的文件夹里。打开log.txt或者client_log.txt,搜mod关键字,基本能看到Mod加载失败的具体报错信息。有的错误信息会直接精确到某个文件哪一行,省去瞎猜的时间。
除了看日志,我调试Mod时还习惯用print大法。在modmain.lua开头加一行:
print("[MyMod] modmain loaded")然后看日志里有没有输出这行内容。如果没有,说明Mod根本没被加载;如果有,再逐步缩小范围。尤其要养成一个好习惯:写完一个目录结构调整,就重启游戏验证一次,别攒着一堆改动到最后一起测试,否则出了问题都不知道是哪一步引入的。
7. 最后给你一套自己的“脚手架习惯”
目录结构这东西,看着简单,但真到了自己动手组织一个功能复杂的Mod时,才会发现有一个清晰的骨架是多么重要。我刚开始写Mod时,也是从单文件、把什么都往modmain里塞开始的。后来Mod功能多了,改一个数值要找半天,有一种“自己写的代码自己都怕”的崩溃感。从那以后,每次新建Mod我都先搭一套清清爽爽的脚手架:modinfo、modmain、scripts分类目录、资源目录。搭好之后才开始写业务逻辑,后面所有功能都是在这个骨架上长出来的肉。
这套习惯不止在饥荒Mod里用得上。你如果以后去折腾英灵神殿(Valheim)Mod、或者做Unity/ROBLOX这类游戏盒子里的内容开发,会发现底层都是同一套哲学:入口文件负责启动,脚本按职责拆分,资源放在游戏约定好的位置,元信息单独管理。目录结构解决的不是什么高深算法问题,而是“让别人(以及三个月后的你自己)能看明白这个Mod到底干了什么”这个最朴素的问题。
今天的内容就到这里。下一篇我准备继续往里走,聊聊modmain.lua里那些高频API的具体用法,以及怎么在游戏里调试自己的Mod。在这之前,你可以先把目录结构按自己的需求搭起来,哪怕是临时的,也比你继续对着一个空文件夹发呆要强。