# 🍔 烹饪经营游戏 - 核心架构与设计文档

欢迎来到我们的烹饪经营游戏项目!🎉

无论你是刚刚接触游戏开发的新手,还是刚加入我们团队准备一起大干一场的协作开发者,这篇文档都将是你最好的 “地图”。

在这款游戏里,我们要实现的核心玩法非常类似于《胡闹厨房》(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 中的 _recipeDictTryGetRecipe 方法
  • 💡 这是什么意思?
    当玩家拿着 “熟面条” 点击 “清汤” 时,游戏怎么知道它们能不能组合?
    如果用传统列表 (List),游戏需要从头到尾翻看每一条配方,这叫 O (N) 复杂度(效率低)。我们在游戏启动时,把 List 转成了 Dictionary (哈希字典)。
  • 🍔 通俗例子:List 就像是你一页一页翻书找特定的内容;而 Dictionary 是一本带超级目录的字典,你想找 “面条怎么合成”,直接翻到拼音 M,瞬间就能找到。在业界这叫 O (1) 的时间复杂度,性能起飞!

# ⚙️ 二、 核心系统模块拆解

熟悉了内功心法,我们来看看具体的系统(脚本)是怎么配合运作的。

# 🍅 1. 物资获取系统 (源头)

相关脚本IngredientBox (食材筐), ToolBox (工具箱), ToolStation (工具台)
业务逻辑
这是玩家获取物品的地方。它们的核心逻辑出奇的一致:

  1. 检查 PlayerHand.Instance 手里是不是空的。
  2. 如果是空的,克隆 (Instantiate) 对应的预制体交到玩家手里。
  • 💡 协作提示ToolStationToolBox 还做了一层视觉优化,当工具被拿走时,台面上的工具图片会隐藏,这给了玩家非常直观的物理反馈(“东西确实被我拿在手里了”)。

# 🍳 2. 烹饪与状态机系统 (加工)

相关脚本StoveController
业务逻辑
这是整个游戏最复杂的类,承担了食物从生到熟的蜕变。

  1. 多段烹饪:它不仅支持 “把生肉煮成熟肉”,还支持二次操作。比如 “土豆” 煮熟后,进入 WaitingForSecondAction 状态,必须等玩家拿 “搅拌棒” 来点一下,才能进入下一阶段。
  2. 数据交接:在烹饪完成时,锅会自动读取 FOODSO 中的 cookedData (熟食数据),把它包装成一个新的实体吐给玩家。
  • 💡 协作提示:如果你要增加新的厨具(比如烤箱、切菜板),完全可以照抄 StoveController 的状态机思路,稍微改改判定条件就行!

# 🍔 3. 闲置区与合成系统 (组合)

相关脚本HoldingSlot (托盘 / 闲置区)
业务逻辑
这是用来暂存物品的地方,同时它也是合成大锅炉
当玩家手里有东西,且托盘里也有东西时,会触发 TryCombineFood

  1. 问手里的食物:“你的配方本里,有针对托盘里食物的合成路线吗?”
  2. 问托盘的食物:“你的配方本里,有针对手里食物的合成路线吗?”
  3. 如果配对成功(比如 面包 + 汉堡肉),立刻销毁旧的两个物体,生成一个新的 “汉堡” 实体。

# 🧍 4. 顾客队列系统 (消耗)

相关脚本CustomerSpawner (生成器), CustomerController (顾客本体)
业务逻辑

  • 生成器:控制游戏难度。每隔几秒生成一个顾客,并限制排队上限( maxQueueSize )。
  • 视觉与逻辑解耦:这里用了一个非常高级的技巧。顾客在 UI 排队组件中是瞬间移动的(逻辑位置),但我们把顾客的图片( visualRoot )从本体里拆了出来,让图片每帧平滑地向逻辑位置移动( Vector3.MoveTowards )。这使得排队补位的动画极其丝滑。
  • 收银结算:玩家点击顾客上菜时,对比 handFoodData == currentOrderData ,正确则收走食物,顾客开心离开。

# 🛠️ 5. 生产力工具 (编辑器拓展)

相关脚本CSVToFoodSO
业务逻辑
这是写给游戏策划的 “魔法棒”。在业界,游戏后期的道具动辄几百上千个,让策划在 Unity 里一个个右键创建 FOODSO 会让他们抓狂。
所以我们写了这个工具:策划只需要在 Excel 里填表,导出 CSV,点击菜单栏 Tools -> 一键导入食材+配方 (CSV) ,代码会自动在后台生成所有的 FOODSO 文件和组合配方。

  • 💡 协作提示:策划同学请注意,修改数值直接改 CSV 然后重新导入即可,不要手动去修改零散的 Asset 资产,防止数据覆盖冲突。

# 🤝 三、 避坑指南

如果你今天要开始往这个项目里加新功能,请务必遵守以下代码规范:

  1. 绝对不要滥用 Update
    你能看到像 HoldingSlot 这样的类连 Update 都没有,全是事件驱动(OnClick)。保持这种好习惯,只有需要计时(如锅的进度条)或插值移动(如顾客排队)时才用 Update
  2. 不要硬编码字符串比较
    判断物品种类时,请用数据引用比对: if (handData == waterData) ,千万不要写 if (handData.foodName == "Water") 。引用的比对不仅快,而且不会因为策划手滑改了个名字就导致游戏逻辑崩溃。
  3. UI 射线拦截问题 (Raycast Target)
    我们项目里用了大量的拖拽和点击。当玩家手里拿着食物时,食物图片可能会挡住鼠标去点击背后的锅具。请留意 PlayerHand.HoldFood 里的 canvasGroup.blocksRaycasts = false; 这一句。如果你加了新的 UI 特效,记得不要让它挡住射线的判定。
更新于