Unity餐厅经营游戏开发全攻略:状态机、SQLite存档与避坑实践

📅 发布时间:2026/10/10 6:58:37
Unity餐厅经营游戏开发全攻略:状态机、SQLite存档与避坑实践
简介这是一份基于Unity引擎、以C#为核心的餐厅经营游戏本科毕业设计项目面向计算机专业正在完成大作业或毕业设计的同学也适合需要Unity项目实战练习的开发者。压缩包共916个文件核心包含142个C#脚本、119个DLL依赖库、33个FBX模型、21个Prefab预制体及7个Unity场景文件同时附带数据库、7个PDF论文相关文档和演示多媒体材料整体包体约226.97MB目录结构清晰便于按模块检索与学习。目前已有78人学习下载。该项目为导师指导并认可的高分设计评审分达98分源码经本地编译调试可正常运行。除完整项目源码外还提供数据库文件、毕业论文及演示视频可系统参考餐厅经营游戏的场景搭建、角色交互、经营逻辑与UI实现等关键模块是一份完整度较高的毕业设计实战样例。1. 先想清楚Unity餐厅经营游戏凭什么值得做成毕业设计把一个餐厅经营游戏当毕业设计最难的不是让菜能端上桌而是让「顾客进门、点餐、等餐、用餐、结账」这一条链路从头到尾不出错地跑起来。很多同学一开始只盯着做饭动画和金币数字最后被顾客排队卡死、上菜重复、存档读不出来这些事折磨到改题。实际上这类项目是最适合本科阶段展示完整软件工程能力的方向它同时覆盖Unity场景管理、C#面向对象设计、数据库持久化和UI交互四块正好对应论文的需求分析、系统设计、数据库设计、系统实现四个章节。这篇笔记把开发路径、核心脚本、数据库结构和踩坑记录完整拆开给你一份可以直接照做的落地清单。2. 先立起系统和论文骨架餐厅经营游戏的模块划分与Unity选型2.1 经营循环不是「做饭模拟」是七个状态的事件流餐厅经营游戏表面上是做菜内里是一个离散事件模拟系统。顾客进店、找座、点单、等餐、吃饭、离店加上厨师做菜、服务员上菜每一步都由事件触发而不是靠玩家自由操作。把这个事件流理清楚论文里的「需求分析」章节就有了一半内容。我一般会先把经营循环拆成七个状态待客、引座、点单、备餐、上菜、用餐、结账。每个状态由前一个状态的事件触发比如点单完成后才进入备餐备餐完成才触发上菜。public enum CustomerState { Entering, // 进门 FindingSeat,// 找座位 Ordering, // 点单 Waiting, // 等餐 Eating, // 用餐 Payment, // 结账 Leaving // 离店 }这个枚举是整个顾客AI的地基。状态枚举对应Unity的Animator参数每切换一个状态同步播放入场、坐下、举手表态、进食、掏钱这些动画。关键点是状态切换必须通过一个统一的控制器方法而不是在每个脚本里直接改枚举值否则后面加「顾客等太久生气离店」这类分支时状态会乱成一团。参数说明Entering到FindingSeat之间不是自动流转的要检测座位是否空闲。我在实际项目里用一个SeatManager维护空闲座位列表顾客进入FindingSeat时向它请求座位拿到座位坐标后才继续。这套设计在论文里能画成状态图答辩时有东西可讲。2.2 为什么核心逻辑必须用纯C#而不是全靠插件餐厅经营游戏有一个诱惑网上能找到大量现成的资源商店插件帮你做订单系统、顾客AI、寻路。但我强烈建议核心逻辑全部手写C#原因不是「插件不好」而是毕业设计论文需要你讲清楚每一个机制的实现。常见做法是场景美化、UI特效可以少量用插件但订单流转、顾客状态、数据存取这三个模块必须纯手写。理由有三第一插件是黑匣子论文里写不出设计思路答辩被追问就容易露馅第二插件的API更新频率和Unity版本绑定一旦版本升级你的项目可能整个跑不起来第三手写C#代码量是论文「系统实现」章节的重要支撑一个订单类加一个状态机控制器代码量轻松上千行完全够写。另外餐厅经营游戏的逻辑复杂度适中纯C#完全扛得住。我做过一个模拟项目X同时在线30个顾客每个顾客每帧只做两三次简单的状态检查和数值更新用List遍历毫无压力。真正吃性能的是UI刷新和寻路这两块用Unity自带的UGUI和NavMesh就能解决不需要额外引入重型框架。2.3 把论文目录映射成模块清单做一个毕业设计代码和论文是一体两面。我的习惯是在动手写代码前先把论文目录列出来再反推每个章节需要什么代码模块。餐厅经营游戏的标准论文目录和代码模块对应关系如下。论文章节对应代码模块需要产出的核心类需求分析经营循环流程、角色行为定义CustomerController、OrderSystem系统设计模块划分、类图、状态图GameManager、SceneLoader数据库设计菜品表、订单表、存档表DatabaseHelper、SaveData系统实现核心逻辑代码与算法说明Order、SeatManager、UIManager这个映射关系的重要性在于它决定了每一块代码要写到什么完成度。比如数据库设计这章要求有建表语句和ER图那你至少要在项目里真实跑通SQLite的增删改查而不是只在文档里画几张表。我见过有同学论文写了完整的数据库设计结果项目里用的是PlayerPrefs答辩时被老师一追问就卡壳了——数据库代码必须真实落地。3. 跑通第一单生意场景搭建、C#脚本与订单闭环3.1 最小场景一张桌子、一个厨师、一条点单入口不要一开始就搭建豪华餐厅。先用一张桌子、一个厨师岗位、一个入口跑通「顾客进门→坐下→点单→出菜→用餐→结账→离店」的最小闭环。这个闭环跑通后剩下的都是复制粘贴和扩展。场景搭建我建议用Unity的2D模式用Sprite做角色和桌椅用Tilemap画地板。理由很简单餐厅经营的核心玩法是逻辑和数值不是3D建模2D能省掉大量美术时间而且UGUI和2D Sprite的坐标系统一致调试起来不费劲。using UnityEngine; public class OrderStation : MonoBehaviour { public Transform cookPosition; // 厨师站立点 public Transform plateSpawnPoint; // 菜品生成点 public float cookDuration 3f; // 单道菜制作时间秒 public bool IsCooking { get; private set; } public IEnumerator Cook(Order order) { IsCooking true; Debug.Log($开始烹饪 {order.DishName}耗时 {cookDuration} 秒); yield return new WaitForSeconds(cookDuration); Instantiate(order.dishPrefab, plateSpawnPoint.position, Quaternion.identity); IsCooking false; order.OnCookCompleted?.Invoke(order); } }OrderStation挂在一个空物体上代表「厨房出菜口」。核心逻辑是Cook这个协程收到订单后锁住出菜口等待指定秒数后生成菜品预制体再触发订单完成事件。IsCooking属性用于防止多个订单同时占用出菜口——如果顾客A的菜还没做完顾客B的订单就得排队。参数说明cookDuration是经营数值的一部分直接影响游戏节奏。我一般设成3到5秒太短玩家没操作感太长顾客满意度掉得太快。这个参数在论文里可以做成数值平衡表的示例让老师看到你有意识地调过参数。3.2 顾客状态机从进门到落座的核心控制器顾客AI是整个项目最复杂的单体脚本。它要同时处理移动、动画、订单发起和满意度变化。我习惯把一个完整的顾客行为写进一个CustomerController内部通过状态枚举切换逻辑而不是拆成多个组件互相呼叫。using System.Collections; using UnityEngine; public class CustomerController : MonoBehaviour { public CustomerState currentState CustomerState.Entering; public float patience 30f; // 耐心值秒归零后离店 public float speed 2f; // 移动速度 private Transform targetSeat; private Animator animator; private OrderSystem orderSystem; private float patienceTimer; private void Awake() { animator GetComponentAnimator(); orderSystem FindObjectOfTypeOrderSystem(); } public void AssignSeat(Transform seat) targetSeat seat; private void Update() { switch (currentState) { case CustomerState.Entering: MoveTo(targetSeat); break; case CustomerState.FindingSeat: if (SeatManager.Instance.IsSeatAvailable(out Transform seat)) { AssignSeat(seat); currentState CustomerState.Ordering; orderSystem.SubmitOrder(this); } break; case CustomerState.Waiting: patienceTimer Time.deltaTime; if (patienceTimer patience) { currentState CustomerState.Leaving; Debug.Log(顾客等太久生气离店); } break; } } private void MoveTo(Transform target) { Vector3 direction (target.position - transform.position).normalized; transform.position direction * speed * Time.deltaTime; animator.SetBool(Walking, direction.magnitude 0.1f); } }这个脚本的关键是状态切换只发生在明确的事件点找到座位才提交订单等待超时才离店。patienceTimer只在Waiting状态下累计不会在其他状态误触发超时逻辑。MoveTo用最朴素的直线移动没有加寻路因为场景里最远距离也就几个屏宽直线走完全够。参数说明patience是顾客满意度的核心参数我建议搭配「菜品等待时间」一起调。比如一道菜3秒出顾客耐心30秒理论上能容忍10道菜的等待时长但玩家同时接待多桌时后面的顾客会明显不满这就是经营压力来源。3.3 订单生命周期定制一份带超时判定的Order类订单不只是「菜名 状态」它还要记录顾客、菜品、下单时间、完成时间并对外发出状态变化事件。这些字段在论文数据库设计里都能对应到订单表值得在一开始就设计完整。using System; using UnityEngine; public class Order { public string DishName { get; } public CustomerController Customer { get; } public int TableId { get; } public DateTime CreatedAt { get; } public DateTime? CompletedAt { get; private set; } public GameObject dishPrefab { get; } public event ActionOrder OnCookCompleted; public event ActionOrder OnOrderExpired; public bool IsExpired DateTime.Now - CreatedAt TimeSpan.FromSeconds(60); public Order(string dishName, CustomerController customer, int tableId, GameObject dishPrefab) { DishName dishName; Customer customer; TableId tableId; CreatedAt DateTime.Now; this.dishPrefab dishPrefab; } public void MarkCompleted() { CompletedAt DateTime.Now; Debug.Log($订单完成{DishName}顾客桌号 {TableId}); OnCookCompleted?.Invoke(this); } public void CheckExpired() { if (IsExpired) { OnOrderExpired?.Invoke(this); } } }Order是一个纯C#对象不继承MonoBehaviour因为订单本身不需要挂载在场景物体上把它当数据对象处理更干净。CreatedAt记录下单时间IsExpired用系统时间判断是否超时这样即使游戏暂停Time.timeScale 0超时判定也不会停下来逻辑上更接近真实餐厅的压力场景。参数说明TimeSpan.FromSeconds(60)是订单超时上限。这个值和顾客的patience是两套独立的超时逻辑——顾客耐心值影响的是「顾客体验」订单超时影响的是「厨房生产」。实际项目里我把「顾客耐心」调得比「订单超时」短这样顾客会在订单超时前就离店避免菜做出来了却没人吃的尴尬状态。4. 数据库与存档让经营进度和论文数据层都站得住4.1 建四张表菜品、订单、顾客行为、存档数据库部分我推荐直接上SQLite不需要额外搭服务器。SQLite是单文件数据库Unity通过官方包或第三方库就能访问而且建表语句和标准SQL基本一致论文里写起来专业。餐厅经营游戏最少需要四张表菜品表菜名、价格、制作时间、解锁等级、订单表订单号、桌号、菜品ID、下单时间、完成时间、顾客行为表顾客ID、进店时间、满意度、离店原因、存档表金币、解锁菜品列表、当前关卡进度。CREATE TABLE IF NOT EXISTS Dish ( DishId INTEGER PRIMARY KEY AUTOINCREMENT, DishName TEXT NOT NULL, Price INTEGER NOT NULL DEFAULT 10, CookTime REAL NOT NULL DEFAULT 3, UnlockLevel INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS Orders ( OrderId INTEGER PRIMARY KEY AUTOINCREMENT, TableId INTEGER NOT NULL, DishId INTEGER NOT NULL, CustomerId INTEGER NOT NULL, CreatedAt TEXT NOT NULL, CompletedAt TEXT, Status INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS CustomerLog ( CustomerId INTEGER PRIMARY KEY AUTOINCREMENT, EnterTime TEXT NOT NULL, Satisfaction FLOAT NOT NULL DEFAULT 100, LeaveReason TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS SaveData ( Id INTEGER PRIMARY KEY CHECK (Id 1), Gold INTEGER NOT NULL DEFAULT 0, UnlockedDishes TEXT NOT NULL DEFAULT [], Level INTEGER NOT NULL DEFAULT 1 );这几张表的设计逻辑是菜谱是静态配置数据玩家经营过程中不会改表结构只改数据订单和顾客行为是流水记录每产生一笔记录就写入存档表是单行表用CHECK (Id 1)约束保证只有一行避免多条存档数据冲突。UnlockedDishes用JSON字符串存解锁的菜ID列表省去建关联表。参数说明Satisfaction是浮点字段存0到100之间的值对应顾客离店时的满意度快照。这个字段在论文中可以做统计分析——比如统计满意度低于50的订单比例能反推出哪些菜品制作时间太长这比拍脑袋调参数更有说服力。4.2 用SQLite做持久化Helper封装与Android打包前检查Unity里用SQLite有一个绕不开的坑桌面编辑器Windows/Mac和移动平台Android/iOS的底层SQLite实现不同直接引用系统库在编辑器里能跑打包到安卓就报找不到DllNotFoundException。我踩过这个坑后来固定了一个稳妥的封装方案。using System; using System.IO; using Mono.Data.Sqlite; using UnityEngine; public static class DatabaseHelper { private static string connectionString; private static string GetConnectionString() { if (connectionString ! null) return connectionString; string dbPath Path.Combine(Application.streamingAssetsPath, game.db); connectionString $URIfile:{dbPath}; return connectionString; } public static void ExecuteQuery(string sql, ActionSqliteDataReader callback null) { using (var connection new SqliteConnection(GetConnectionString())) { connection.Open(); using (var command connection.CreateCommand()) { command.CommandText sql; using (var reader command.ExecuteReader()) { callback?.Invoke(reader); } } connection.Close(); } } }这段代码把数据库连接过程统一收口。关键点是GetConnectionString()只计算一次避免每次查询都重新拼路径。Application.streamingAssetsPath是Unity规定的只读目录桌面端把game.db放进 StreamingAssets 文件夹即可安卓端则需要先把这个文件复制到可写目录否则无法写入流水数据。参数说明callback参数用于读取查询结果传入一个接收SqliteDataReader的匿名函数即可。查询和写入统一走这个方法业务层不需要关心连接细节。Android打包前还有一个必须做的检查在Assets/Plugins/Android下放置对应的libsqlite.so这个问题在第5章避坑部分再展开。4.3 Json热更新存档与数据库冷备的配合SQLite存储的是全量流水数据但游戏读档时如果每次都要从数据库重建对象启动会很慢。我的做法是双轨制游戏运行时用Json文件保存当前状态数据库保存历史流水和配置数据两者互不干扰。using System.Collections.Generic; using System.IO; using UnityEngine; [System.Serializable] public class SaveData { public int gold; public Listint unlockedDishes new Listint(); public int level; public Vector3 playerPosition; } public static class SaveManager { private static string SavePath Path.Combine(Application.persistentDataPath, save.json); public static void Save(SaveData data) { string json JsonUtility.ToJson(data, true); File.WriteAllText(SavePath, json); Debug.Log($存档写入{SavePath}); DatabaseHelper.ExecuteQuery( INSERT INTO SaveData (Gold, UnlockedDishes, Level) VALUES (Gold, UnlockedDishes, Level) ON CONFLICT(Id) DO UPDATE SET Goldexcluded.Gold, UnlockedDishesexcluded.UnlockedDishes, Levelexcluded.Level; ); } public static SaveData Load() { if (!File.Exists(SavePath)) return new SaveData(); string json File.ReadAllText(SavePath); return JsonUtility.FromJsonSaveData(json); } }Json存档负责快速读档启动数据库负责记录历史汇总。ON CONFLICT(Id) DO UPDATE是SQLite的upsert语法写入时如果存档表已经有一行就更新没有就新建。playerPosition字段是序列化存档的一个典型示例——Unity的Vector3不能直接存入Json但通过JsonUtility会自动展开为x、y、z三个浮点数。参数说明JsonUtility.ToJson(data, true)第二个参数是prettyPrint设为true后存档文件可读性强出Bug时直接打开save.json排查数值非常方便。这也是答辩时的一个加分项——你可以现场演示打开存档文件指出其中某个字段对应游戏里的哪个状态。5. 避坑与排查Unity餐厅经营游戏中反复出现的5个翻车点5.1 顾客排队卡死SeatManager没有正确释放座位现象第一桌顾客吃完离开后新顾客进店却一直在门口徘徊不往空座位走。检查空座标志位发现座位明明显示空闲但新顾客就是找不到。原因顾客的Leave状态里没有调用SeatManager.Instance.ReleaseSeat()座位虽然空了但内部维护的座位占用列表没有更新导致IsSeatAvailable永远返回false。解决在顾客进入Leaving状态时必须显式释放座位引用。case CustomerState.Leaving: SeatManager.Instance.ReleaseSeat(targetSeat); currentState CustomerState.Entering; // 实际应改为离场动画后销毁 break;这个坑的本质是「对象状态与组件状态不一致」顾客物体还在场景里但逻辑上已经离店。我后来养成了一个习惯凡是使用了「管理器 对象」这种配对关系的一定在对象销毁前完成解绑。这也是面试常问的悬空引用问题。5.2 SQLite在Android上崩溃DllNotFoundException现象编辑器里跑得好好的打包成APK装到手机上一运行报DllNotFoundException: sqlite3整个游戏直接闪退。原因Unity的Mono运行时不自带SQLite的原生库桌面端系统里有sqlite3.dll安卓端却没有。Mono.Data.Sqlite只是托管层底层的C库必须随包携带。解决在Assets/Plugins/Android目录下放置libsqlite3.so且必须匹配目标CPU架构arm64-v8a和armeabi-v7a两个目录都要放。另一个稳妥的替代方案是换用纯托管的SQLite实现但性能和兼容性都不如原生库。检查方法很直接打包前用压缩工具打开生成的APK搜索libsqlite确认所有需要的库都在。我至今保留着这个检查习惯每次打包都先看一眼APK里的原生库列表免得装到测试机上才暴露问题。5.3 顾客耐心值掉速和帧率挂钩Time.deltaTime用法错误现象60帧运行时顾客能等30秒在低配手机上帧率降到20帧顾客10秒就跑光了游戏难到没法玩。原因耐心值的累计写成了patience - 1这种「每帧扣一点」的写法扣减速度和帧率直接挂钩。解决统一改用Time.deltaTime累计并且把耐心值相关的逻辑从Update()中抽出来只做时间累加超时判断单独放在WaitForSecondsRealtime协程中。private void Update() { switch (currentState) { case CustomerState.Waiting: patienceTimer Time.deltaTime; // 帧率无关 if (patienceTimer patience) currentState CustomerState.Leaving; break; } }这条是Unity开发的经典考题所有时间相关逻辑必须用DeltaTime换算否则在不同设备上表现不一致。写毕业设计时尤其要注意因为答辩演示机性能参差不齐你自己电脑上测得好好的换台电脑可能就翻车。5.4 UI点击穿透UGUI的RaycastTarget把游戏操作吃掉了现象游戏里点击「上菜」按钮偶尔会触发角色移动或者按钮点了没反应。原因场景里存在多个全屏Image组件作为背景它们的RaycastTarget默认是勾选的UI阻挡了向游戏世界的射线投射点击被UI层吃掉。解决所有非交互UI元素背景图、装饰图、面板底图一律关闭RaycastTarget。这是一个小而经典的检查项。using UnityEngine; using UnityEngine.UI; public static class UITools { public static void DisableRaycastRecursively(Transform root) { foreach (Transform child in root) { var image child.GetComponentImage(); if (image ! null) image.raycastTarget false; DisableRaycastRecursively(child); } } }这个工具方法在场景搭建完成后跑一遍把整个UI下所有子节点的raycastTarget统一关掉需要接收点击的按钮再手动勾回来。我一般把这个脚本挂到一个[RuntimeInitializeOnLoadMethod]属性标记的静态方法里启动时自动执行省得手工检查几十个面板。5.5 一方面用Instantiate生成菜品一方面忘记回收内存只涨不跌现象游戏长时间运行后内存持续上涨帧率越来越低最后手机发烫严重。原因菜品预制体和顾客预制体用Instantiate创建顾客离开时只销毁了顾客菜品和餐具的预制体没有回收生成了大量垃圾对象。解决引入对象池把出菜口的菜品、进出门的顾客都改为从池中取出、用完归还。using UnityEngine; using System.Collections.Generic; public class ObjectPool : MonoBehaviour { private static ObjectPool _instance; public static ObjectPool Instance _instance; private Dictionarystring, QueueGameObject poolDict new Dictionarystring, QueueGameObject(); private void Awake() _instance this; public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation) { string key prefab.name.Replace((Clone), ); if (poolDict.ContainsKey(key) poolDict[key].Count 0) { GameObject obj poolDict[key].Dequeue(); obj.SetActive(true); obj.transform.SetPositionAndRotation(position, rotation); return obj; } return Instantiate(prefab, position, rotation); } public void Despawn(GameObject obj) { string key obj.name.Replace((Clone), ); obj.SetActive(false); if (!poolDict.ContainsKey(key)) poolDict[key] new QueueGameObject(); poolDict[key].Enqueue(obj); } }对象池的核心思路是复用菜品上桌后玩家拖动到垃圾桶即调用Despawn菜品进入池子下一次出菜直接Spawn从池子里取出避免重新实例化和触发GC。这个优化对餐厅经营游戏尤其重要因为顾客流量是持续的每秒都可能生成和销毁多个物体。参数说明poolDict的键是预制体的原始名字去掉(Clone)后缀来归并同类物体。这个方案在对象类型多、生成频率高的场景下很有效但要注意Despawn时必须先关闭物体会不会导致事件残留——上一章的订单超时事件如果还挂在对象上池子复用时可能触发旧的回调。6. 让毕设再往前走一步对象池、批量顾客与答辩演示素材如果你已经跑到第5章这个程度核心玩法肯定通了剩下的是把完成度再顶一档。我建议把精力集中做三件事性价比最高。第一件是把对象池铺开到所有高频生成物上。第5章已经写了菜品池顺手把顾客也池化顾客离店后不销毁缩小放进隐藏区域等下一个顾客进来时重新启用。这一步能显著拉高游戏的运行稳定性在答辩现场用一台普通笔记本连续演示20分钟不卡顿比任何口头解释都有说服力。第二件是做一个「压力测试模式」。用代码自动生成8到12个顾客同时进店观察订单积压量。这个模式不需要额外开发在现有系统上挂一个协程每隔几秒调用一次CustomerSpawner.Spawn()即可。测试结果可以截图作为论文测试章节的素材展示系统在峰值负载下的表现老师对这部分印象会非常深。第三件是从录制演示视频的角度反推项目优化。我一般会做一个「演示模式」金币自动增加、顾客满意度默认拉高、把所有解锁菜品打开。这个模式只用几行代码但能保证演示视频从头到尾都是流畅的正向反馈。录制时用Unity自带的Recorder窗口设置输出1080p、30帧即可重点是把每一段演示和论文章节对应起来开场展示场景架构、中间展示顾客状态切换、结尾展示存档文件的变化。还有一个答辩常用小技巧在主界面加一个「Debug信息」开关打开后显示当前顾客数、订单数、内存占用。这个调试面板本身就是一篇论文的展示素材——你可以现场演示顾客压力测试同时让面板上的数字涨起来直观证明系统稳定性。另外养成一个习惯每次改完数值都打开save.json看一眼确认实际存进去的和预期一致。这些优化做完你的项目就不再是一个「能跑的小游戏」而是一个「有稳定性验证、有性能优化、有数据支撑」的完整软件工程作品。拿这个状态去写论文和做答辩演示心里是有底的。希望帮到你。本文还有配套的精品资源点击获取