简介:这是一份面向高校计算机相关专业毕业生的Unity餐厅经营游戏完整项目源码,以C#为主要开发语言,适合正在准备毕业设计或希望系统入门Unity游戏开发的学习者。项目围绕餐厅模拟经营这一典型场景,覆盖从场景搭建、UI交互到顾客点菜、烹饪上菜、支付结算等完整业务闭环,并涉及物理碰撞、动画状态机、音频混音与性能优化等进阶内容。压缩包共900个文件,约218MB,其中142个cs脚本承载核心游戏逻辑,119个dll为依赖库,另有39张png、33个fbx模型、21个prefab预制体、15个mat材质、7个unity场景与7个mp3音频等资源,目录结构清晰,便于按模块查阅与二次开发。目前已有1420人学习下载,可作为毕业设计选题参考、Unity+C#实战练手或课程项目模板,帮助读者快速理解游戏开发整体流程与关键环节。
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.Load<DemoConfig>("Demo"); if (demoConfig == null) { Debug.LogWarning("Demo.asset 未找到,使用默认值"); } else { GameSettings.CustomerInterval = demoConfig.customerInterval; GameSettings.DishPrice = demoConfig.dishPrice; } SceneManager.LoadSceneAsync(mainSceneName, LoadSceneMode.Single); } }这段代码里Resources.Load<DemoConfig>("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 = GetComponent<Animator>(); 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 烹饪队列与上菜逻辑
烹饪不是瞬间完成的,需要一个队列。常见做法是用Queue<DishData>加一个Coroutine来模拟。厨房同时只能做一道菜,做完自动做下一道。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Kitchen : MonoBehaviour { private Queue<DishData> cookingQueue = new Queue<DishData>(); 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 = GetComponent<Animator>(); } 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勾选Loop,sfxSource不勾。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 - Overlay,EventSystem确保场景里有一个,不要重复。如果按钮点不动,检查Graphic Raycaster是否在 Canvas 上。
5.4 现象:Demo.asset修改后运行没变化
原因:Resources.Load加载的是打包时的资源,编辑器里改了Demo.asset但没保存,或者加载路径不对。解决:确认Demo.asset在Assets/Resources下,改完按Ctrl+S保存,再 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 List<DishData> 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 ObjectPool<Customer> pool; private void Awake() { pool = new ObjectPool<Customer>( 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设 10,maxSize设 20,超过就销毁。actionOnGet和actionOnRelease分别做激活和隐藏。注意Customer脚本里如果有Coroutine,在Release时要StopAllCoroutines,否则下次Get出来会带着旧协程跑。
从那以后我每次拿到一个 Unity 工程,都强制先看ProjectSettings.asset的版本、再查.asmdef引用、最后才点 Play。这个顺序帮我省了至少三次重装引擎的时间。希望帮到你。
本文还有配套的精品资源,点击获取