十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity6美食经营游戏Part3:用数据驱动和状态管理告别“会做菜不会做游戏”

Unity6美食经营游戏Part3:用数据驱动和状态管理告别“会做菜不会做游戏” Unity6 美食经营类游戏教程做到 Part3通常是整个系列里最劝退的阶段。Part1 和 Part2 还能靠搭场景、调材质、写一两个交互动作带来新鲜感到了 Part3往往要开始把菜单、菜谱、订单、金币、库存、升级这些东西真正串成一个可以反复玩下去的单机循环。很多人在这一周突然发现自己“会做菜”但“不会做游戏”demo 能跑却玩不下去按钮有响应但状态总是对不上加了新菜品改起来像打地鼠。这篇文章想聊一个可能和你直觉相反的观点Part3 的价值不在功能数量而在于你第一次被迫建立数据抽象和工程化思维。单机版美食经营游戏能不能做完不是看你会不会做炒菜动画而是看你敢不敢把“一份订单从下单到结算”的状态流转管成一条不会乱的数据流。1. 先搞清楚美食经营游戏的核心循环再开始写 Part3 的功能1.1 单机版的循环从来不是“做菜”而是资源与状态的往返先定义一下这里说的“美食经营类游戏单机版”。不讨论联机、排行榜、真实玩家同桌这类内容只有玩家本地和 AI 顾客交互最终目标是经营一个餐厅持续获得收益、解锁新菜谱、升级设备或装扮餐厅。把范围缩小之后你会发现这类游戏的底层循环其实非常统一顾客进入餐厅查看菜单产生一个订单。玩家根据订单内容选择菜品进入制作流程。制作需要消耗食材并经过一个持续时间可以加入失败率或操作小游戏。菜品完成后交给顾客玩家获得金币和评价。金币用来购买食材、解锁菜谱、升级厨房设备解锁之后又会吸引新顾客或提高订单复杂度。听起来不复杂但为什么很多人在 Part3 做着做着就写不下去因为教程的每个功能片段是分开演示的而游戏需要它们同时运行。分开看每个按钮和状态都没问题合在一起就变成“互踢皮球”。顾客在等菜订单系统不知道玩家正在做饭玩家做了两份菜上菜系统不知道该先给谁金币加了库存系统没有扣食材。所有问题都指向同一个点你还没有把核心循环中的对象和状态关系建立清楚。1.2 Part3 真正的分水岭从“表现层”走向“规则层”很多新手在 Part3 时会迫切地想给菜加更多动画比如切菜、倒油、翻炒。这部分当然重要但不应该成为第一个任务。一套游戏规则如果不能在数据结构上自洽动画做得越好最后推翻重写时越心疼。我更建议在做新功能前先把整局游戏的流程画出来。不用画得很专业一张纸一支笔就可以玩家进入游戏后第一个看到什么界面什么时候可以开始接单一次最多同时接几单订单从产生到完成要经过哪几个状态制作过程中如果切到别的界面计时还在吗食材不够时是禁止下单还是允许玩家超时结算界面什么时候弹出这些问题看起来像策划文档但实际上会直接暴露代码需要哪些数据结构。你在 Part3 写的第一个类不应该是“菜刀脚本”而应该是“订单状态”。因为美食经营游戏看起来是一个即时反应游戏本质上却是一套离散状态流转系统。把状态流转想清楚后面做 UI、做动画、做音效都只是在不同的层上反映这套状态。1.3 单向数据流单机版也有必要单机版没有服务器你仍然可以把数据流设计得像联机版一样清晰。最简单的做法是所有玩家操作都通过 UI 或输入系统发出一个意图系统的管理器接收意图后修改数据数据变化后通过事件或监听接口通知 UI 刷新。不要允许 UI 脚本里直接修改金币、食材、订单状态这样的全局数据。为什么因为单机版虽然只有一个玩家但没有单向数据流的约束后代码很容易变成“谁都能改谁都在改”。今天你在金币按钮回调里加了 10 块钱明天你在出餐逻辑里又加了 15 块后天存档系统读金币时发现数值和日志对不上。排查时你根本不知道该信哪一行代码。单向数据流不能解决所有问题但能把变化集中到有限的几个系统边界里这对单机项目后期维护是最友好的。一个很直接的判断标准如果某个数据会被三个以上脚本直接读写它就应该被放进一个专门的 Manager并由 Manager 统一负责更新和通知。2. 把菜单、食材、菜谱拆成“数据”而不是写成散落的变量2.1 从写死到数据驱动加一道菜不再需要重新改代码Part1 和 Part2 阶段很多教程会让你为了演示功能直接在脚本里写“番茄炒蛋”“牛肉面”这样的例子。一开始没问题但当菜品数量超过 5 个你会发现每个商品按钮的点击处理函数都很像只是 ID 不同。这时候就该把菜品从代码里抽出来变成数据。常见做法是在 Unity6 里使用 ScriptableObject 来定义菜的配置。比如// DishConfig.cs using System.Collections.Generic; using UnityEngine; public enum FoodCategory { MainFood, Drink, Dessert } [CreateAssetMenu(fileName DishConfig, menuName Game/DishConfig)] public class DishConfig : ScriptableObject { public string dishName; public FoodCategory category; public Sprite icon; public ListIngredientStack ingredients; public float cookTime; public int price; }再用一个类来表示食材数量[System.Serializable] public class IngredientStack { public IngredientType ingredientType; public int count; }这样一个新菜品就对应一个 .asset 文件你不再需要复制和修改大量 UI 脚本。对于一个美食经营游戏这是 Part3 阶段最能直接减少重复工作的重构。2.2 配置数据和运行时数据要分开ScriptableObject 适合放配置但绝对不是库存和订单状态的正确存放位置。曾经见过新手把金币直接存在一个 ScriptableObject 字段里并让它跨场景生存这会导致存档、重置游戏、多存档时非常麻烦。更稳妥的做法是用 ScriptableObject 保存不会在运行中改变的配置数据菜谱、食材说明、初始价格、图标。用普通的 C# 类或 MonoBehaviour 保存运行时数据当前金币、当前库存、当前订单列表、玩家解锁状态。运行开始时从配置数据初始化运行时数据。两者分离之后你才能反复开始新游戏而不会把上一次游戏的残留状态带到新一局。存档恢复时也只需要重建运行时数据配置数据直接从资源系统加载。2.3 用列表还是字典给初学者的建议美食经营游戏里经常需要用菜 ID 查菜谱用食材 ID 查库存。C# 的 Dictionary 很直观但在 Unity 的 JsonUtility 序列化时不能直接保存字典。如果未来准备用 JsonUtility 做存档我更建议运行时用 Dictionary 方便查询但保存时转换为 List或者干脆用 List 存储查询时用 LINQ 或循环查找。只要菜品数量在几十个以内线性查找的性能损耗完全可以忽略不必从一开始就背着“字典序列化”的复杂度。这个取舍在 Part3 阶段尤其重要因为你会发现很多看起来优雅的方案最后都会被工具链限制拉回现实。3. 单机版也要有“订单系统”重点在于判定和计时3.1 订单状态先定义再实现做美食经营游戏时很多新手会直接从“点击菜谱按钮就显示一盘菜”开始写。但稍微复杂一点的需求出现后比如“客人等待超过 30 秒会生气离开”你就被迫去处理状态。为什么不先定义状态呢一个简单的订单状态枚举可以是这样public enum OrderState { WaitCook, // 等待玩家开始制作 Cooking, // 玩家正在制作 ReadyToServe, // 制作完成等待上菜 Served, // 已上菜订单完成 Expired, // 超时或取消 }一个订单从创建到结束就是在这些状态之间移动。每进入一个新状态可以做几件事改变 UI 显示、更新计时器、播放音效、扣除食材、增加金币。这样你会发现“订单”不再是一堆散乱的逻辑而是一个有明确生命周期的对象。3.2 用算法来控制“多订单并行”的时序单机经营游戏的难点在于同时存在多位顾客、多份订单。玩家可能同时接到三个订单一个正在做一个等待一个已经好了但还没端上桌。这时你需要的不是复制三份 UI 逻辑而是一个订单管理器用列表保存所有订单实例每帧检查状态。比如public class OrderManager : MonoBehaviour { public ListOrderInstance orders new ListOrderInstance(); void Update() { float dt Time.deltaTime; for (int i orders.Count - 1; i 0; i--) { bool done orders[i].Tick(dt); if (done) orders.RemoveAt(i); } } }对应地OrderInstance里可以有一个Tick方法根据当前状态更新计时和逻辑超时则推进到失败状态如果状态是ReadyToServe等待玩家点击上菜即可。这段逻辑不复杂但通过一个管理器统一驱动比每个订单在自己的协程里各跑各的更容易控制。协程当然可以用但调试状态机时集中驱动会清楚很多。3.3 计时、超时和失败条件别用帧计数计时逻辑最常见的坑是用frameCount或者让每个订单自己驱动计时结果不同的顾客等待时间忽快忽慢。更好的方案是统一使用Time.deltaTime累加剩余时间。即使使用协程做倒计时也尽量在同一个OrderManager中统一管理避免绕来绕去的嵌套协程。另外要小心暂停游戏时的时间源。如果你使用Time.timeScale 0做暂停菜单那么默认的Time.deltaTime会变成 0倒计时也会暂停。如果希望顾客在暂停时仍然等待那就要改用独立的时间源。单机版通常建议暂停时所有计时都停否则玩家切出游戏回来会发现客人全跑了体验很差。计时问题不用都用协程写。先用 Update 里累加 deltaTime 完成一版等逻辑稳定后再决定要不要优化否则你会在调试时被嵌套协程绕晕。3.4 UI 反馈是状态变化的第一现场状态系统再正确如果 UI 不刷新玩家只会觉得“游戏坏了”。在本地单机版里我推荐使用事件或委托来广播状态变化。例如订单状态变成ReadyToServe时可以触发事件UI 上的订单卡片再次刷新金币变化时也发事件顶栏货币控件更新。不需要一帧一帧地轮询 UI 数值。这样一个订单完成时玩家看到的状态变化链是玩家点击“上菜”按钮。按钮触发OrderManager.ServeOrder(orderId)。订单状态改为Served。金币系统增加收益。事件通知顶栏金币刷新、通知订单卡片消失、通知经验或评分系统记录。整个过程没有谁“直接修改显示”但最终所有显示都正确。这也是最容易排查和扩展的反馈链路。4. 存档系统是单机版最容易低估的部分4.1 存什么不存什么先回答一个基础问题单机版美食经营游戏大家希望存档能保留什么一般是金币、已解锁菜谱、设备等级、餐厅装扮、关卡进度和设置。不该存的是运行时临时对象比如某位顾客当前站在哪、当前订单还剩几秒。如果游戏退出通常只要求回到主界面再进入游戏时重新生成一批顾客和订单。如果把运行时订单也写入存档反而会在恢复时面临不同步问题。所以设计存档时先做减法。写一个 SaveData 类只保存“长期状态”不保存“当前帧快照”。这也会让你想明白哪些数据属于玩家资产哪些只是当前一局的临时表现。4.2 用 JSON 写一份最小存档Unity6 里做单机存档最常见的是 JSON。使用JsonUtility写起来方便但限制是不能直接序列化字典字段必须是[Serializable]。一个最小 SaveData 是这样[System.Serializable] public class GameSaveData { public string saveVersion 1.0; public int gold; public Liststring unlockedDishIds; public Listint equipmentLevels; }写盘时把金币、列表等填入序列化后保存到Application.persistentDataPath下的某个文件。读盘时先判断文件是否存在再反序列化。这里有一个容易踩的坑JsonUtility.FromJson如果字段名不匹配会返回默认值而不会报错。所以读档时最好检查几个关键字段比如unlockedDishIds是否为空否则就当坏档处理。如果项目规模变大也可以选择Newtonsoft.Json或MessagePack它们支持字典、多态和更多类型但会引入依赖。对 Part3 阶段JsonUtility通常足够。4.3 保存时机与存档安全性很多初学者把存档写在OnApplicationQuit里这没问题但要注意程序崩溃、闪退、中途强杀时OnApplicationQuit可能来不及执行。更稳妥的做法是在关键节点写入完成一个订单、购买一次物品、结束一天营业后。如果担心频繁写盘影响性能可以做一个“脏标记”有改动且画面进入安全状态时再写盘。单机小游戏通常不需要加密存档但至少要有一个版本号。以后你新增了菜谱、增加了道具旧存档数据可能缺少字段或保存格式变化。读档时检查 saveVersion调用对应的迁移函数比让玩家看到一个空档或坏档好得多。这个习惯从 Part3 养起到后面做内容扩展时你会感谢自己。5. 批量数据、预制体和场景组织中期项目混乱的根源5.1 不要为每个菜品都新建一个预制体我曾经看到过有人把 20 道菜做成了 20 个预制体每个预制体里面都放一套按钮和文本后续加一个菜要复制粘贴大半天。更合理的做法是一个通用菜品卡片预制体数据从 DishConfig 读取。Card 上只有一个Init(DishConfig config)方法调用后自动设置图标、名字、价格。这样做的好处不仅是减少预制体数量更关键的是修改表现样式时只需改一个预制体。如果在 Unity6 中需要做不同蔬菜、不同成品模型也可以让一个 3D 展示位支持模型替换或者用预制体变量来复用容器。核心思想一致数据驱动一份展示不要让资源管理的问题变成你手动维护 20 份 UI 的问题。5.2 对象池只在需要时引入顾客、出餐特效、金币飘字这些对象如果频繁生成销毁Unity 的 GC 和实例化开销会拖低帧率。一种通用做法是把不再使用的物体放进池子需要时从池子里取不够时再创建。但是请先测清楚性能再引入不要一开始就给所有东西上池。Part3 阶段如果你发现玩家点一单就产生 10 个不同物体来回切换界面时每隔几秒卡一下那就是该上池的信号。对象池很简单自己写一个最小版本也行例如用一个队列存 GameObject取出时 SetActive(true)放回时 SetActive(false)。重点是约定好“谁负责归还”。5.3 文件夹和命名Part3 最划算的工程支出项目中期最让人头疼的往往不是算法而是找一个脚本要找半天。我建议在 Unity6 里按 Feature 组织文件夹而不是按脚本类型堆在一起。比如Assets/ Features/ Orders/ Scripts/ Data/ Prefabs/ Cooking/ Scripts/ Data/ Prefabs/ UI/ Scripts/ Prefabs/ Core/ Events/ Managers/ SaveSystem/ Config/ Dishes/这个结构不一定适合所有项目但它强调的是和“订单”相关的代码、数据、预制体放在一起改动时不用跨大量目录翻找。比这个更基本的是命名一致。内部方法或变量哪怕只是从Update()里抽出来的一个函数也尽量取一个能说明意图的名字而不是Update2或DoSomething。这些看起来琐碎但在 Part3 之后的作用会越来越大。6. Part3 踩坑排查从“运行不报错但玩不下去”到“真报错”6.1 常见现象不报错但状态对不上美食经营游戏在 Part3 阶段的报错往往不是红字而是“看起来合理但玩不下去”。比如点击接单没有反应、订单卡片不消失、金币没有增加、菜做好了却点不了上菜按钮。这类问题的根源通常不在单个脚本而在事件或状态链路断掉。遇到这种问题不要去改 UI 上的显示值也不要急着在按钮里加更多 if。先问三个问题谁触发了这个动作谁在监听并处理这个动作处理完之后有没有通知 UI 刷新比如“点击接单没反应”你需要确认点击事件确实触发了按钮的 onClickonClick 是否调用了OrderManager.CreateOrder()CreateOrder()里是否成功创建了订单并广播了事件订单列表 UI 是否订阅了事件。把这条链路上的日志都打出来一般很快就能定位到断点。6.2 一个可以复用的排查链路无论问题表现是“没反应”“数值不对”还是“偶尔失效”我通常按这个顺序排查先看 Inspector确认 UI 按钮绑定了正确方法预制体没有缺引用脚本启用了。再看日志在操作入口和出口各打一条日志确认事件或方法真的被调用。再看数据打印操作前后的订单状态和金币值判断是状态没改还是改完没保存。再看 UI 刷新确认 UI 是否订阅了对应的数据变化事件或者刷新时读取的是不是同一个运行时实例。最后看边界有没有用重复 ID、空引用、列表越界、多个管理器各存了一份状态副本。这个排查链路几乎能覆盖 Part3 阶段 90% 的“逻辑看起来没问题但玩起来不对劲”的问题。关键是要克制住“感觉好像是这里错了就改这里”的冲动按链路一层层往下走。6.3 日志规范不只是 Debug.LogUnity 的 Debug.Log 在开发阶段很直观但打印太多也会变成噪音。可以做一个简单的打印规则调试阶段把“入口日志”“状态变更日志”“UI 刷新日志”用不同前缀区分需要保留的日志加上调用点和时间。比如Debug.Log($[Order] Create id{orderId}, state{state}); Debug.Log($[UI] RefreshList count{list.Count});等排查完问题再决定哪些日志留下、哪些删除。不要把所有调试信息都删光核心逻辑的状态变更日志建议保留因为后续继续做 Part4、Part5 时它们会是你理解旧代码的助手。7. 工程化才是 Part3 的主线学习时别跳过7.1 为什么 Part3 阶段不需要“完美设计”我理解很多人在看教程时会有一种冲动希望自己的代码结构一步到位。但 Part3 这个阶段项目的复杂度和你的经验还不足以支撑“微服务式”的过度设计。你需要做的是选一套简单的约定然后坚持。比如所有玩家操作都通过 UI 按钮触发按键回调只做转发。所有长期数据都由 Manager 类持有不散落在单个物体上。所有配置数据放在 ScriptableObject 里运行时数据放在运行时对象里。所有外部资源路径都用常量或引用不手写字符串。这套约定不需要很复杂但它能让你的项目在功能量上涨时不至于快速腐烂。学到这里你可以试着停掉视频自己加一个新菜看看需要改几个文件。如果每次加菜都要动 UI、订单、库存、存档、场景说明抽象还不够。7.2 值得写测试的核心规则有些人会认为单机小游戏写测试是浪费时间但美食经营类游戏里确实有一类逻辑很适合用测试保护收益计算、食材扣除、解锁条件、收益倍率。它们通常是纯函数或依赖少量状态。你可以用 Unity Test Framework 写一个简单测试类专门跑这些规则。这样以后调整数值或重构状态流转后只要运行测试就能快速知道有没有把规则改坏。一个很朴素的例子是有一个函数根据订单价格、成本和制作时长算利润它可以不依赖 MonoBehaviour。你把它定义成纯 C# 方法测试时输入几个数字断言结果。不要指望覆盖全部 UI 和流程只保护最核心的规则就已经能带来安全感了。7.3 怎么判断自己有没有资格进入 Part4做完 Part3 后不急着跟视频继续往下走。给自己设置一个“完成标准”现在项目还能不能轻易支持新增一种顾客类型如果新增一种顾客只要求加配置、加一个状态那说明数据层和状态层已经合格如果每次加需求都要复制粘贴整套订单逻辑说明前面几项工程点还没有消化。你可以在一个分支上试着实现“顾客会点隐藏菜单”这种小功能作为通关测验。能顺利做完就说明 Part3 真正帮你建立了构建游戏的骨架而不只是看过几组视频。美食经营类游戏单机版做到 Part3表面上是继续堆功能实际上是第一次把“demo”推向“完整可玩”的关键转折。这个阶段最值得投资的不是更好的特效而是清晰的数据抽象、状态流转、存档设计和项目组织。很多教程没有明说真正的分水岭不是你会不会写某个脚本而是当功能多起来之后你还能不能让状态始终一致、数据始终可靠、改动始终可控。如果你正卡在 Part3 的某个环节可以先停一停不用急着赶进度。把订单状态、数据驱动、存档和排查链路理顺你后面再做活动系统、装扮系统甚至无限关卡都会轻松很多。这也是 Part3 这份“无聊”真正值钱的原因它逼你从功能堆叠走向工程化。
返回列表