多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity餐厅经营游戏C#毕业设计:完整工程解析与避坑指南

Unity餐厅经营游戏C#毕业设计:完整工程解析与避坑指南 简介这是一份面向高校计算机相关专业毕业生的Unity餐厅经营游戏完整项目源码以C#为主要开发语言适合正在准备毕业设计或希望系统入门Unity游戏开发的学习者。项目围绕餐厅模拟经营这一典型场景覆盖从场景搭建、UI交互到顾客点菜、烹饪上菜、支付结算等完整业务闭环并涉及物理碰撞、动画状态机、音频混音与性能优化等进阶内容。压缩包共900个文件约218MB其中142个cs脚本承载核心游戏逻辑119个dll为依赖库另有39张png、33个fbx模型、21个prefab预制体、15个mat材质、7个unity场景与7个mp3音频等资源目录结构清晰便于按模块查阅与二次开发。目前已有1420人学习下载可作为毕业设计选题参考、UnityC#实战练手或课程项目模板帮助读者快速理解游戏开发整体流程与关键环节。1. 拆开这个 Unity 餐厅经营游戏压缩包它到底能跑出什么如果你正在找一份能直接跑起来的 C# 毕业设计又不想从零搭场景、写 UI、连动画状态机那这个基于 Unity 的餐厅经营游戏压缩包值得先看一眼。它不是一个只有几行脚本的空壳 Demo而是把餐厅模拟经营里最核心的几条链路——顾客生成、点菜、烹饪、上菜、结账、UI 反馈、动画联动、音频播放——都塞进了一个完整工程里。你拿到手之后最直接的用法是把它当作「可运行的参考实现」先跑通再拆脚本最后按自己的选题改数值、换模型、加关卡。适合的人群很明确C# 基础语法过关、Unity 界面能认全、但没独立做过完整游戏循环的毕业生或者想拿一个现成工程练手「unity 扩展」和「unity 特性」的初级开发者。压缩包里能看到Cut.anim、NoCut.anim这类动画切片文件也有Boxophobic.SkyboxCubemapExtended.Editor.asmdef这种第三方天空盒扩展的程序集定义说明作者在环境氛围上做过处理不是纯白盒场景。ProjectSettings.asset和Demo.asset则意味着工程配置和演示数据是分开的你改坏了一个还能回滚另一个。接下来不聊虚的直接按「怎么把它跑起来、脚本怎么读、坑在哪」的顺序拆。2. 把工程跑起来Unity 版本、程序集与场景加载顺序2.1 先确认 Unity 版本和渲染管线别急着点 Play拿到压缩包第一件事不是双击场景而是看ProjectSettings.asset里的版本号。这个工程里出现了Boxophobic.SkyboxCubemapExtended.Editor.asmdef说明它依赖一个自定义的天空盒扩展包而这类包对 Unity 版本和渲染管线比较敏感。常见做法是如果工程没有附带Packages/manifest.json里的完整依赖列表就先用 Unity Hub 装一个 LTS 版本优先选 2021.3 或 2022.3这两个版本对旧版.asmdef和.anim的兼容性最稳。打开工程后先看 Console 有没有报Assembly not found或Shader error有的话先解决程序集引用再谈运行。程序集定义文件.asmdef的作用是把代码编译成独立的 DLL而不是全塞进Assembly-CSharp。这个工程里Boxophobic.Utils.Editor.asmdef和Boxophobic.Utils.Scripts.asmdef分开了编辑器和运行时好处是打包时不会把编辑器代码带进去。但如果你在脚本里直接using Boxophobic.Utils;却忘了在 asmdef 的 References 里加引用就会编译失败。检查路径选中报错的脚本看 Inspector 里它属于哪个 Assembly再去对应 asmdef 的 References 列表里补上。// 常见做法在运行时脚本里安全地引用工具类避免编辑器命名空间污染 using UnityEngine; using Boxophobic.Utils; // 确保当前 asmdef 已引用 Boxophobic.Utils.Scripts public class RestaurantBootstrap : MonoBehaviour { [SerializeField] private GameObject customerPrefab; [SerializeField] private Transform spawnPoint; private void Start() { // 延迟一帧生成等所有 Awake 和场景加载完成 Invoke(nameof(SpawnFirstCustomer), 0.1f); } private void SpawnFirstCustomer() { if (customerPrefab null || spawnPoint null) { Debug.LogError(顾客预制体或生成点未赋值检查 Inspector); return; } Instantiate(customerPrefab, spawnPoint.position, Quaternion.identity); } }上面这段代码的逻辑是把顾客生成从Start里拆出来延迟 0.1 秒执行避免和场景里其他Awake抢初始化顺序。参数customerPrefab和spawnPoint必须在 Inspector 里拖拽赋值不要用GameObject.Find在运行时找否则打包后容易因为对象名被裁剪而返回 null。Invoke的 0.1 秒是个经验值如果你的场景里有异步加载的 NavMesh 或光照贴图可以加到 0.3 秒。2.2 场景加载顺序与 Demo.asset 的作用Demo.asset通常是一个ScriptableObject用来存演示用的配置数据比如顾客生成间隔、菜品价格、初始金币。它和ProjectSettings.asset分开的好处是你改坏了 Demo 数据直接删掉重新生成一个就行不会影响工程设置。加载顺序上常见做法是建一个Boot场景里面只放一个GameManager和一个RestaurantBootstrap由它来SceneManager.LoadSceneAsync加载主餐厅场景。这样做的原因是餐厅场景里可能有大量模型和光照直接作为启动场景会让第一次加载很慢而且不好做加载进度条。using UnityEngine; using UnityEngine.SceneManagement; public class BootLoader : MonoBehaviour { [SerializeField] private string mainSceneName Restaurant_Main; private void Start() { // 先加载 Demo 配置再异步切场景 var demoConfig Resources.LoadDemoConfig(Demo); if (demoConfig null) { Debug.LogWarning(Demo.asset 未找到使用默认值); } else { GameSettings.CustomerInterval demoConfig.customerInterval; GameSettings.DishPrice demoConfig.dishPrice; } SceneManager.LoadSceneAsync(mainSceneName, LoadSceneMode.Single); } }这段代码里Resources.LoadDemoConfig(Demo)要求Demo.asset必须放在Assets/Resources目录下否则加载不到。参数mainSceneName要和 Build Settings 里勾选的场景名完全一致大小写敏感。如果你把Demo.asset放在其他目录就得改用AssetDatabase仅编辑器或Addressables但毕业设计里放 Resources 最省事。2.3 动画切片 Cut.anim 与 NoCut.anim 的挂载方式Cut.anim和NoCut.anim大概率是某个角色或物体的两个动画片段一个带切割动作一个不带。在 Unity 里.anim文件不能直接拖到场景物体上必须先建一个Animator Controller把这两个片段拖进去再通过状态机切换。常见坑是直接拖.anim到物体上Unity 会自动创建一个 Animator Controller但状态之间没有过渡条件播放完就停。正确做法是手动建 Controller加一个bool参数比如isCutting在Cut和NoCut之间用Has Exit Time取消勾选过渡时间设 0.1 秒。using UnityEngine; public class CustomerAnimator : MonoBehaviour { private Animator animator; private static readonly int IsCutting Animator.StringToHash(isCutting); private void Awake() { animator GetComponentAnimator(); if (animator null) { Debug.LogError(Animator 组件缺失检查预制体); } } public void SetCutting(bool cutting) { if (animator ! null) { animator.SetBool(IsCutting, cutting); } } }Animator.StringToHash比直接写字符串isCutting更高效也避免拼写错误。参数IsCutting是静态只读全局只算一次。如果你的动画状态机里参数名改了这里也要同步改否则SetBool不会报错但动画不切换属于典型的「玄学」问题。3. 餐厅经营核心逻辑顾客、点菜、烹饪、上菜、结账的 C# 实现3.1 顾客生成与状态机设计餐厅经营游戏的骨架是顾客状态机。一个顾客从进店到离店至少经历Entering、WaitingToOrder、Ordering、WaitingForFood、Eating、Paying、Leaving七个状态。常见做法是用enum加switch而不是上复杂的 Behavior Tree因为毕业设计的规模用状态机足够而且好调试。public enum CustomerState { Entering, WaitingToOrder, Ordering, WaitingForFood, Eating, Paying, Leaving } public class Customer : MonoBehaviour { public CustomerState State { get; private set; } CustomerState.Entering; public float Patience { get; private set; } 30f; // 耐心值秒 private void Update() { Patience - Time.deltaTime; if (Patience 0f State ! CustomerState.Leaving) { // 耐心耗尽直接离开并扣分 State CustomerState.Leaving; RestaurantManager.Instance.OnCustomerAngry(this); } } public void ChangeState(CustomerState newState) { State newState; // 进入新状态时重置耐心值具体数值可按状态区分 Patience newState CustomerState.WaitingForFood ? 45f : 30f; } }Patience是核心参数不同状态给不同值等菜时给 45 秒等点菜给 30 秒。Update里每帧扣时间归零就触发离开逻辑。注意RestaurantManager.Instance要用单例并且在Awake里赋值否则顾客生成时可能拿不到实例。常见翻车点是顾客预制体上挂了Customer脚本但RestaurantManager还没初始化导致空引用。3.2 点菜与菜单数据用 ScriptableObject 管菜品菜品数据不要硬编码在脚本里用ScriptableObject建菜单。每个菜品一个 asset包含名称、价格、烹饪时间、预制体引用。这样你加新菜不用改代码策划也能自己填。using UnityEngine; [CreateAssetMenu(fileName NewDish, menuName Restaurant/Dish)] public class DishData : ScriptableObject { public string dishName; public float price; public float cookTime; public GameObject dishPrefab; public Sprite icon; }CreateAssetMenu让你在 Project 右键菜单里直接建菜品。cookTime是烹饪耗时和顾客耐心值做对比如果cookTime Patience顾客大概率会生气。参数dishPrefab是上菜时实例化的模型icon用于 UI 显示。注意ScriptableObject在运行时修改不会保存到磁盘所以价格这种数据最好只读要改就在编辑器里改。3.3 烹饪队列与上菜逻辑烹饪不是瞬间完成的需要一个队列。常见做法是用QueueDishData加一个Coroutine来模拟。厨房同时只能做一道菜做完自动做下一道。using System.Collections; using System.Collections.Generic; using UnityEngine; public class Kitchen : MonoBehaviour { private QueueDishData cookingQueue new QueueDishData(); private bool isCooking false; public void AddOrder(DishData dish) { cookingQueue.Enqueue(dish); if (!isCooking) { StartCoroutine(CookNext()); } } private IEnumerator CookNext() { isCooking true; while (cookingQueue.Count 0) { DishData current cookingQueue.Dequeue(); yield return new WaitForSeconds(current.cookTime); // 烹饪完成通知上菜 RestaurantManager.Instance.OnDishReady(current); } isCooking false; } }Queue保证先点先做isCooking防止重复开协程。WaitForSeconds的参数就是DishData.cookTime你可以在 Inspector 里给每道菜设不同时间。坑在于如果顾客在菜做好之前离开OnDishReady里要判断对应顾客是否还在否则会往空座位送菜。常见做法是给每个订单加一个Customer引用上菜前检查customer.State。3.4 结账与金币系统结账逻辑简单但要注意浮点数精度。金币用int价格用float结算时Mathf.RoundToInt取整。public class RestaurantManager : MonoBehaviour { public static RestaurantManager Instance { get; private set; } private int coins 100; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; } public void OnCustomerPaid(DishData dish) { int earned Mathf.RoundToInt(dish.price); coins earned; UIManager.Instance.UpdateCoins(coins); } public void OnCustomerAngry(Customer customer) { coins - 10; // 扣分惩罚 UIManager.Instance.UpdateCoins(coins); } }单例模式里Awake做了重复实例销毁防止场景切换时出现两个RestaurantManager。OnCustomerPaid和OnCustomerAngry分别处理收入和惩罚UIManager负责刷新 UI。注意coins不要用public字段直接暴露用属性或方法访问否则外部脚本乱改会导致数值异常。4. UI、动画与音频的联动让餐厅「活」起来4.1 Canvas 与 UI 事件绑定Unity 的 UI 系统用Canvas加EventSystem按钮点击靠Button.onClick.AddListener。常见做法是把 UI 脚本挂在一个UIManager上统一管理金币文本、订单列表、提示弹窗。using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public static UIManager Instance { get; private set; } [SerializeField] private Text coinsText; [SerializeField] private GameObject orderPanel; private void Awake() { Instance this; } public void UpdateCoins(int amount) { if (coinsText ! null) { coinsText.text $金币: {amount}; } } public void ShowOrder(DishData dish) { orderPanel.SetActive(true); // 这里可以进一步设置订单图标和文字 } }coinsText和orderPanel在 Inspector 里拖拽赋值。UpdateCoins用字符串插值注意Text组件在 Unity 2021 之后逐渐被TextMeshPro替代但毕业设计里用Text完全够。坑在于如果UIManager在场景切换时被销毁Instance会变成 null所以要么用DontDestroyOnLoad要么每个场景放一个。4.2 Animator 与代码的联动动画状态机里的参数用Animator.SetBool、SetTrigger控制。顾客坐下、点菜、吃饭、离开都可以用Trigger切换。注意Trigger是消耗型的触发一次后自动复位适合做一次性动作。public class CustomerAnimation : MonoBehaviour { private Animator animator; private void Awake() { animator GetComponentAnimator(); } public void PlaySit() { animator.SetTrigger(Sit); } public void PlayEat() { animator.SetBool(IsEating, true); } public void StopEat() { animator.SetBool(IsEating, false); } }SetTrigger(Sit)对应状态机里的Sit触发器SetBool(IsEating)对应吃饭循环。参数名必须和 Animator 窗口里完全一致大小写敏感。常见问题是动画过渡没取消Has Exit Time导致Sit播完要等默认时长才切下一个状态看起来像卡住。4.3 音频管理AudioSource 与 AudioMixer音效分两类背景音乐用AudioSource循环播放音效用PlayOneShot。AudioMixer用来统一调音量但毕业设计里可以不用直接调AudioSource.volume。public class AudioManager : MonoBehaviour { public static AudioManager Instance { get; private set; } [SerializeField] private AudioSource bgmSource; [SerializeField] private AudioSource sfxSource; [SerializeField] private AudioClip coinSound; private void Awake() { Instance this; } public void PlayCoin() { if (sfxSource ! null coinSound ! null) { sfxSource.PlayOneShot(coinSound); } } }bgmSource勾选LoopsfxSource不勾。PlayOneShot可以叠加播放适合金币这种短音效。注意AudioClip的Load Type在 Inspector 里设成Decompress On Load还是Streaming背景音乐用Streaming省内存音效用Decompress On Load避免延迟。5. 避坑与排查这个工程里最容易翻车的五个点5.1 现象Play 之后顾客不生成Console 无报错原因RestaurantBootstrap脚本挂在场景里但customerPrefab或spawnPoint在 Inspector 里是None。Unity 不会对未赋值的SerializeField报错只会静默跳过。解决选中挂脚本的物体在 Inspector 里把预制体和生成点拖进去或者加Debug.LogError在Start里检查。5.2 现象动画播放但角色不移动或者移动了但动画不播原因Animator组件和NavMeshAgent或CharacterController冲突。如果动画带Root Motion但代码里又用transform.Translate移动就会互相打架。解决要么关掉Apply Root Motion要么把移动完全交给动画。常见做法是餐厅游戏里顾客移动用NavMeshAgent动画只做上半身Root Motion关掉。5.3 现象打包后 UI 文字消失或按钮点不动原因Canvas的Render Mode设成了Screen Space - Camera但打包后相机被裁剪或EventSystem丢失。解决毕业设计统一用Screen Space - OverlayEventSystem确保场景里有一个不要重复。如果按钮点不动检查Graphic Raycaster是否在 Canvas 上。5.4 现象Demo.asset修改后运行没变化原因Resources.Load加载的是打包时的资源编辑器里改了Demo.asset但没保存或者加载路径不对。解决确认Demo.asset在Assets/Resources下改完按CtrlS保存再 Play。如果还不行在BootLoader里加Debug.Log(demoConfig.customerInterval)看实际读到的值。5.5 现象程序集编译报错The type or namespace name Boxophobic could not be found原因.asmdef文件没有正确引用Boxophobic.Utils.Scripts或者Boxophobic文件夹被移动了。解决找到报错的 asmdef在 Inspector 的Assembly Definition References里加上Boxophobic.Utils.Scripts和Boxophobic.Utils.Editor如果编辑器脚本需要。如果文件夹被移动asmdef 的引用会断重新拖拽即可。6. 进阶技巧用 ScriptableObject 做关卡配置与性能自查这个工程跑通之后最值得做的一件事是把所有可调参数从脚本里抽出来集中到一个LevelConfig的ScriptableObject里。这样做的好处是你换关卡不用改代码直接复制一个 asset 改数值就行。具体做法是建一个LevelConfig类包含顾客生成间隔、最大顾客数、初始金币、菜品列表、背景音乐引用。using UnityEngine; using System.Collections.Generic; [CreateAssetMenu(fileName LevelConfig, menuName Restaurant/LevelConfig)] public class LevelConfig : ScriptableObject { public float customerSpawnInterval 5f; public int maxCustomers 8; public int startingCoins 100; public ListDishData availableDishes; public AudioClip backgroundMusic; }然后在RestaurantManager里加一个LevelConfig字段Awake时读取。参数customerSpawnInterval控制生成频率maxCustomers防止顾客无限堆积导致卡顿。availableDishes是菜品列表你可以做多个关卡 asset每个关卡开放不同菜品。性能自查方面餐厅游戏最容易出问题的是 Draw Call 和 GC。常见做法是顾客预制体用ObjectPool复用不要频繁Instantiate和Destroy。UI 文本更新不要每帧调只在金币变化时调。动画状态机不要每帧SetBool用事件驱动。如果你在 Profiler 里看到GC.Alloc每帧都有检查是不是在Update里new了字符串或列表。using UnityEngine; using UnityEngine.Pool; public class CustomerPool : MonoBehaviour { [SerializeField] private Customer customerPrefab; private ObjectPoolCustomer pool; private void Awake() { pool new ObjectPoolCustomer( createFunc: () Instantiate(customerPrefab), actionOnGet: c c.gameObject.SetActive(true), actionOnRelease: c c.gameObject.SetActive(false), actionOnDestroy: c Destroy(c.gameObject), defaultCapacity: 10, maxSize: 20 ); } public Customer Get() { return pool.Get(); } public void Release(Customer customer) { pool.Release(customer); } }ObjectPool是 Unity 2021 之后内置的defaultCapacity设 10maxSize设 20超过就销毁。actionOnGet和actionOnRelease分别做激活和隐藏。注意Customer脚本里如果有Coroutine在Release时要StopAllCoroutines否则下次Get出来会带着旧协程跑。从那以后我每次拿到一个 Unity 工程都强制先看ProjectSettings.asset的版本、再查.asmdef引用、最后才点 Play。这个顺序帮我省了至少三次重装引擎的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表