# 🍔 烹饪经营游戏 - 核心架构与设计文档
欢迎来到我们的烹饪经营游戏项目!🎉
无论你是刚刚接触游戏开发的新手,还是刚加入我们团队准备一起大干一场的协作开发者,这篇文档都将是你最好的 “地图”。
在这款游戏里,我们要实现的核心玩法非常类似于《胡闹厨房》(Overcooked)或《老爹汉堡店》:玩家需要拿取食材、在厨具上进行加工(甚至需要二次操作)、将食材组合,最后把成品交给排队等待的顾客。
为了实现这套复杂的逻辑,同时保持代码干净、好扩展,我们使用了几个非常经典的业界设计模式和架构。别担心,接下来我会用最通俗的 “人话” 和生动的例子,带你把它们扒得干干净净!
# 🏗️ 一、 核心架构与业界黑话指南
在看具体的代码前,我们先来搞懂几个贯穿整个项目的核心概念。
# 1. 单例模式 (Singleton)
- 🔍 在哪里用到了?
PlayerHand.cs(玩家的手) - 💡 这是什么意思?
想象一下,现实世界里只有一个太阳,不管你在哪,抬头看到的都是同一个太阳。在游戏里,玩家的 “鼠标 / 手” 全局只有一个。为了让锅、菜板、顾客都能方便地知道 “玩家现在手里拿着什么?”,我们让PlayerHand变成了一个单例。 - 🍔 通俗例子:单例就像是厨房里的 “主厨”。如果谁想确认主厨手里拿了什么(拿取食物),不需要挨个问帮厨,直接在厨房大喊一声
PlayerHand.Instance,瞬间就能拿到数据。 - 💻 代码思路:在
Awake()里把自己赋值给静态变量Instance。任何脚本只需调用PlayerHand.Instance.HasFood()就能跨脚本瞬间完成判定。
# 2. 有限状态机 (Finite State Machine - 简称 FSM)
- 🔍 在哪里用到了?
StoveController.cs(灶台锅具) &CustomerController.cs(顾客) - 💡 这是什么意思?
状态机是游戏开发里最最最常用的神兵利器!它的核心思想是:一个物体在同一时刻只能处于一种状态,并且只有在满足特定条件时,才能切换到另一个状态。 - 🍔 通俗例子:十字路口的交通灯。它只能是红灯、黄灯或绿灯。绿灯亮了一定时间后,一定会变成黄灯(状态流转条件),它绝对不可能同时是红灯和绿灯。
- 🎮 游戏场景:
- 灶台 (
StoveController):空锅 (Empty) -> 烧水中 (Boiling) -> 待下锅 (Idle) -> 烹饪中 (Cooking) -> 挂起等待工具 (WaitingForSecondAction) -> 菜做好了 (Finished)。如果锅是空的,你往里扔面条是不行的,状态机会无情拦截你。 - 顾客 (
CustomerController):生成中 (Spawning) -> 等待点餐 (WaitingToOrder) -> 等待上菜 (WaitingForFood) -> 离开 (Leaving)。不在 “等饭” 状态的顾客,你是喂不进去食物的。
- 灶台 (
# 3. 数据驱动设计 (Data-Driven Design) & 可编程对象 (ScriptableObject)
- 🔍 在哪里用到了?
FOODSO.cs(食物数据) - 💡 这是什么意思?
新手最容易犯的错就是把数据写死在代码里(比如if(foodName == "Tomato") cookTime = 5;)。如果游戏有 100 种食材,代码就成了一座垃圾山。数据驱动就是把 “逻辑代码” 和 “数值” 分开。 - 🍔 通俗例子:游戏引擎是 “MP3 播放器”,而 ScriptableObject (SO) 就是 “光盘”。你想换一首歌,不需要把播放器拆了重造,只需要换一张光盘即可。
- 💻 代码思路:
FOODSO里存了食物的名字、图片、需要多久煮熟。策划人员不需要懂代码,只需要在 Unity 面板里新建一个FOODSO文件填入数值,程序逻辑就能完美兼容。
# 4. 空间换时间 (哈希表 O (1) 极速查找)
- 🔍 在哪里用到了?
FOODSO.cs中的_recipeDict和TryGetRecipe方法 - 💡 这是什么意思?
当玩家拿着 “熟面条” 点击 “清汤” 时,游戏怎么知道它们能不能组合?
如果用传统列表 (List),游戏需要从头到尾翻看每一条配方,这叫 O (N) 复杂度(效率低)。我们在游戏启动时,把 List 转成了Dictionary(哈希字典)。 - 🍔 通俗例子:List 就像是你一页一页翻书找特定的内容;而 Dictionary 是一本带超级目录的字典,你想找 “面条怎么合成”,直接翻到拼音 M,瞬间就能找到。在业界这叫 O (1) 的时间复杂度,性能起飞!
# ⚙️ 二、 核心系统模块拆解
熟悉了内功心法,我们来看看具体的系统(脚本)是怎么配合运作的。
# 🍅 1. 物资获取系统 (源头)
相关脚本: IngredientBox (食材筐), ToolBox (工具箱), ToolStation (工具台)
业务逻辑:
这是玩家获取物品的地方。它们的核心逻辑出奇的一致:
- 检查
PlayerHand.Instance手里是不是空的。 - 如果是空的,克隆 (Instantiate) 对应的预制体交到玩家手里。
- 💡 协作提示:
ToolStation和ToolBox还做了一层视觉优化,当工具被拿走时,台面上的工具图片会隐藏,这给了玩家非常直观的物理反馈(“东西确实被我拿在手里了”)。
# 🍳 2. 烹饪与状态机系统 (加工)
相关脚本: StoveController
业务逻辑:
这是整个游戏最复杂的类,承担了食物从生到熟的蜕变。
- 多段烹饪:它不仅支持 “把生肉煮成熟肉”,还支持二次操作。比如 “土豆” 煮熟后,进入
WaitingForSecondAction状态,必须等玩家拿 “搅拌棒” 来点一下,才能进入下一阶段。 - 数据交接:在烹饪完成时,锅会自动读取
FOODSO中的cookedData(熟食数据),把它包装成一个新的实体吐给玩家。
- 💡 协作提示:如果你要增加新的厨具(比如烤箱、切菜板),完全可以照抄
StoveController的状态机思路,稍微改改判定条件就行!
# 🍔 3. 闲置区与合成系统 (组合)
相关脚本: HoldingSlot (托盘 / 闲置区)
业务逻辑:
这是用来暂存物品的地方,同时它也是合成大锅炉!
当玩家手里有东西,且托盘里也有东西时,会触发 TryCombineFood :
- 问手里的食物:“你的配方本里,有针对托盘里食物的合成路线吗?”
- 问托盘的食物:“你的配方本里,有针对手里食物的合成路线吗?”
- 如果配对成功(比如 面包 + 汉堡肉),立刻销毁旧的两个物体,生成一个新的 “汉堡” 实体。
# 🧍 4. 顾客队列系统 (消耗)
相关脚本: CustomerSpawner (生成器), CustomerController (顾客本体)
业务逻辑:
- 生成器:控制游戏难度。每隔几秒生成一个顾客,并限制排队上限(
maxQueueSize)。 - 视觉与逻辑解耦:这里用了一个非常高级的技巧。顾客在 UI 排队组件中是瞬间移动的(逻辑位置),但我们把顾客的图片(
visualRoot)从本体里拆了出来,让图片每帧平滑地向逻辑位置移动(Vector3.MoveTowards)。这使得排队补位的动画极其丝滑。 - 收银结算:玩家点击顾客上菜时,对比
handFoodData == currentOrderData,正确则收走食物,顾客开心离开。
# 🛠️ 5. 生产力工具 (编辑器拓展)
相关脚本: CSVToFoodSO
业务逻辑:
这是写给游戏策划的 “魔法棒”。在业界,游戏后期的道具动辄几百上千个,让策划在 Unity 里一个个右键创建 FOODSO 会让他们抓狂。
所以我们写了这个工具:策划只需要在 Excel 里填表,导出 CSV,点击菜单栏 Tools -> 一键导入食材+配方 (CSV) ,代码会自动在后台生成所有的 FOODSO 文件和组合配方。
- 💡 协作提示:策划同学请注意,修改数值直接改 CSV 然后重新导入即可,不要手动去修改零散的 Asset 资产,防止数据覆盖冲突。
# 🤝 三、 避坑指南
如果你今天要开始往这个项目里加新功能,请务必遵守以下代码规范:
- 绝对不要滥用 Update:
你能看到像HoldingSlot这样的类连Update都没有,全是事件驱动(OnClick)。保持这种好习惯,只有需要计时(如锅的进度条)或插值移动(如顾客排队)时才用Update。 - 不要硬编码字符串比较:
判断物品种类时,请用数据引用比对:if (handData == waterData),千万不要写if (handData.foodName == "Water")。引用的比对不仅快,而且不会因为策划手滑改了个名字就导致游戏逻辑崩溃。 - UI 射线拦截问题 (Raycast Target):
我们项目里用了大量的拖拽和点击。当玩家手里拿着食物时,食物图片可能会挡住鼠标去点击背后的锅具。请留意PlayerHand.HoldFood里的canvasGroup.blocksRaycasts = false;这一句。如果你加了新的 UI 特效,记得不要让它挡住射线的判定。