
简介针对计算机相关专业毕业设计与项目实战需求这套基于Unity的餐厅经营游戏项目包含完整C#源码、数据库、论文及演示视频是一份经导师指导并获得98分高分评价的可运行作品。资源包共916个文件压缩后约227MB涵盖C#脚本、Unity场景、预制体、模型材质、音频、PDF论文与Word文档其中cs脚本和unity文件构成游戏核心逻辑prefab与fbx用于场景搭建数据库脚本则完整呈现菜品与订单数据表结构。目前已有78人学习下载。项目从餐厅经营玩法、顾客接待流程、菜品数据管理到界面交互完整呈现源码经过本地编译调试可稳定运行适合大作业、毕业设计参考及Unity游戏开发实战练习可帮助学习者快速理解游戏架构设计、代码组织与资源管理等多个环节的衔接与实现。1. 先看懂这个标题餐厅经营游戏项目到底在交付什么某开发者选了「基于Unity的餐厅经营游戏」当毕业设计题目前两周把餐厅模型、桌椅和按钮都摆好了以为剩下的只是贴图结果一运行发现顾客不吃不喝、金币不动、满意度狂掉。经营游戏的核心从来不是场景好看而是金币、饥饿值、满意度这套数值循环能自洽地转起来。这篇文章就沿着这个标题讲透一个Unity餐厅经营游戏项目里玩法循环怎么拆、C#脚本怎么写、存档数据库怎么接、论文和演示视频怎么补齐。适合正在做毕设、想用完整游戏项目练手、或需要复现一套「源码数据库论文演示视频」交付物的人照着走就能跑通。2. 拆解方案玩法闭环、场景结构、脚本目录与数据选型2.1 先想清楚玩法循环再动手金币、饥饿值与满意度的闭合回路餐厅经营游戏看起来是「做菜上菜」实际上是一套完整的数值闭合回路顾客进店坐下根据饥饿值点单订单进入队列玩家完成烹饪并上菜顾客按价格付钱满意度随之变化金币增减再到解锁新菜品、升级桌椅最后吸引更多顾客。这个回路如果有一环断了游戏就变成「装修模拟器」——这也是很多demo翻车的根本原因美术做了一堆数值模型没立住。我一般会先把核心参数写在纸上再开始搭Unity项目。给一组可以直接抄的初始数值游戏节奏就能立刻跑起来参数示例值影响初始金币100决定前期容错空间汉堡售价15收入曲线的基准汉堡烹饪时间2.5秒单份产能上限顾客等待上限20秒容错空间与紧张感满意度衰减1.2点/秒难度曲线陡峭程度满意度触发离开阈值30惩罚点低于就离店注意等待时长和烹饪时长的关系一份汉堡2.5秒一个顾客等20秒理论上能接待7桌左右。参数之间是乘法关系不是孤立常数。改烹饪时间或等待上限会影响全局节奏所以先定数值模型再写代码否则后面每改一个数字都要翻一堆脚本。2.2 搭建Unity项目骨架场景划分、Canvas层级与脚本目录规范项目骨架决定了后期写论文、调bug和扩展功能时的效率。场景我一般切三个MainMenu、GameScene、ResultScene。游戏主逻辑全部放在GameScene菜单只负责读档和进入游戏结算场景单独放统计面板——这样场景职责清楚答辩时也容易解释。Assets/ ├── Scenes/ │ ├── MainMenu.unity │ ├── GameScene.unity │ └── ResultScene.unity ├── Scripts/ │ ├── Core/ │ │ ├── GameManager.cs │ │ ├── OrderManager.cs │ │ └── AudioManager.cs │ ├── Data/ │ │ ├── SaveData.cs │ │ └── DatabaseHelper.cs │ ├── NPC/ │ │ ├── Customer.cs │ │ └── CustomerSpawner.cs │ └── UI/ │ ├── HUDController.cs │ └── OrderPanel.cs ├── Plugins/ │ └── sqlite3.dll ├── Resources/ │ └── Config/ │ └── game_params.json └── Prefabs/ ├── Customer.prefab └── Table.prefab这个结构有几个好处。Core管全局状态NPC管顾客行为UI只管显示Data层单独隔离数据库和存档——类与类之间的依赖方向是单向的UI不会直接操作数据库都是通过GameManager转发。写论文画类图时按这个目录画就是现成的模块划分。Canvas层级也需要提前定。经营HUD、订单面板、暂停弹窗不要放同一个Canvas里互相遮挡我习惯拆四层背景层、经营HUD层、订单对话层、暂停弹窗层。层与层之间用Sorting Order控制而不是靠创建顺序否则后面加一个弹窗就会盖住按钮。2.3 数据层选型JSON与SQLite怎么选数据库到底存什么一个餐厅经营游戏的数据层通常要处理两类数据一类是菜品价格、烹饪时间、解锁条件这类配置数据读多写少另一类是订单流水、金币变化、经营统计这类累计数据需要追加和查询。两类数据用同一种方案处理都会难受所以我一般拆开JSON管配置SQLite管存档和流水。维度JSONSQLite读写方式整个文件加载SQL语句查询适合数据参数配置、小体量存档订单流水、统计报表打包路径Resources可直接读取需拷到持久化目录查询能力弱强支持聚合统计维护成本低中数据库里我固定建三张表菜品表dish、订单流水表orders、存档表save_state。订单流水落库的意义不只是存档更是给论文准备数据材料——答辩时展示「近30局收入统计」「超时订单占比」这些图表全部来自orders表比口头说玩法做了多久更有说服力。这就是为什么数据库不是可选项而是这个项目的交付证据链。JSON配置则使用Unity的JsonUtility解析注意它不能直接解析数组根节点需要包一层对象。这个坑后面会专门说。3. 把最小可跑的经营循环写成C#代码点单、状态机与数值配置3.1 点餐与上菜从鼠标点击到订单完成的最小命令链先跑通最短链路顾客下单订单出现在UI面板玩家点击完成金币增加。不要一上来就做完整生产链先闭环再丰富细节。下面是一段可用的OrderManager核心代码using System.Collections.Generic; using UnityEngine; public class OrderManager : MonoBehaviour { public static OrderManager Instance; public int currentMoney 100; public float customerSatisfaction 100f; private QueueOrderData pendingOrders new QueueOrderData(); private void Awake() { Instance this; } // 顾客点单订单入队同时通知UI刷新 public void AddOrder(CustomerController customer, DishData dish) { OrderData order new OrderData { customerId customer.Id, dishName dish.name, price dish.price, cookTime dish.cookTime, remainingTime dish.waitLimit }; pendingOrders.Enqueue(order); UIManager.Instance.ShowOrder(order); } // 玩家点击“完成订单”先检查菜品是否已备好 public bool TryCompleteOrder(OrderData order) { if (InventoryManager.Instance ! null InventoryManager.Instance.HasDish(order.dishName)) { currentMoney order.price; customerSatisfaction Mathf.Min(100f, customerSatisfaction 5f); pendingOrders new QueueOrderData(RemoveOrder(pendingOrders, order)); UIManager.Instance.RemoveOrder(order); return true; } return false; } }这段代码用Queue实现先来先服务顾客点单顺序就是订单队列顺序UI面板从队头渲染。TryCompleteOrder返回bool方便调用方知道操作是否成功再做对应的音效或提示。OrderData里remainingTime是给顾客等待超时用的后面状态机会消费这个字段。参数说明customerSatisfaction上限用Mathf.Min卡在100避免重复奖励把满意度刷爆每次完成订单加5点满意度是临时值正式项目应放到JSON配置里后面3.3会讲。InventoryManager的HasDish方法是判断「这道菜已经做好」在最小链路里可以先返回true等加了烹饪系统再替换成真实库存判断。3.2 顾客状态机进店、等待、用餐、离开四态怎么切顾客是经营游戏的「心跳」几乎所有数值变化都由顾客状态驱动。这里我不推荐给每个顾客挂Animator状态机——表现层可以用Animator但逻辑层用枚举状态机更直接状态切换点一目了然出bug时打个日志就能看到顾客卡在哪个状态。public enum CustomerState { Entering, // 进店走向空桌 Waiting, // 坐下等待点餐或上菜 Dining, // 用餐中结束后付钱离开 Leaving // 离开移出场景 } public class CustomerController : MonoBehaviour { public int Id { get; private set; } public CustomerState State { get; private set; } public float patience 20f; public void SetState(CustomerState next) { State next; OnStateChanged?.Invoke(State); } private void Update() { if (State CustomerState.Waiting) { patience - Time.deltaTime; if (patience 0f) { // 等待超时满意度惩罚并离店 GameManager.Instance.AddSatisfaction(-10f); SetState(CustomerState.Leaving); } } else if (State CustomerState.Dining) { // 用餐倒计时结束后结算收入 diningTimer - Time.deltaTime; if (diningTimer 0f) { GameManager.Instance.AddMoney(dishPrice); SetState(CustomerState.Leaving); } } } }状态机逻辑核心是把「状态」和「表现」分离。Entering时让NavMeshAgent走向座位到了就SetState(Waiting)Waiting时只盯耐心值Dining时盯用餐倒计时。每个状态只做一件事不会出现顾客一边点餐一边吃饭的错乱。参数说明patience是每个顾客实例独立的字段异常值不要写成全局静态量否则一个顾客超时所有顾客清空AddSatisfaction由GameManager统一控制方便做全局暂停。这里的GameManager也是后面论文里「单例管理类」的典型例子答辩常被问能讲清楚就行。3.3 经营数值调整用JSON配置代替散落常量餐厅经营游戏的数值调整频率非常高——调售价、改等待时间、加新菜品如果每个数值都是脚本里的public字段改一版要重新编译、重新进场景验证非常痛苦。我习惯把全部经营参数集中到一个JSON文件放在Resources/Config下启动时加载一次。{ moneyStart: 100, dishList: [ { id: 1, name: 汉堡, price: 15, cookTime: 2.5, waitLimit: 20, unlockLevel: 0 }, { id: 2, name: 牛排, price: 35, cookTime: 4.0, waitLimit: 18, unlockLevel: 3 } ], satisfaction: { decayPerSecond: 1.2, punishOnLeave: 10, rewardOnDine: 5 } }注意这里的字段命名要和C#类的字段名一致JsonUtility是按字段名匹配的。dishList放在外层对象里这是给Unity JsonUtility用的固定写法——不支持数组作为根节点必须包一层。using UnityEngine; public class GameConfig : MonoBehaviour { public static GameConfig Instance; public GameParams Params { get; private set; } private void Awake() { Instance this; TextAsset asset Resources.LoadTextAsset(Config/game_params); Params JsonUtility.FromJsonGameParams(asset.text); } }Resources.Load路径不需要扩展名文件名对应具体资源路径。改成JSON配置后调数值只需要修改文件不用动任何代码。这也是「数据驱动开发」在游戏项目里的最小落地形式写论文时能单独拎出来讲一节。参数说明dishList的unlockLevel对应金币解锁阈值在GameManager里每次加钱后检查当前金币是否达到下一个解锁等级达到则激活对应菜品按钮。cookTime不是给顾客状态机用的是给烹饪进度条用的两者分开别混成一个字段——这是常见的逻辑混淆坑。4. 数据库与存档SQLite接入、建表与三处必改配置4.1 把SQLite接进UnityDLL放置、路径初始化与最小连接代码Unity接SQLite的方式在Windows编辑器和移动端是有差异的。通常做法是把对应目标平台的sqlite3.dll放到Assets/Plugins目录然后使用Mono.Data.Sqlite命名空间做托管层。这里最关键的是数据库文件路径编辑器下放项目根目录方便查看真机上必须放到Application.persistentDataPath这是Unity给每个应用的持久化目录。using UnityEngine; using Mono.Data.Sqlite; using System.IO; public class DatabaseHelper { private string connectionString; public void Init() { string dbPath GetDbPath(); if (!File.Exists(dbPath)) { // 首次启动从Resources模板写入持久化目录 TextAsset template Resources.LoadTextAsset(Data/game_template); File.WriteAllBytes(dbPath, template.bytes); } connectionString $URIfile:{dbPath}; } private string GetDbPath() { #if UNITY_EDITOR return Path.Combine(Application.dataPath, ../game.db); #else return Path.Combine(Application.persistentDataPath, game.db); #endif } public SqliteConnection Open() { var conn new SqliteConnection(connectionString); conn.Open(); return conn; } }这段代码解决了三个常见问题路径区分、首次启动模板创建、连接串格式。Resource里放一个空的game_template数据库文件本质上是一个「数据库模板」正式运行时复制到持久化目录再连接。我踩过一个很深的坑资源模板也要放在Resources下而不是StreamingAssets下否则Android端读模板文件流程完全不同这个在避坑章详细展开。条件编译指令UNITY_EDITOR让编辑器下直接读项目根目录调试时可以随时用数据库工具打开检查表结构比在真机上分析方便太多。4.2 建表与订单落库三张表的字段设计与为什么这么拆数据库表结构设计要跟着论文的数据分析需求走。我的三张表各有分工dish表存菜品静态信息orders表存每一笔订单流水save_state表存单行存档。订单流水和存档必须分表否则每次读档要把几百条历史订单全加载进内存。CREATE TABLE IF NOT EXISTS dish ( id INTEGER PRIMARY KEY AUTOINCREMENT, dish_name TEXT NOT NULL, price INTEGER NOT NULL, cook_time REAL NOT NULL, unlock_level INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id TEXT NOT NULL, dish_name TEXT NOT NULL, order_time TEXT DEFAULT (datetime(now, localtime)), result TEXT NOT NULL, income INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS save_state ( id INTEGER PRIMARY KEY CHECK (id 1), money INTEGER NOT NULL, satisfaction REAL NOT NULL, unlocked_level INTEGER DEFAULT 1, updated_at TEXT DEFAULT (datetime(now, localtime)) );orders表里result字段记录订单是「完成completed」还是「超时timeout」income记录实际收入。这个设计让论文的统计图表可以直接用SQL聚合查询生成——比如「超时订单占比」「各菜品销量排行」一句GROUP BY就能出数据不需要额外记日志。参数说明save_state表用CHECK(id 1)保证只有一行这是存档表的常见写法避免并发出现多条存档记录。order_time用localtime让时间戳可读直接存本地时间如果项目需要多时区统计再改存UTC。4.3 连接管理与批量写入别在Update和协程里反复开关数据库SQLite连接不是线程安全的Unity主线程操作也要小心。最常见的问题是有人在Update里每帧读写数据库导致卡顿和连接泄漏。正确的做法是游戏运行期间把订单记录先存在内存List里场景退出或固定时间点批量写入。public void BatchInsertOrders(ListOrderRecord records) { using (var conn Open()) using (var cmd conn.CreateCommand()) { cmd.CommandText BEGIN TRANSACTION;; foreach (var r in records) { cmd.CommandText string.Format( INSERT INTO orders (customer_id, dish_name, result, income) VALUES ({0}, {1}, {2}, {3});, r.customerId, r.dishName, r.result, r.income); } cmd.CommandText COMMIT;; cmd.ExecuteNonQuery(); } }事务批量写入比逐条insert快一个数量级因为磁盘I/O集中在一次提交。这里直接拼接SQL是可行的——所有字段来自游戏内部生成不来自玩家输入如果将来要做玩家自定义昵称就必须改成参数化查询防止SQL注入这个习惯要一开始就养成。我一般把批量写入的时机放在两个地方玩家进入结算场景时写一次、游戏正常退出时写一次。不要在每帧Update里写也不要在协程里与yield混用协程恢复时如果场景已销毁数据库连接对象可能已释放会抛异常。这些都是被验证过的高频翻车点。5. Unity餐厅经营游戏常见问题与避坑5条高频翻车记录5.1 顾客走到桌前原地打转NavMeshAgent与桌子碰撞体现象顾客到达桌子附近后原地转圈不坐下偶尔还会穿模。原因桌子Prefab自带Collider烘焙NavMesh时把桌子当成障碍物NavMeshAgent的目标点又刚好在障碍物中心agent判断不可达就原地徘徊另一种情况是目标点设在了桌子内部与碰撞体重叠路径系统认为目标被阻挡。解决不要在桌子中心放目标点。我在每张桌子旁边放一个空的子物体命名SeatPoint位置略偏在桌角外侧NavMeshAgent的目标点指向SeatPoint同时把桌子的NavMesh Obstacle勾上Carve重新Bake。顾客到达SeatPoint后触发SetState(Waiting)再播一个坐下的动画就行。这个做法的好处是即使后面换桌子模型也不用改寻路代码。5.2 真机上存档只有第一次能读StreamingAssets路径是只读的现象编辑器下数据库读写正常打包到Android后第一次运行能读档退出重进发现存档丢失有时直接报FileNotFoundException。原因StreamingAssets在Android打包后位于APK的压缩包内部不能直接当普通文件路径读写。很多人先写Path.Combine(Application.streamingAssetsPath, game.db)做初始化编辑器下没问题Android下一运行就炸。解决数据库文件模板放进Resources下用Resources.Load读byte数组再写入persistentDataPath后续所有连接都指向persistentDataPath下的文件。注意不要用File.Copy去拷StreamingAssets目录——Android上那个路径是jar:file://开头的URL协议不是真实文件路径File.Copy一定失败。绕开StreamingAssets用Resources统一处理是兼容性最稳的方案。5.3 点了暂停顾客耐心还在掉timeScale与协程的关系现象实现暂停功能后UI按钮都停了但顾客依然在超时离店满意度狂掉。原因Time.timeScale 0只影响Time.deltaTime而协程里的WaitForSeconds不受timeScale控制——很多人以为协程会跟着暂停实际不会。CustomerController里如果用WaitForSeconds做等待倒计时暂停状态下一律照跑。解决所有经营倒计时统一改成每帧累减并在Update开头判断暂停状态if (GameManager.Instance.IsPaused) return; timer - Time.deltaTime;不要用Time.unscaledDeltaTime去补偿那会让倒计时在暂停时继续走。一局游戏里的时间逻辑全部走GameManager的IsPaused开关禁止散落各脚本自行判断这是最省心的做法。顺带排查其他依赖WaitForSeconds的逻辑比如顾客入场延迟、UI提示消失延迟都应该支持暂停判断。5.4 订单按钮点了没反应EventSystem与RaycastTarget现象订单面板的按钮能看见、能悬停但点击完全无响应场景里的3D物体反而能选中。原因场景里没有EventSystem或者面板上层有一个全屏Image占位它的RaycastTarget属性是打开的挡住了下方按钮的射线检测。这种情况在复制场景、重组UI后尤其常见。解决确认场景里只有一个EventSystem然后检查UI层级把纯装饰性Image的RaycastTarget全部取消勾选只保留Button、Toggle、InputField等交互组件勾选。排查顺序我按经验固定先看EventSystem是否存在再看目标按钮上层有没有遮挡物最后看Canvas的Sorting Order是否被其他Canvas压在下面——按这个顺序绝大多数「点了没反应」都能定位。5.5 打包后中文变成方块动态字体依赖真机系统字库现象编辑器里按钮文字正常显示中文打包到Android后全部变成「□□」。原因Unity默认的Arial动态字体在编辑器下会回退到操作系统字体所以中文显示正常真机上没有中文字库时动态字体找不到回退字体直接渲染成方块。解决在项目里放一个开源的TTF中文字体文件为所有Text组件的Font字段手动赋值。如果项目已经大量使用TextMeshPro要同时检查TMP的Font Asset是否也用了中文字体TMP默认字体同样不含中文字形。另一个办法是字库只包含用到的字符生成Font Asset体量小、加载快适合菜品名和UI按钮这类固定文本但经营游戏里如果要做玩家自定义店名动态字体反而更省事。二选一之前先把「用到的所有字符」列出来再决定用哪套方案。6. 让交付更像成品参数表、论文配图与演示录像的三项收尾功夫验收到最后阶段代码能跑只是起点毕设和作品集更看重「完整性和可解释性」。我在这类项目里总结了三项收尾功夫参数表、论文配图、演示录像。参数表是把所有可调数值汇总成一张对照表放在项目文档里。包含字段名、默认值、调整影响三列比如「waitLimit 20秒 影响顾客容错」「decayPerSecond 1.2 影响难度曲线」。这张表的价值在于答辩时老师问「这个数值为什么这么定」你能说出「是参数调整出来的」而不是「随手写的」自己改数值时也有一张地图不会越改越乱。论文配图要跟实际项目代码对齐最忌讳画一套与实现不符的图。我的画法很简单用例图画角色与行为按Player、Customer、System三方画类图直接对照Scripts目录画Core、Data、NPC、UI四个模块正好对应四组类ER图画三张表orders和dish之间用dish_name关联序列图画「点餐→上菜→结算」主流程。每张图的元素不超过12个太多显得臃肿太少显得假。演示视频的拍法比想象中重要很多项目代码和论文都不错视频拍得像场景漫游把游戏性浪费了。我按四段镜头录镜头内容时长开场主菜单进入游戏展示店名5~8秒经营顾客进店、点餐、上菜、收钱全流程15~20秒高峰多桌同时点餐订单队列滚动10秒收尾结算面板展示金币、满意度切到数据库流水界面10秒录制时固定用手机或录屏软件别用手持拍摄屏幕画面稳一点观众才会把注意力放在数值变化和玩法上。我早期做类似项目时把一半时间花在改贴图上最后熬夜补论文图和数据库实在不划算。这套项目方向真正值得投入的地方在于数值闭环、数据可查、文档与代码对齐——做到这三件事生态位就比纯堆美术的更稳后续做其他经营类项目也直接复用这套框架。希望帮到你。本文还有配套的精品资源点击获取