简介:这是一份面向《饥荒》模组开发者的Lua源码包,用于在烹饪锅中实现自定义食物,主要解决玩家不理解食物定义、食谱注册与效果触发等问题,适合刚接触mod编写或想复刻食物逻辑的游戏玩家。资源以zip压缩包形式发布,共10个文件,核心是4个lua脚本,分别承担物品属性配置、食谱注册、主逻辑入口等职责,另含tex/png贴图与xml描述文件,整套包仅17KB,结构十分轻量。已有373人浏览学习,说明这类源码对饥荒mod新手有较直接的参考价值。源码覆盖了从物品定义(名称、图标、属性)、食谱配方(原料、烹饪时间)到注册进游戏、特殊效果(如喂食猪人)的完整链路,并兼顾了多mod兼容与测试要点。通过阅读和调试这份工程,读者可快速掌握饥荒烹饪锅食物的制作规范,也能在此基础上扩展属于自己的独特菜谱。
1. 先想清楚:为什么给饥荒烹饪锅加菜,绕不开这堆源码
某开发者想给烹饪锅加一道「两个烤肉加蜂蜜炖出来的新料理」,他直接在游戏里翻设置、找菜谱文件,折腾一晚上没找到入口——因为饥荒里一道菜不是写在一张「料理配方表」里,而是在一堆 lua 源码里通过「食材组合 + 优先级匹配 + prefab 生成」跑出来的。这篇笔记要解决的问题,就是把「制作饥荒 烹饪锅食物 源码」这件事拆透:你怎么读懂这堆源码、怎么给烹饪锅加自定义料理、怎么让新菜真的出现在锅里而不是被原版菜抢走,以及改坏了怎么排查。适合两类人:一类是想做烹饪锅 Mod 的新手,另一类是拿到别人源码包但看不懂为什么某些菜能出锅、某些菜永远被顶掉的维护者。
2. 拆解烹饪锅的运转逻辑:先看懂食物是怎么被煮出来的
2.1 一条食材扔进锅,背后要过四道关卡
烹饪锅这个设备在饥荒里对应一个组件(component),常见实现里叫crockpot。它做的事情很简单:接收放进来的食材、累计烹饪时间、到点后从一堆候选料理里挑一个生成。但「挑一个」这步才是源码的重点——它不是随机挑,而是按一套固定顺序做判定。
我一般把判定拆成四步:第一步检查是不是特殊配方,比如曼德拉草汤、太妃糖、怪物千层饼这类只靠单一食材就能锁定的料理;第二步遍历所有 Recipe,按「食材标签是否匹配」筛出候选;第三步在候选里按优先级排序;第四步根据料理的priority和食材的「需要度」做最终选择。落到代码上,一个料理的关键定义大概长这样:
-- recipes.lua 里的一个料理定义 AddRecipe("honeyham", { Ingredient("smallmeat", 2), Ingredient("honey", 1), Ingredient("veggie", 1) }, { potlevel = 1, priority = 30, cooktime = 40 })这段代码的逻辑是:AddRecipe第一个参数是这道菜在源码内部用的名字,第二个参数是一个食材数组,每个Ingredient里第一个值是食材 prefab 名,第二个值是数量;第三个参数是一个配置表。potlevel控制这道菜能在几级锅出现,priority是匹配权重,cooktime是煮多久。注意这里用的是smallmeat而不是「小肉块」,因为源码里所有东西都走 prefab 名,不是显示名。也就是说,当你放进去的食材凑齐了「两小块肉 + 一蜂蜜 + 一蔬菜」三样时,这道菜才进入候选池。
这四道关卡里最容易被忽略的是第一关。原版里有些料理走「特殊配方」通道,不参与普通匹配。比如你往锅里丢一个曼德拉草,不管剩下三格放什么,出来的基本都是曼德拉草汤,因为它在特殊配方表里被优先拦截了。所以你想自定义料理时,如果食材组合容易被误判成特殊配方,你的菜就永远轮不到上场。
2.2 一个料理在代码里的完整身份:prefab、stew、tuning
光有AddRecipe还不够。一道菜真正出现在游戏里,必须有一个对应的 prefab——也就是它的实体定义。这个 prefab 决定了它在背包里长什么样、吃了回多少属性、能不能被鸟换种子、能不能喂给某些生物。常见做法是单独写一个 prefab 文件,然后在 prefab 里注册一个stewable组件,用它承载料理数值:
-- prefabs/xxxstew.lua 里的简化示例 local assets = { Asset("IMAGE", "images/inventoryimages/customstew.tex"), Asset("ATLAS", "images/inventoryimages/customstew.xml"), } local function fn() local inst = CreateEntity() inst.entity:AddTransform() inst.entity:AddAnimState() inst.entity:AddNetworkEntity() MakeInventoryPhysics(inst) inst:AddTag("preparedfood") inst:AddComponent("edible") inst.components.edible.foodtype = FOODTYPE.MEAT inst.components.edible.hungervalue = 75 inst.components.edible.healthvalue = 20 inst.components.edible.sanityvalue = 5 inst:AddComponent("stackable") inst.components.stackable.maxsize = 1 inst:AddComponent("perishable") inst.components.perishable:SetPerishTime(TUNING.PERISH_PRESERVED) return inst end return Prefab("customstew", fn, assets)这段代码看着长,实际上核心就几件事:给这个实体挂上可食用、可堆叠、会腐烂三个组件。foodtype决定它算肉还是蔬菜,会直接影响沃利这类角色的偏好;hungervalue、healthvalue、sanityvalue就是吃下去后恢复的三维数值。perishable的SetPerishTime用的是TUNING里的预设值——这里的TUNING是游戏全局调参表,我一般会填PERISH_PRESERVED表示保存期长,如果你改成PERISH_SLOW或更短,这道菜会烂得很快。
2.3 这个标题的「源码」到底指源码做什么
很多抱着「制作饥荒烹饪锅食物源码」来的人,拿到手后发现是一堆.lua文件,第一反应是「这不是源码吧」。这里得先把预期摆正:饥荒的 Mod 层源码就是 lua,不涉及引擎层。你不需要编译、不需要装工具链,游戏本身就自带一个 lua 解释器,你写的代码放对位置就能加载。真正要改的东西集中在三块:料理注册表(recipes.lua 一类的定义)、prefab 实体文件、以及图标贴图资源。这个认知很重要——因为如果你把精力花在找「有没有更底层的编译源码」上,方向就偏了。
常见的源码包结构是 mods 目录下挂一个 Mod 文件夹,里面modmain.lua作为入口,modinfo.lua给游戏读取元信息,recipes.lua或直接把AddRecipe写在modmain里。你拿到别人的源码包,第一件事不是打开一个个文件读,而是先在modinfo.lua里看它依赖什么、加载优先级是什么,再在modmain.lua里搜AddRecipe和AddPrefab这两个函数,基本就能定位这个 Mod 改了什么。
3. 动手做第一道自定义料理:最小可跑的烹饪锅 Mod
3.1 搭骨架:modinfo 与目录结构,让游戏认得出你的 Mod
在写任何料理逻辑之前,先把 Mod 的壳建好。饥荒读取 Mod 的方式是扫描 mods 文件夹下每个子文件夹,先读modinfo.lua拿到名字和描述,再执行modmain.lua。我习惯的目录结构是这样的:
mods/customstew/ modinfo.lua modmain.lua recipes.lua prefabs/customstew.lua images/inventoryimages/customstew.tex images/inventoryimages/customstew.xml注意用户数据目录里的mods文件夹,不是游戏安装目录的mods——后者是内置官方内容,你往里面写文件,一次验证更新就被覆盖,这是最常见的翻车点。modinfo.lua内容如下:
-- modinfo.lua return { name = "Custom Stew Mod", description = "Adds a custom crock pot dish", author = "YourName", version = "1.0", mod_version = 1, dst_compatible = true, all_clients_require_mod = true, icon_atlas = "modicon.xml", icon = "modicon.tex", }这里的dst_compatible和all_clients_require_mod是联机版两个关键字段。前者表示这个 Mod 支持联机模式,后者表示所有客户端都必须装同一个 Mod——如果你的新菜只在自己本地注册,别人进你的房间会看到你手上拿着一个「未知物品」甚至直接掉线。icon_atlas和icon是 Mod 在列表里显示的图标,如果你暂时没有贴图,可以先省略这两个字段,游戏会用默认图标代替,不影响功能。
3.2 注册入口:把 AddRecipe 和 AddPrefab 挂到 modmain
壳建好后,modmain.lua是加载核心。你在这里导入配方文件和 prefab 文件,逻辑很简单:先require配方,再让游戏知道你新增了一个 prefab。我一般会这样写:
-- modmain.lua local recipes = require("recipes") AddPrefab("customstew")但这里有个坑:require("recipes")在饥荒 Mod 环境里不一定会按你预期工作,因为recipes.lua这个名字可能跟游戏内部模块冲突。更稳的写法是把自己的配方文件起一个不重名的名字,比如custom_recipes.lua,然后在 modmain 里这样引用:
-- modmain.lua local custom_recipes = require("custom_recipes") AddPrefab("customstew")实际加载顺序上,require会返回一个模块表,但如果你的custom_recipes.lua在文件顶层直接调用了AddRecipe,那require执行的一瞬间配方就已经注册完了。我看过不少新手在这步纠结「为什么 require 了没反应」,其实不是没反应,是AddRecipe在文件加载时已经执行,而游戏刷新料理列表有自己的时机。如果你需要确保在游戏初始化完成后再注册,可以依赖modimport或把注册逻辑包在AddGamePostInit回调里,但绝大多数自定义料理场景用顶层调用就够了。
3.3 第一道菜:注册一个不会被抢走的料理
终于到写菜的部分。这里用一道新手举例最多的「蜂蜜烤肉」来演示:两个小肉 + 一个蜂蜜 + 一个任意蔬菜。之所以选它,是因为这个组合同时涉及肉类、蜂蜜、蔬菜三类标签,能让你立刻体会到标签匹配的威力:
-- custom_recipes.lua AddRecipe("honey_roast", { Ingredient("smallmeat", 2), Ingredient("honey", 1), Ingredient("veggie", 1) }, { potlevel = 1, priority = 50, cooktime = 20, num_to_give = 1, })这段代码里的num_to_give我特别说明一下:它表示一次烹饪产出几个料理,默认是 1。有些原版菜比如火鸡大餐会把num_to_give设成 2,因为它本质上是「一道菜份量足够养活两个人」。你自定义时先别动这个字段,把 1 稳住再说。
接下来是核心匹配逻辑:当玩家往锅里放了两个小肉、一个蜂蜜、一个蔬菜后,系统会把锅里的食材转换成一个「标签需求表」,然后遍历所有 Recipe,看哪个被满足。priority在这里的用处是:如果同时有多道菜都满足食材条件,系统会优先选priority高的。我把它设在 50,是一个偏高的值,目的是避免这道菜被原版那些 priority 20-30 的常见菜顶掉。
但这个做法有个副作用:一旦你调高了 priority,玩家不管怎么组合食材,只要凑齐这两个小肉一蜂蜜一蔬菜,出的基本都是你的菜。这是一把双刃剑,后面避坑章里我会详细讲怎么用标签和优先级做「限定出现」而不是「抢占出现」。代码写完后,别忘了在 prefabs 目录里补上customstew.lua,内容参考上一章的别炖菜模板,把数值改成你想要的就行。
3.4 图标与贴图:为什么新菜在背包里是紫绿方块
代码全部写对后,你进游戏会立刻遇到一个视觉问题:新菜出来了,但图标是紫绿相间的方块。这不是因为你代码写错了,而是因为游戏找不到customstew.tex和customstew.xml这组贴图资源。常见的解决方式有两种:第一种是正经做贴图,用游戏自带的工具把绘制好的 PNG 转成 tex 和 xml;第二种是偷懒方案,直接借用游戏里某个已有的料理贴图。我一般调试阶段用第二种:
-- prefabs/customstew.lua 里的简化写法 inst:AddComponent("inventoryitem") inst.components.inventoryitem:ChangeImageName("cookedmeat") inst.components.inventoryitem.imagename = "cookedmeat" inst.components.inventoryitem.atlasname = "images/inventoryimages/cookedmeat.xml"这么改的意思是告诉游戏:这个物品的实体模型用自定义的,但背包图标借用「熟肉」的贴图。好处是能立刻看到效果,坏处是如果你把图标名写成cookedmeat而这道菜实际是炖菜,玩家把图标放大了会觉得很违和。正经方案里,ChangeImageName对应的tex和xml需要放在images/inventoryimages/目录下,并且 xml 里的atlas项要和文件名一致。这一步没有捷径,就是做图、导出、放对位置、改对名字。
4. 原版菜谱的优先级玄学:为什么你的菜老是煮成别的
4.1 priority 与需要度:同锅食材如何裁决归属
把一堆食材扔进锅,同时满足好几道菜的配方要求,这是特别常见的情况。比如你放了两个蜂蜜一个肉一个浆果——蜂蜜烤肉可能满足,蜂蜜火腿也可能满足,果酱蜜饯也可能满足。最终出哪道菜,不是看哪道菜先被定义,而是看两个数值的组合。第一个是priority,第二个是「需要度」。
「需要度」这个概念在源码里不是显式字段,而是通过Recipe里条件判断的复杂程度隐式体现的。说得直白点:当食材数量完全覆盖某个配方时,这个配方进入候选;如果食材数量只覆盖了一部分,匹配度就差。系统会先把匹配度高的配方挑出来,再在它们之间比较 priority。这个过程我经常跟朋友说成「先看你能不能做,再看你想不想做」——能不能做是食材覆盖判断,想不想做是 priority 排序。
为了让你对数值有个参照感,我把常见料理的 priority 分布和适用场景列成一张表,这表是按我多年调试经验总结的,不是官方数据:
| priority 区间 | 代表类型的菜 | 我的使用建议 |
|---|---|---|
| 80-100 | 特殊料理、节日限定、角色专属菜 | 做「特定食材组合铁定出这道菜」时用 |
| 40-70 | 高级肉菜、需要多种食材的复合菜 | 自定义主要菜的甜区,能顶掉大部分原版同组合菜 |
| 10-30 | 普通单人份料理、早期果腹菜 | 做基础填充菜、或想「偶尔出但是不抢风头」时用 |
| 0-10 | 失败料理、勉强能吃的糊糊 | 几乎不设,设了就是摆设 |
我的血泪经验是:新手最容易把 priority 设到 100 以上,结果玩家放啥都出你的菜,连原版经典配方都被顶没了。更聪明的做法是设成「比你想顶掉的那道菜高 10 到 20」。比如原版蜂蜜火腿 priority 大约在 30 档,你想让自己的蜂蜜烤肉在同样食材下优先出,就设 50,而不是 99。
4.2 改原版菜谱的三种姿势:覆盖、新增、替换食材
除了新增料理,很多人的需求是「我不加菜,我就想把原版某道菜的配方改一下」。比如觉得火鸡大餐要 4 个鸡腿太苛刻,想把需求改成 2 个鸡腿。这个诉求在源码层面有三种做法:
第一种做法是直接覆盖原版 Recipe,核心逻辑是先找到 Game 里已经注册好的配方表,然后改它的 ingredients:
-- 覆盖原版火鸡大餐的食材需求 for k, v in pairs(Recipes) do if v.name == "turkeydinner" then v.ingredients = { Ingredient("drumstick", 2), Ingredient("veggie", 1) } end end这段代码要写在modmain.lua的加载阶段,而且要在原版食谱注册完成后执行。饥荒的 Mod 加载顺序是:先加载游戏本体全部数据,再执行 Mod 的 modmain,所以你在 modmain 里遍历Recipes表时,原版菜已经在里面了。Recipes是一个全局注册表,key 是料理名,value 是配方对象,通过v.name匹配你要改的菜,然后替换 ingredients 数组。
第二种做法是不碰原版,新增一道名字不同的菜,但它食材组合和原版非常相似。这个做法我推荐给所有刚上手的人,因为它完全不影响原版玩家体验——你没改火鸡大餐,只是加了一道看起来很像的菜。第三种做法是替换食材,比如把某个食材换成同标签的另一个食材,这个其实还是覆盖,只是只动数组里一项。三种方式我通常按「是否要保留原菜」来选:保留原菜就新增,不保留就直接覆盖。
4.3 锅的类型与派生菜:potlevel 设错会怎样
potlevel是一个容易被忽略的字段。它的作用是指定这道菜能在哪种锅上做出来。原版里有普通烹饪锅,也有后期解锁的便携烹饪锅、甚至某些 Mod 里的强化锅。如果potlevel设成 2,但你的世界里只造了普通锅,那这道菜怎么煮都出不来——不是配方不匹配,是锅的等级不够。
这里要区分「锅等级」的机制:普通锅默认potlevel = 1,便携锅或高级锅通常支持2或更高。你在自定义菜时,如果你希望这道菜「只有高级锅能做」,就设 2;如果你希望任何锅都能做,设 1 或 0。设 0 通常表示最低级锅也能做,但某些判定逻辑会把 0 当成「没限制」或「不能做」,所以我自己一般只设 1 或 2,不碰 0。
还有一个连坐问题:派生菜。有些料理不是直接煮出来的,而是通过其他道具合成的。如果你设了很高的 potlevel 让某道菜只能在高级锅出,但玩家手里的高级锅配方还没解锁,他放进食材后锅会转很久然后给你一碗糊糊——这个体验非常差。我的习惯是:给自定义菜设 potlevel 之前,先问自己一句「这道菜是要让所有玩家轻松做出来,还是要做成高级玩家的奖励」。前者设 1,后者设 2,千万别模棱两可。
5. 烹饪锅 Mod 避坑实录:我从翻车里总结的 5 个排查方向
这一章直接上干货。以下五条是我在调试烹饪锅自定义料理时踩过最多次的坑,每一条都按「现象 → 原因 → 解决」来写,你看代码跑不通时按图索骥就行。
5.1 坑一:新菜做出来了,但背包里显示紫绿方块
这个现象我上面提到过,这里再展开说。现象:料理能出锅,能吃,数值也对,但凡是这盘菜,背包图全是紫绿相间的马赛克。原因:inventoryitem组件找不到imagename对应的贴图资源文件。你那道菜的 prefab 里写了imagename = "customstew",但images/inventoryimages/目录下根本没有customstew.tex和customstew.xml。解决:要么补上真正的贴图资源,要么像我在 3.4 里那样,把imagename改成已有料理的名字(比如cookedmeat)。这不算最终方案,但能让你在调试期间把精力集中在料理逻辑上,而不是被贴图挡住。
5.2 坑二:锅放满食材后一直转圈,最后出了别的东西
现象:你精心设计了食材组合,放进去之后锅盖一直冒烟,时间一到出来的不是你的菜,而是原版的某道菜。原因:你的料理在候选池排序时输给了另一道优先级更高或匹配度更高的菜,或者你的食材组合本身被特殊配方通道拦截了。解决:先在modmain.lua里临时写一行print("custom recipe candidate"),然后查看log.txt,确认AddRecipe是否真的注册成功。如果能注册但出来还是别的菜,就把 priority 调高,直到你的菜能稳定胜出。我一般从 50 起步,每次加 10,直到找到不被顶掉的临界值,然后固定住。
5.3 坑三:代码明明写了,但游戏里完全没变化
现象:你把代码全部写好,刷新资源、重进游戏,结果锅里还是原版那几道菜,你的新菜连影子都没有。原因:最常见的有三种——Mod 没启用、缓存没更新、AddRecipe写在了错误的加载阶段。解决:第一步到 Mod 管理界面确认开关打开了;第二步把mods/settings里的配置删掉重进;第三步在modmain.lua第一行加print("custom stew loaded"),去log.txt看有没有这行输出。没有输出就说明整个文件没被加载,优先级比配方逻辑更高。
5.4 坑四:食材名字用错了,怎么煮都是失败料理
现象:你代码里写的是Ingredient("蜂蜜", 1),玩家往锅里放了真正的蜂蜜,但锅出的是失败料理。原因:饥荒的Ingredient接收的是 prefab 名,不是中文显示名。honey、smallmeat、berries这些才是合法值,中文名在解析时直接找不到对应食材,于是食材列表为空。解决:写配方时对着游戏数据里的东西对照查 prefab 名,常见做法是先print一下GLOBAL.prefabs或者直接查社区整理的食材对照表。这不算高级问题,但特别容易在食材一多之后犯。
5.5 坑五:两个 Mod 同时注册同名料理,一个把另一个静默覆盖
现象:装了两个自定义菜 Mod,游戏没报错,但其中某个 Mod 的菜完全消失了,或者两张图标长得一样的菜出现在不同分类里。原因:两个 Mod 的AddRecipe用了同一个内部名字。饥荒的配方注册表是一个全局字典,后注册的同名条目会覆盖先注册的。解决:给自己的所有料理和 prefab 起一个带前缀的名字,比如mycustomstew、mycustomdish01,不要用honeyham这种可能跟其他 Mod 撞车的通用名。同时养成习惯,在modinfo.lua里声明自己的命名空间,方便排查冲突。
6. 验证与进阶:让新菜在实测里按你预想的数值生效
代码能跑通只是第一步,数值验证才是让玩家不骂你的关键。我的习惯是先进游戏用控制台直接给角色喂一道新菜,然后看三维变化是否匹配。控制台里这样操作:
-- 控制台:给玩家一个自定义料理并打印饥饿值 c_give("customstew") local player = GetPlayer() print("hunger before: " .. tostring(player.components.hunger.current)) player.components.hunger:DoDelta(-100) print("hunger after empty food: " .. tostring(player.components.hunger.current))这段代码的思路是先把背包里已有的饥饿扣到接近 0,然后手动吃刚生成的那道菜。如果hungervalue设了 75,吃完后饥饿值应该接近 75 或封顶到最大值。如果你发现吃一口只回了 10,那不是代码错了,而是你可能在 prefab 里改的是另一个副本的数值,或者AddRecipe里传的食材组合根本没匹配到你这道菜——你需要看烹饪日志确认。
进阶玩法有两种我很推荐你尝试。第一种是「季节限定菜」:给料理挂一个 tag,然后在crockpot组件的cook回调里检查当前季节,只有冬天才允许做饭放进这道菜。第二种是「厨师专属菜」:利用performer角色标签,只有某角色能烹饪,做法是在AddRecipe后追加一个判断条件,检查玩家的 prefab 名。这两种做法的技术含量不高,但对玩家来说新鲜感很强,也是很多成熟 Mod 的标配。
我最后养成的习惯是:每加一道菜,就在modmain.lua顶部建一个常量表,把所有料理名、priority、数值集中管理,而不是散落在各个文件里。这样调试时改一个数进游戏测一次,比在十几个文件里来回翻省太多时间。希望帮到你——先跑通一道菜,再谈美化。
本文还有配套的精品资源,点击获取