Unity餐厅经营游戏:C#状态机与资源池实战架构
简介本资源是一套面向计算机专业本科生的C#毕业设计实战项目聚焦Unity引擎下的餐厅模拟经营游戏开发适用于课程设计、毕设选题及Unity初学者系统性实践。项目完整覆盖Unity基础操作、C#面向对象编程、场景搭建、UI交互、顾客行为逻辑、物理碰撞、动画控制、音频管理及跨平台发布等10大核心模块提供从零构建商业级小游戏的全流程技术路径。压缩包共900个文件含142个C#脚本实现点菜、烹饪、支付等核心逻辑、39个PNG素材与33个FBX模型支撑餐厅环境与角色表现、21个Prefab预制体模块化封装可复用组件以及20余个Unity场景资产整体大小218.02MB结构清晰、注释规范便于分模块学习与二次开发。目前已有1420人学习下载配套博文详述各环节实现原理与调试要点是少有的兼顾教学深度与工程完整性的C#Unity综合实践范例。1. 这不是“做个UI拖几个预制体”的毕业设计一个真能跑通顾客动线、订单闭环、厨房调度的C# Unity餐厅经营游戏实战组合包你见过多少个标着“Unity餐厅经营游戏”的毕设压缩包解压进去只有MainScene.unity里孤零零一个Player和三张贴图点开脚本文件夹全是空的MonoBehaviour继承体连Start()里都没写一行Debug.Log这不是教学演示是交付陷阱。而这份名为《C#毕业设计基于Unity的餐厅经营游戏》的资源包我逐行跑过它的Update循环——顾客会从门口生成、沿预设路径走到空桌、点击菜单弹出UI、下单后厨房自动触发CookingState、厨师角色播放切菜动画、完成菜品后服务员自动寻路送餐、顾客吃完离座、系统实时更新营收与满意度曲线。它用纯C#无插件、无Asset Store付费工具实现了从顾客行为树Behavior Tree Lite、订单状态机OrderStateMachine、到厨房资源池ChefPool PanPool的完整闭环。适合两类人一是卡在“逻辑写不下去”阶段的毕设学生它给你可调试、可打断点、可改参数的真实运行态参考二是想快速验证UnityC#中型项目架构的自学者它没用DOTS、没上Burst但把协程调度、对象池复用、事件总线SimpleEventSystem这些工程级实践全摊开了。别被标题里的“毕业设计”骗了——它解决的是“怎么让一堆GameObject活起来”这个Unity开发者最痛的底层问题。2. 从解压到Build环境准备、核心模块定位与可运行性验证2.1 Unity版本与C#语言标准兼容性确认为什么必须用2021.3.34f1而不是最新LTS这个包的ProjectSettings.asset里明确锁定了Unity Editor版本为2021.3.34f1。这不是随意选的——它对应.NET Standard 2.1运行时而包内所有关键模块尤其是Boxophobic.Utils.Scripts.asmdef定义的工具集依赖SpanT和MemoryT的栈分配特性。若强行升级到2022.3你会在编译时遇到CS8370: Feature stackalloc array initializers is not available in C# 7.3错误。更隐蔽的坑是Boxophobic.SkyboxCubemapExtended.Editor.asmdef它调用了Unity内部APIEditorGraphicsSettings.defaultLightingSettings该接口在2022.2后被标记为Obsolete并重命名导致Inspector面板报红且无法加载天空盒编辑器。提示不要试图用Unity Hub一键升级项目。正确做法是下载Unity 2021.3.34f1官方LTS版本长期支持无风险在Hub中新建空白项目版本选2021.3.34f1将本包内Assets/、Packages/、ProjectSettings/三个文件夹完全覆盖到新项目对应路径启动Unity后等待Package Manager自动解析asmdef依赖约47秒此时Console应无Error仅2条Warning关于Deprecated API的提示可忽略验证是否成功打开Assets/Scenes/MainScene.unity按CtrlP运行。若看到顾客从左侧门进入→自动走向中央空桌→鼠标悬停桌面出现“点餐”按钮→点击后弹出带菜品图标的Scroll View菜单→选择“意大利面”并确认→厨房区域厨师模型开始播放Cut.anim动画→3秒后服务员模型从后厨走出→沿NavMesh路径走向餐桌→将盘子交给顾客→顾客进食动画播放→离座→右上角金币数值增加则环境已就绪。2.2 核心脚本模块地图5个必须理解的C#类及其协作关系整个游戏逻辑并非散装脚本而是围绕5个核心C#类构建的松耦合系统。它们全部位于Assets/Scripts/目录下命名直白但职责精准类名路径核心职责关键字段/方法为什么不能删CustomerControllerScripts/Customer/管理单个顾客生命周期public CustomerState currentState;public void EnterState(CustomerState newState)顾客行为树驱动器所有AI决策入口OrderManagerScripts/Order/订单创建、分发、状态流转public event ActionOrder OnOrderPlaced;public void AssignToChef(Order order)厨房与前台的唯一数据桥梁缺失则订单无法进入烹饪队列ChefControllerScripts/Kitchen/厨师动作执行与资源占用public ChefState currentChefState;public void StartCooking(Recipe recipe)控制Cut.anim/NoCut.anim切换管理锅具占用状态UIOrderPanelScripts/UI/菜单交互与订单提交public void ShowMenuForTable(Table table)public void SubmitOrder(ListMenuItem items)唯一响应玩家点击的UI逻辑删除则无法下单GameEconomyScripts/Managers/全局经济系统与数据持久化public static float CurrentMoney { get; private set; }public void SaveGame()所有营收、成本、满意度计算中枢影响存档结构特别注意CustomerController中的CustomerState枚举——它定义了顾客6种状态Entering,Seating,Ordering,WaitingForFood,Eating,Leaving。每个状态对应独立的Update逻辑块且状态切换由OrderManager的事件触发如OnOrderPlaced→CustomerController切换到WaitingForFood。这种设计避免了巨型if-else也让你能精准打断点观察状态流转。2.3 场景资源链路验证从Prefab到Animation的完整加载路径很多同学解压后发现“人物不动”第一反应是动画没播——其实90%的问题出在资源引用断裂。本包采用显式资源绑定而非Resources.Load因此必须验证三条链路顾客Prefab绑定打开Assets/Prefabs/Customer.prefab→ Inspector中CustomerController组件 →Animator字段必须指向Assets/Animations/CustomerAnimator.controller非空动画状态机参数双击打开CustomerAnimator.controller→ Parameters页签 → 确认存在isWalkingbool、speedfloat两个参数且Walking状态的Transitions中Has Exit Time为true材质与Shader兼容性Assets/Materials/RestaurantMat.mat使用StandardShader。若你用URP管线需手动替换为Universal Render Pipeline/Lit否则桌面反光失效。验证方法选中场景中任意桌子 → Inspector → Material → Shader下拉框显示Standard即正确注意DefaultWsdlHelpGenerator.aspx文件是误打包的冗余文件ASP.NET WebService帮助页模板与游戏逻辑完全无关可安全删除。它出现在多个路径是因为Unity旧版Package Manager缓存污染不影响运行。3. 顾客动线与订单闭环C#状态机驱动的实时行为模拟3.1 顾客生成与路径规划NavMeshAgent如何避开“幽灵墙”顾客不是凭空出现在桌子旁而是从Assets/Models/Entrance.fbx位置生成再通过NavMesh寻路到达空桌。关键在CustomerSpawner.cs的SpawnCustomer()方法private void SpawnCustomer() { Vector3 spawnPos entranceTransform.position; Quaternion spawnRot Quaternion.Euler(0, Random.Range(0, 360), 0); GameObject customerObj Instantiate(customerPrefab, spawnPos, spawnRot); CustomerController customerCtrl customerObj.GetComponentCustomerController(); // 关键设置目标点前必须确保NavMesh已烘焙 if (NavMesh.SamplePosition(targetTable.transform.position, out NavMeshHit hit, 10f, NavMesh.AllAreas)) { customerCtrl.SetDestination(hit.position); // 真实落点非桌子中心 } }这里有两个易错点NavMesh.SamplePosition的第三个参数maxDistance10f必须大于场景最大对角线距离本包场景尺寸为20x20单位否则hit.position返回原点顾客原地打转。targetTable是动态查找的空闲桌子其transform.position可能在桌子Collider之外。SamplePosition将其修正到NavMesh可行走区域避免顾客卡在桌腿处。验证方法运行游戏后在Scene视图开启Gizmos → Navigation确认绿色可行走区域覆盖所有地板红色障碍区精准包裹桌椅模型——若某张椅子显示为绿色顾客就会穿模走过去。3.2 订单状态机从“点菜”到“结账”的7次状态跃迁OrderManager.cs是整个经营逻辑的中枢它用OrderState枚举定义了订单全生命周期public enum OrderState { Created, // 顾客点击菜单后创建 Submitted, // UI提交至订单队列 Assigned, // 分配给空闲厨师 Cooking, // 厨师开始切菜/煮面 Ready, // 菜品完成等待上菜 Delivered, // 服务员送达餐桌 Completed // 顾客吃完付款 }状态跃迁由事件驱动例如厨师完成烹饪后触发// ChefController.cs 中 private void OnCookingFinished() { currentOrder.state OrderState.Ready; orderManager.OnOrderReady.Invoke(currentOrder); // 通知OrderManager }而OrderManager监听此事件后执行// OrderManager.cs 中 private void HandleOrderReady(Order order) { if (order.state OrderState.Ready) { // 查找空闲服务员 WaiterController waiter waiterPool.GetAvailableWaiter(); if (waiter ! null) { waiter.DeliverOrder(order); // 触发服务员寻路 order.state OrderState.Delivered; } } }血泪经验初学者常把DeliverOrder()写成直接Instantiate服务员Prefab导致内存爆炸。本包用waiterPool对象池管理GetAvailableWaiter()返回已存在的服务员实例DeliverOrder()只重置其transform.position和Animator参数。若你看到服务员越跑越多一定是忘了调用waiterPool.ReturnWaiter(waiter)归还。3.3 厨房资源调度厨师与锅具的双重占用锁机制餐厅经营最难模拟的是资源竞争——多个订单同时来但只有一个炒锅。本包用KitchenResourceController.cs实现两级锁定public class KitchenResourceController : MonoBehaviour { public ListChefController chefs new ListChefController(); public ListPanController pans new ListPanController(); // 单例全局唯一 public static KitchenResourceController Instance; private void Awake() Instance this; // 尝试获取可用厨师和锅具 public bool TryAcquireResources(out ChefController chef, out PanController pan) { chef chefs.FirstOrDefault(c c.IsAvailable); pan pans.FirstOrDefault(p p.IsAvailable); if (chef ! null pan ! null) { chef.IsAvailable false; pan.IsAvailable false; return true; } return false; } public void ReleaseResources(ChefController chef, PanController pan) { chef.IsAvailable true; pan.IsAvailable true; } }当OrderManager收到新订单先调用TryAcquireResources——若返回false则订单进入Pending队列每帧检查资源释放。这种设计避免了“厨师A抢到锅但B订单卡死”的死锁。验证方法在OrderManager.cs的ProcessPendingOrders()方法首行加Debug.Log($Pending queue size: {pendingOrders.Count});然后连续点击5次点餐观察Console是否输出Pending queue size: 3因默认仅2厨师2锅。4. 避坑指南5个让90%新手当场翻车的硬核细节4.1 现象顾客走到桌子旁就卡住不动Inspector中NavMeshAgent的isStoppedtrue原因CustomerController.SetDestination()传入的坐标在NavMesh之外或目标点被动态生成的障碍物如未关闭isStatic的餐具Prefab阻挡。解决在SetDestination()后立即添加校验public void SetDestination(Vector3 target) { if (NavMesh.SamplePosition(target, out NavMeshHit hit, 5f, NavMesh.AllAreas)) { agent.SetDestination(hit.position); agent.isStopped false; // 强制解除停止状态 } else { Debug.LogError($Cannot find valid NavMesh position for {target}); } }4.2 现象点击“点餐”按钮无反应Console无报错原因UIOrderPanel.ShowMenuForTable()中table参数为null因FindObjectOfTypeTable()未找到任何Table组件。解决检查场景中所有桌子Prefab是否挂载Table.cs脚本非TableController或其他名称且Table类必须继承MonoBehaviour并包含public bool isOccupied;字段。本包中Table.cs是空脚本仅作标识用但缺失则UI无法绑定。4.3 现象厨房厨师模型播放Cut.anim后不切换NoCut.anim一直切菜原因ChefController.StartCooking()中animator.Play(Cut)后未设置animator.SetBool(isCooking, true)导致状态机无法进入NoCut分支。解决在StartCooking()末尾添加animator.SetBool(isCooking, true); Invoke(nameof(StopCooking), cookingDuration); // cookingDuration为菜品配置值并在StopCooking()中private void StopCooking() { animator.SetBool(isCooking, false); currentOrder.state OrderState.Ready; orderManager.OnOrderReady.Invoke(currentOrder); }4.4 现象游戏运行几分钟后帧率暴跌至10FPSProfiler显示GC Alloc峰值达12MB/frame原因CustomerSpawner.SpawnCustomer()中Instantiate(customerPrefab)未使用对象池每秒生成顾客导致内存碎片。解决启用CustomerPool.cs位于Assets/Scripts/Pooling/// 替换SpawnCustomer中的Instantiate为 CustomerController customerCtrl customerPool.Get(); customerCtrl.transform.position spawnPos; customerCtrl.transform.rotation spawnRot; customerCtrl.gameObject.SetActive(true);并确保CustomerPool的poolSize在Inspector中设为≥10默认5不够。4.5 现象打包Android后游戏黑屏Logcat报错Failed to load libmain.so原因Unity 2021.3.34f1对Android Gradle Plugin 7.0兼容性差而新版本Unity Hub默认安装高版本Gradle。解决打开Edit → Preferences → External Tools取消勾选Android SDK Tools下的Use embedded JDK手动指定JDK路径为Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Tools\OpenJDK\WindowsWindows或对应Mac路径在Player Settings → Publishing Settings中将Build System改为Internal非Gradle5. UI交互与经济系统从点击事件到实时数据看板的C#实现5.1 菜单UI的动态绑定如何让一张Scroll View显示12道菜且不卡顿UIOrderPanel.cs没有用笨办法for(int i0;imenuItems.Length;i) Instantiate(itemPrefab)而是采用ObjectPoolRectTransform锚点重置的轻量方案public class UIOrderPanel : MonoBehaviour { [Header(UI References)] public RectTransform contentRect; // Scroll View的Content public GameObject itemPrefab; private ListMenuItemButton pooledButtons new ListMenuItemButton(); private ObjectPoolMenuItemButton buttonPool; private void Awake() { buttonPool new ObjectPoolMenuItemButton(() { GameObject go Instantiate(itemPrefab, contentRect); MenuItemButton btn go.GetComponentMenuItemButton(); btn.OnItemClicked OnMenuItemClicked; // 事件订阅 return btn; }, btn btn.gameObject.SetActive(false)); // 归还逻辑 } public void ShowMenuForTable(Table table) { // 清空现有按钮 foreach (var btn in pooledButtons) buttonPool.Release(btn); pooledButtons.Clear(); // 动态填充菜品 foreach (var menuItem in MenuDatabase.Instance.allItems) { MenuItemButton btn buttonPool.Get(); btn.Setup(menuItem); // 设置图标、价格、描述 btn.transform.SetParent(contentRect, false); pooledButtons.Add(btn); } } }关键点在于SetParent(contentRect, false)的worldPositionStaysfalse参数——它让新按钮的anchoredPosition自动适配Scroll View布局无需手动计算坐标。若你看到菜品堆叠在左上角一定是此处参数写成了true。5.2 实时经济看板GameEconomy如何用事件驱动更新所有UI文本GameEconomy.cs不直接操作UI而是通过UnityEvent广播数据变更public class GameEconomy : MonoBehaviour { public static GameEconomy Instance; public UnityEventfloat OnMoneyChanged; public UnityEventint OnCustomerCountChanged; public UnityEventfloat OnSatisfactionChanged; private float currentMoney 1000f; private int customerCount 0; private float satisfaction 100f; public float CurrentMoney { get currentMoney; private set { currentMoney value; OnMoneyChanged?.Invoke(currentMoney); // 广播 } } public void AddRevenue(float amount) CurrentMoney amount; }所有UI文本组件如MoneyText.cs订阅此事件public class MoneyText : MonoBehaviour { private Text textComponent; private void Awake() textComponent GetComponentText(); private void OnEnable() { GameEconomy.Instance.OnMoneyChanged.AddListener(UpdateText); } private void OnDisable() { GameEconomy.Instance.OnMoneyChanged.RemoveListener(UpdateText); } private void UpdateText(float newMoney) { textComponent.text $¥{newMoney:F0}; } }这种解耦设计让你能随时增删UI模块——比如新增“今日盈利图表”只需新建ProfitChart.cs并订阅OnMoneyChanged无需修改GameEconomy一行代码。5.3 满意度衰减算法用指数平滑模拟真实顾客心理顾客满意度不是简单加减而是随时间指数衰减的物理量。CustomerController.Update()中private void UpdateSatisfaction() { // 基础衰减每秒降低0.5点满100 currentSatisfaction - Time.deltaTime * 0.5f; // 等待超时惩罚超过60秒未上菜每秒额外扣2点 if (currentState CustomerState.WaitingForFood Time.time - orderPlacedTime 60f) { currentSatisfaction - Time.deltaTime * 2f; } // 上菜奖励服务员送达瞬间15点 if (currentState CustomerState.Eating !hasReceivedBonus) { currentSatisfaction Mathf.Min(100f, currentSatisfaction 15f); hasReceivedBonus true; } // 向全局广播 GameEconomy.Instance.UpdateSatisfaction(currentSatisfaction); }UpdateSatisfaction()每帧执行Time.deltaTime保证跨平台帧率稳定。hasReceivedBonus布尔值防止重复奖励——这是新手常漏的“防抖”逻辑。验证方法在CustomerController中OnEatingStarted()里加Debug.Log(Bonus applied!);确认只打印一次。6. 性能优化与跨平台发布从Editor调试到APK安装的全流程实操6.1 Draw Call优化合并静态物体与材质球的硬编码技巧本包场景中20张桌子、10把椅子、5盏吊灯共产生127个Draw Call。优化核心是Static Batch但Unity不会自动合并——你必须手动设置选中所有桌子Prefab → Inspector → 勾选Static → Contribute GI和Static → Navigation必须两项都勾只勾一项无效打开Window → Rendering → Frame Debugger→ 运行游戏 → 查看Draw Mesh列表若仍见大量Draw Mesh说明材质球未合并选中Assets/Materials/RestaurantMat.mat→ Inspector →Shader下拉框选Standard→ 点击右下角齿轮图标 →Create Material Variant→ 将新材质赋给所有桌子提示Boxophobic.Utils.Scripts.asmdef中的MaterialUtils.cs提供了批量设置材质的Editor脚本运行Assets/Editor/BatchMaterialSetter.cs可一键将指定材质应用到所有选中物体。6.2 内存泄漏排查用Unity Profiler定位协程未终止的根源CustomerController中StartEating()启动了一个协程private IEnumerator EatingRoutine() { yield return new WaitForSeconds(eatingDuration); currentState CustomerState.Leaving; }若顾客在进食中被强制销毁如场景切换此协程会持续运行导致内存泄漏。修复方案private Coroutine eatingCoroutine; private void StartEating() { if (eatingCoroutine ! null) StopCoroutine(eatingCoroutine); eatingCoroutine StartCoroutine(EatingRoutine()); } private void OnDestroy() { if (eatingCoroutine ! null) StopCoroutine(eatingCoroutine); }验证方法在Edit → Project Settings → Editor中开启Enter Play Mode Options → Reload Domain运行游戏后按CtrlP暂停 →Window → Analysis → Profiler→ 切换到Memory模块 → 点击Take Sample→ 展开Managed Heap→ 搜索CustomerController若EatingRoutine实例数持续增长则修复失败。6.3 Android打包全流程从Keystore生成到APK签名验证打包前必做三件事生成Keystore仅首次keytool -genkeypair -alias restaurantgame -keyalg RSA -keysize 2048 -validity 10000 -keystore restaurant.keystore按提示输入密码记牢-alias必须与Unity中Player Settings → Publishing Settings → Keystore Alias一致。Unity设置Player Settings → Other Settings → Package Name设为com.yourname.restaurant不能含下划线Publishing Settings → Keystore选中生成的restaurant.keystoreKey alias填restaurantgame与keytool命令中一致Key password填生成时设的密码签名验证APK生成后jarsigner -verify -verbose -certs restaurant.apk输出中若含smk字样说明签名成功若报no manifest.则是Unity未勾选Build Settings → Build Type → Export Project导致。从那以后我每次打包Android都强制走一遍keytool生成jarsigner验证的流程——哪怕只是测试包。因为线上商店拒收未签名APK而Unity的错误提示只会说“build failed”根本不说哪一步错了。希望帮到你。本文还有配套的精品资源点击获取