简介:一份面向饥荒模组开发入门者与自定义食物需求玩家的《饥荒》烹饪锅食物Lua源码包,围绕游戏中烹饪锅进阶食物制作这一典型需求,完整覆盖从物品定义、食物属性设置、食谱注册到特殊效果触发的全套流程,并糅合模组兼容与调试思路,适合作为对照学习和二次开发基础。压缩包共10个文件,以Lua脚本为核心逻辑,配合tex/xml配置和png图标素材,整体仅17KB,内容紧凑但结构清晰;主入口脚本负责加载与注册,食物定义脚本管理属性,食谱脚本实现烹饪合成,图标与文本资源则保证游戏内表现,可直接加载运行观察实际效果。目前已有373人学习下载,这份源码既演示了饥荒Mod的标准目录组织与常用API调用方式,也给出自定义食物接入烹饪锅的完整范式,稍作修改即可扩展出更多独特食谱,尤其适合希望通过实践掌握Lua游戏脚本的开发者。
1. 饥荒烹饪锅的“源码”:把隐藏的配方规则变成可复现的匹配引擎
饥荒的烹饪锅在玩家眼里是玄学:同样的几块食材,放进去有的出肉丸,有的出失败料理,多一块蜂蜜就可能变成另一道菜。但在“制作饥荒烹饪锅食物源码”这个标题指向的方向里,烹饪锅不过是一张带优先级的条件匹配表——每个食材挂着一组“度”数值,每道菜定义一组门槛和黑名单,引擎把所有食材组合枚举出来,按优先级找到第一个满足条件的配方。这件事做透之后,做饥荒MOD加自定义料理、做配饭计算器帮玩家找最优膳食、甚至把同一套机制搬进其他合成类游戏,都是同一份代码的换皮。这篇文章会从度系统讲到可运行的Python匹配引擎,再讲优先级、边界和四个最常踩的坑,全程给可以直接复现的代码。
2. 配方机制先行:度系统、食材标签与优先级
2.1 度系统:为什么配方依赖“数值条件”而不是“食材列表”
饥荒的食材体系里没有“这张配方必须放肉丸”这种写法。每个食材携带一组标签,每个标签带数值,这些数值累加起来就是这道菜的“度向量”。比如一块小肉挂着meat: 1,一个浆果挂着fruit: 1,一块大肉则挂着meat: 3。配方匹配时看的是“肉度总量是否大于等于1”,而不是“是否包含某块肉”。
这个设计的核心价值是可替换性。玩家可以用小肉、大肉、怪兽肉、蛙腿任意组合去凑肉度,只要总量满足门槛就能出肉丸。如果配方写成固定列表,那么食材组合的灵活性就全没了,游戏里“缺什么用什么顶”的玩法也会跟着消失。你在实现源码时,应该把每个食材建模成一个字典,键是度名称,值是数值贡献量:
FOOD_ITEMS = { "small_meat": {"meat": 1, "monster": 0, "tags": ["meat_src"]}, "big_meat": {"meat": 3, "monster": 0, "tags": ["meat_src"]}, "monster_meat": {"meat": 1, "monster": 1, "tags": ["meat_src"]}, "berry": {"fruit": 1, "tags": ["plant"]}, "stick": {"fill": 1, "tags": ["filler"]}, # 填充物,不计任何度 "honey": {"sweet": 1, "tags": ["sweet_src"]}, "egg": {"egg": 1, "tags": ["egg_src"]}, "fish": {"fish": 1, "tags": ["fish_src"]}, }monster这个标签在这里不是度,是一个布尔性质的标记,用来给禁用怪兽肉的配方做黑名单拦截。fill表示填充量,只用于判断“这道菜是否塞满了食材槽”,不参与配方条件判断。实现时要注意:填充物(树枝、冰块、浆果这类低价值食材)虽然不加度,但会让失败料理的概率大幅降低——这是烹饪锅“保底机制”的一部分,后面避坑章节会专门讲。
2.2 配方表的数据结构:强制项、黑名单与优先级
配方表是整个引擎的核心数据。每道菜需要描述四个维度:入口条件(哪些度达到多少)、必须包含的食材(白名单)、禁止出现的食材或标签(黑名单)、以及在同一组食材同时命中多道菜时谁说了算。我一般这样建模:
RECIPES = [ { "id": "meatballs", "name": "肉丸", "priority": 100, "require": {"meat": 1}, "forbidden": [], # 空表示不限制 "must": [], # 空表示无强制食材 }, { "id": "honey_ham", "name": "蜜汁卤肉", "priority": 120, "require": {"meat": 2}, "forbidden": [], "must": ["honey"], # 必须包含蜂蜜 }, { "id": "fish_sticks", "name": "鱼条", "priority": 110, "require": {"fish": 1}, "forbidden": ["monster"], "must": ["stick"], # 必须带树枝 }, { "id": "wet_goop", "name": "失败料理", "priority": 0, "require": {}, "forbidden": [], "must": [], }, ]priority是这里最容易被忽略的字段。同一组食材完全可能同时满足多个配方:比如一块鱼加三块树枝,肉度是0、鱼度是1,它同时够到“鱼条”的门槛,也够到“失败料理”的兜底门槛,如果你按配方表的插入顺序从头匹配,会先命中低优先级的兜底菜。正确做法是先把配方按priority降序排列,再逐条检查,取第一个命中的结果。forbidden黑名单的检查要放在数值检查之后,因为黑名单拦截的优先级高于区间门槛,但低于must白名单的强制校验。
3. 最小可运行引擎:用 Python 实现配方匹配
3.1 遍历组合与度向量累加:核心代码
烹饪锅允许放入1到4种食材,同一个槽位可以放多份同种食材。因此匹配的第一步是从食材背包里枚举所有可能的组合,计算每个组合的度向量。这里不需要做全排列,因为配方只关心“这一锅里有什么”,不关心食材放入的顺序。用标准库的itertools.combinations_with_replacement就可以覆盖“同种食材放多份”的情况:
from itertools import combinations_with_replacement from collections import defaultdict def calc_vector(items): """计算一组食材的度向量,items 是食材 id 列表""" vec = defaultdict(int) for item in items: for key, val in FOOD_ITEMS[item].items(): if isinstance(val, int): vec[key] += val return vec def enumerate_combos(ingredients, max_slots=4): """枚举背包食材的所有组合,返回 (组合, 度向量) 列表""" results = [] ids = list(ingredients.keys()) # ingredients: {食材id: 数量} pool = [] for item_id, count in ingredients.items(): pool.extend([item_id] * count) for size in range(1, max_slots + 1): for combo in combinations_with_replacement(pool, size): results.append((combo, calc_vector(combo))) return resultscombinations_with_replacement的选择是有讲究的。它允许同一个元素重复取,正好对应“4块小肉”这种放法,同时又不会把(小肉, 浆果)和(浆果, 小肉)当成两种组合重复枚举。传入的ingredients是带数量的字典,这样玩家背包里“3块肉、5个浆果”能被展开成池子再组合,不会漏掉少于4格的组合。需要注意:组合枚举的复杂度是背包食材种类数的4次方,玩家背包通常几十种食材,量级完全可接受。
3.2 按优先级匹配的完整循环
拿到所有组合的度向量之后,匹配循环的核心是“降序优先 + 条件逐项检查”。我把匹配逻辑拆成独立的check_recipe函数,方便后续加新配方时单独调试:
def check_recipe(recipe, vec, combo): """检查一组食材是否满足单个配方的条件""" for degree, threshold in recipe["require"].items(): if vec.get(degree, 0) < threshold: return False if recipe["must"]: recipe_items = set(recipe["must"]) if not recipe_items.issubset(set(combo)): return False if recipe["forbidden"]: # 黑名单检查:任何一个食材携带禁止标签,直接拒绝 for item in combo: item_data = FOOD_ITEMS[item] for tag in recipe["forbidden"]: if item_data.get(tag, 0) > 0 or tag in item_data.get("tags", []): return False return True def match_recipe(ingredients, recipes=RECIPES): """返回 (最佳配方, 对应食材组合, 度向量)""" sorted_recipes = sorted(recipes, key=lambda r: r["priority"], reverse=True) for combo, vec in enumerate_combos(ingredients): for recipe in sorted_recipes: if check_recipe(recipe, vec, combo): return recipe, combo, vec # 理论上不会走到这里,因为兜底菜 priority=0 一定会命中 return None, None, Nonesorted_recipes这一步必须在每次匹配时执行,而不是模块加载时执行一次。因为配方表可能在运行期被MOD动态修改(比如你想做一个可热加载的配方工具),固定排序会引入并发下的脏状态。check_recipe里三个检查的顺序也有讲究:先查数值门槛,再查must白名单,最后查forbidden黑名单。数值门槛是最便宜的字典查询,先做可以快速淘汰大多数配方;黑名单检查涉及遍历组合里的每个食材,代价相对高,放最后。
3.3 跑通一个完整样例:从食材到料理
把前面的模块拼起来,用一组经典食材验证:两块小肉、一个蜂蜜、一个树枝。这组食材在游戏里应该出蜜汁卤肉,因为肉度总和是2,且强制要求蜂蜜。跑一下引擎:
def main(): bag = { "small_meat": 2, "honey": 1, "stick": 1, "berry": 3, } recipe, combo, vec = match_recipe(bag) if recipe: print(f"匹配结果: {recipe['name']}") print(f"食材组合: {combo}") print(f"度向量: {dict(vec)}") if __name__ == "__main__": main()输出结果应该是“密汁卤肉”,组合是(蜂蜜, 小肉, 小肉, 树枝),度向量里meat是2。这里有个容易被新手忽略的点:树枝作为填充物进入了组合但没贡献任何度,它只是占满第4个槽位。如果背包换成“蜂蜜、小肉、浆果、浆果”,肉度只有1,就会优先命中“肉丸”而不是“蜜汁卤肉”——因为肉丸的门槛只要求肉度1,优先级虽然低于蜜汁卤肉,但蜜汁卤肉的肉度2门槛没被满足。这个例子建议你亲手跑一遍,理解“门槛拦截”和“优先级排序”是两个独立维度。
4. 配方匹配的四大避坑点:现象、原因与解决
4.1 填满4格却出失败料理:度向量没算对
现象:玩家放入“树枝、浆果、浆果、浆果”,锅里出来的是失败料理。开发者查配方表发现明明有肉丸,肉度要求是1,怎么没匹配上。
原因:浆果的度是fruit: 1,不是meat: 1。树枝的度是fill: 1,也不贡献肉度。这组食材的肉度为0,连肉丸的最低门槛都没摸到,自然落到兜底菜。
解决:在匹配引擎的可视化调试模式里,把每个组合的度向量直接打印出来。我一贯的做法是在calc_vector里加一个debug=True参数,输出“组合 + 度向量”的对照表,看哪个组合的哪个度差了多少。血泪经验是:度向量的键名一定要全局统一,比如meat还是meat_point,这决定了配方表门槛能不能正确命中,建议在项目里用常量枚举统一定义。
4.2 与Wiki配方一致却匹配出别的菜:优先级顺序反了
现象:照着Wiki配方“一鱼、两菜、一树枝”放进引擎,输出的是蔬菜汤而不是鱼条。
原因:这组食材同时满足两个配方的门槛——鱼度达到1,蔬菜度也达到1。如果你的代码按插入顺序遍历配方表,而蔬菜汤恰好排在鱼条前面,就会先命中蔬菜汤。Wiki只告诉你“这是鱼条配方”,没告诉你“它是所有命中配方里优先级最高的那个”。
解决:全局按priority降序排序后匹配,并且这个排序不能是稳定的插入排序兜底。我给配方表加priority字段时把基础菜设在80到120区间,特殊菜像蜜汁卤肉设120以上,失败料理固定0。调配方优先级时,必须同时检查所有食材组合的回归结果,避免“修好鱼条、打崩肉丸”的连锁翻车。
4.3 做MOD时客户端与服务器结果不一致:食材ID与同步问题
现象:多人游戏里,客户端预览显示“肉丸”,但服务器实际存档写入的是“失败料理”。玩家重进房间后菜变了。
原因:饥荒的烹饪逻辑在网络架构里是服务器权威计算。客户端本地跑了一份匹配预测,但两端食材的ID映射不一致——客户端用了本地化名称当键,服务器用的是内部PrefabID。键名对不上,度向量算出来就不一样,匹配结果自然分叉。
解决:引擎内部一律用稳定ID做键,显示名称只存在于UI层。食材ID建议直接采用游戏内部的PrefabID字符串(比如smallmeat、berries),不要用中文名或本地化Key。多人游戏里烹饪结果必须以服务器端计算为准,客户端的预览结果只能作展示,不能参与存档逻辑。
4.4 加一个MOD食材,旧配方集体错乱:度数值没有全局校验
现象:新增一个MOD食材“蜜汁肉块”,给它标了meat: 2, sweet: 1。结果玩家放“蜜汁肉块、树枝、树枝、树枝”时,出的是蜜汁卤肉——因为蜜汁卤肉要求肉度2且必须含蜂蜜,而蜜汁肉块同时满足了这两个条件。
原因:新食材的度向量叠加后冲过了多个配方的门槛,原本需要玩家刻意凑的“肉度2 + 蜂蜜”组合,被单一食材无成本满足了。
解决:任何新增食材都必须跑一次全量组合回归,确认它与现有配方表没有产生非预期命中。我通常写一个自动化检查脚本,枚举“新食材 + 任意填充物”的所有组合,输出命中的配方列表,人工过一遍确认没有违反设计意图的菜。另一个实用习惯是给每个MOD食材设置一个“复杂度系数”,禁止一个食材同时携带两种以上的主度数值,从源头规避这种错乱。
5. 把引擎做成真能用的工具:配方表外部化与价值计算
5.1 配方表JSON化:改配方不动代码
引擎跑通之后,第一个该做的事就是把配方表从Python文件里挪到外部JSON。饥荒的配方数值经常因为版本更新而调整,写死在代码里意味着每次改都要动源码、重新分发。JSON外部化之后,MOD作者或者工具使用者只需要改数据文件,引擎代码一行不动。
[ { "id": "meatballs", "name": "肉丸", "priority": 100, "require": {"meat": 1}, "forbidden": [], "must": [] }, { "id": "wet_goop", "name": "失败料理", "priority": 0, "require": {}, "forbidden": [], "must": [] } ]import json def load_recipes(path="recipes.json"): with open(path, "r", encoding="utf-8") as fp: raw = json.load(fp) # 基础校验:优先级必须存在,require必须可解析 for item in raw: if "priority" not in item: raise ValueError(f"配方缺少priority字段: {item.get('name', 'unknown')}") item.setdefault("require", {}) item.setdefault("forbidden", []) item.setdefault("must", []) return raw加载后的校验环节不能省。防御性编程的底线是:配方表里缺字段宁可直接报错,也不要默默填空默认值。setdefault在这里只补可选字段,require和priority这种关键字段缺了就要抛异常。还有一个参数值得注意:加载后应该验证所有require的键名在食材表的度字典里真实存在,否则一个拼写错误会让整个配方的条件判断永远不命中,这种隐蔽bug比报错难查得多。
5.2 从“能出菜”到“最优膳食”:食材价值计算
引擎能匹配出菜之后,玩家真正想要的是“我这包食材怎么做最划算”。饥荒里每道菜有饱食、血量、理智三围属性,价值计算的问题就变成:枚举所有合法组合,按目标维度对结果做排序。由于烹饪锅上限只有4格,组合空间非常小,直接穷举是最高效的解法:
def best_dishes(ingredients, nutrition_table, top_n=5, rank_by="hunger"): """按指定维度返回最优料理排行""" candidates = [] for combo, vec in enumerate_combos(ingredients): recipe, _, _ = match_recipe(ingredients_from_combo(combo)) if recipe and recipe["id"] != "wet_goop": stats = nutrition_table.get(recipe["id"], {}) score = stats.get(rank_by, 0) candidates.append((score, recipe["name"], combo)) candidates.sort(reverse=True, key=lambda x: x[0]) return candidates[:top_n]这里ingredients_from_combo需要把组合转回带数量的字典再喂给匹配引擎,避免match_recipe内部重新枚举时丢失约束。实际项目里我会把nutrition_table也JSON化,因为不同版本的三围数值差异很大,数据与逻辑分离是这类工具能长期维护的前提。排序维度可以是饱食、血量、理智,或者综合加权分。综合分建议做成可配置的权重向量,比如{"hunger": 1.0, "health": 0.8, "sanity": 0.5},方便玩家按自己的游戏阶段调整偏好。
5.3 做成MOD还是本地工具:落地选型
同一个引擎有三种落地形态,投入产出比差很多。本地工具最轻,Python脚本加CLI就能跑,适合查询“给定背包的最优菜谱”,缺点是玩家不能边玩边用。游戏MOD的体验最沉浸,但需要把引擎移植到Lua,且要处理客户端与服务端的计算同步,工程量最大。在线查询站介于两者之间,前端重写一遍引擎,数据用JSON共享,适合做社区工具,但需要服务器和维护成本。
我的建议是按使用频率和受众判断。如果你只是自用加分享给几个朋友,本地工具加一个简单CLI足够;如果你想做社区热门工具,优先考虑在线查询加静态配方表;只有在你想改游戏内行为(比如新增料理、调整配方)时才值得做MOD。做MOD前一定要确认引擎的度系统能对接游戏原生食材的标签体系,否则每个新食材都要在两端写映射,维护成本会吞掉所有乐趣。
6. 必留的护身符:配方回归测试与断言
引擎做到后面,最怕的不是写不出新配方,而是改一个优先级参数把老配方全打崩。我这边吃过一次大亏:为了新增一道节日料理,把蜜汁卤肉的priority从120调到130,结果“蜂蜜、小肉、小肉、树枝”直接匹配到节日料理,玩家背包里所有蜜汁卤肉配方都废了。那次之后我养成了把已知配方固化成断言的习惯,每次改配方表先跑一遍回归:
def test_known_recipes(): # (背包, 期望料理id) 的黄金用例集 cases = [ ({"small_meat": 1, "berry": 3}, "meatballs"), ({"small_meat": 2, "honey": 1, "stick": 1}, "honey_ham"), ({"fish": 1, "stick": 1, "berry": 2}, "fish_sticks"), ({"stick": 1, "berry": 3}, "wet_goop"), ] for bag, expected in cases: recipe, _, _ = match_recipe(bag) assert recipe["id"] == expected, f"{bag} 期望{expected},实际{recipe['id']}" def test_priority_order(): # 同组食材同时命中多道菜时,必须按priority取最高 bag = {"big_meat": 1, "monster_meat": 1, "honey": 1, "stick": 1} recipe, _, _ = match_recipe(bag) # 大肉+怪兽肉+蜂蜜+树枝:肉度4,必须含蜂蜜,优先级最高的菜胜出 assert recipe["priority"] >= 120断言用例集的价值不只在防回归。它本身就是一份活文档——新接手的人看一遍这些(背包, 期望结果)对,就能理解配方的边界条件比读十页说明都快。我自己的习惯是每新增一个配方,必须同时往用例集里加两条数据:一条是“能命中的组合”,一条是“差一个度就命中的反例”,确保新配方不会侵蚀到旧配方的空间。
回头看整个实现,核心其实只有两件事:用度向量把食材变成可计算的数值,再用优先级让多配方冲突时有一个确定性结果。避坑点也都围绕着这两件事展开——度向量的键名要全局统一、配方的门槛和黑名单要分开校验、新食材要过回归。每次调整优先级或新增配方前跑一遍断言,这个习惯比任何代码评审都能更早拦住问题。希望帮到你。
本文还有配套的精品资源,点击获取