尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity餐厅经营游戏毕业设计:C#与SQLite数据库实战避坑指南

Unity餐厅经营游戏毕业设计:C#与SQLite数据库实战避坑指南 简介一套围绕Unity引擎打造的C#餐厅经营游戏本科毕业设计提供完整的联机游戏开发方案。项目采用TCP/IP协议完成客户端与服务端通信使用MySQL存储游戏数据并通过Blender进行人物与场景建模整体拆分为服务端、客户端与共享工程三个模块服务端负责数据库管理和数据计算转发客户端负责根据服务端信息驱动游戏表现共享工程则封装双方通用方法与变量。资源包压缩后大小为两百二十四兆共包含九百二十二个文件其中以C#脚本、动态链接库、三维模型、预制体、材质、场景资产及开发配置为主要构成meta文件用于记录Unity资源导入属性cs文件承载核心逻辑dll文件引入第三方功能库fbx与prefab完成模型搭建。除项目源码外还提供数据库SQL文件、毕业设计文档、演示视频与答辩PPT可直接支撑课设展示或二次开发。目前已有二百七十五人学习适合需要完整网络游戏项目参考的本科生及Unity进阶开发者。1. 餐厅经营游戏毕业设计C#与Unity之外数据库和演示视频才是答辩的命门每年毕业季都能看到一批做 Unity 游戏毕设的同学3D 建模做了几个月玩法看起来也完整最后卡在存档和答辩演示上。C# 本科毕业设计基于 Unity 的餐厅经营游戏这个题目听起来简单无非点单、做菜、收钱但要把源码、数据库和演示视频串成一套能站稳答辩台的项目多数工作其实在看不见的地方顾客状态怎么流转、数据存哪张表、重开场景为什么钱没了、演示视频怎么一镜到底讲清楚设计。这篇文章不谈某一个现成源码包怎么解压而是把“餐厅经营游戏设计与开发”这条路上必须自己搞定的几个关键系统拆开讲先定数据库表再写 C# 实体和状态机最后接 SQLite 存档顺带交代录制演示视频和应对提问的方法。适合打算在 6 到 8 周内独立完成一个游戏类毕设、并且希望答辩时不被“你的数据存在哪、怎么防止丢档”这类问题问倒的同学。2. 先设计数据库再写C#脚本餐厅经营的四张表与实体类映射2.1 最小可行玩法清单哪些系统必须做哪些可以砍掉餐厅经营游戏能做到“看着像个游戏”的最小闭环包括顾客生成→等位→点单→后厨制作→上菜→结账→离店同时经营侧要有金币收入、满意度、菜品解锁。围绕这个闭环必须做的前端模块只有四个顾客 AI状态机、菜品数据、订单数据、存档读档。地图上那些装饰性的桌椅、顾客动画、音效都是加分项不在核心列表里。很多同学犯的第一个错误是把时间花在做 3D 餐厅模型和动画上等装好桌椅再回头写数据层工期已经不够。另一个常见错误是反过来的写了一堆抽象接口、管理器、单例结果最简单的一单交易流程都跑不通。我的建议是这个量级用“场景单例 静态事件 状态机”三层结构就够了不要上依赖注入框架不要动不动 MVVM。毕设评审看的是系统是否完整、数据是否闭合不是架构设计大赛。模块的依赖顺序也建议固定先画数据库表再写实体类再写顾客状态机最后接 UI。这是因为餐厅经营的核心玩法就是数据流在顾客、订单、结算三个环节里转数据表没定下来C# 脚本怎么写都会返工。设计稿在这个项目里不是必须的一张订单状态流转图用手画在草稿纸上就够。2.2 菜品、订单、存档、配置用SQLite建表的DDL与字段解释餐厅经营需要持久化的数据主要是四类菜品本身、每单交易记录、游戏整体进度、全局设置项。很多毕设会试图用一张大表存所有东西比如把餐厅等级、当前金币、菜品列表全部塞进一个字段这是最容易被答辩老师追问的设计。拆成下面四张表每张表职责单一增删改查也更好演示。CREATE TABLE IF NOT EXISTS food ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL DEFAULT 10.0, cost REAL NOT NULL DEFAULT 5.0, cook_time REAL NOT NULL DEFAULT 3.0, unlock_level INTEGER NOT NULL DEFAULT 1, on_menu INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS order_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, food_id INTEGER NOT NULL, food_name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, total_price REAL NOT NULL DEFAULT 0.0, satisfaction REAL NOT NULL DEFAULT 5.0, table_id INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now,localtime)), finished_at TEXT, FOREIGN KEY (food_id) REFERENCES food(id) ); CREATE TABLE IF NOT EXISTS save_data ( id INTEGER PRIMARY KEY CHECK (id 1), money REAL NOT NULL DEFAULT 200.0, rep_score REAL NOT NULL DEFAULT 0.0, unlocked_level INTEGER NOT NULL DEFAULT 1, state_json TEXT, updated_at TEXT NOT NULL DEFAULT (datetime(now,localtime)) ); CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT NOT NULL );订单表里专门冗余了一列food_name原因是订单是流水菜品价格可能调整如果订单只存food_id以后改菜价就会把历史账单也改了流水就不再真实。total_price在写入时直接算好查询时不需要再 JOIN 菜品表对毕业设计这个量级完全够用。save_data表用CHECK (id 1)锁死单行避免多次初始化产生多条存档state_json列用来放整个游戏运行时的序列化状态第四章会专门写。SQLite 默认外键约束是关闭的每次建表后要执行一次PRAGMA foreign_keys ON;否则FOREIGN KEY形同虚设。这条在代码里必须写在连接打开之后、增删改查之前。另外datetime(now,localtime)是 SQLite 的本地时间写法如果只写datetime(now)拿到的会是 UTC 时间国内使用时订单流水时间会差 8 个小时演示订单列表时很容易被老师看出来。2.3 C#实体类与表结构对齐下划线命名的三个边界坑SQLite 字段名习惯用snake_caseC# 字段习惯用PascalCase两者之间如果不做映射代码读起来非常别扭。最简单的做法是手写映射因为项目很小不需要上 ORM。实体类直接对应表结构一个类一张表[System.Serializable] public class FoodItem { public long id; public string name; public float price; public float cost; public float cookTime; public int unlockLevel; public bool onMenu; } [System.Serializable] public class OrderRecord { public long id; public long foodId; public string foodName; public int quantity; public float totalPrice; public float satisfaction; public int tableId; public string createdAt; }这里有三个坑值得专门说。第一个坑是REAL不能用GetInt32读SQLite 里价格和时间都用 REAL 存储从SqliteDataReader取出时必须用Convert.ToSingle(reader[price])否则会抛FormatException或得到错误精度。第二个坑是布尔值SQLite 没有专门的 BOOLEAN 类型on_menu存的是 0 和 1实体类里写bool没问题但读写时要做reader.GetInt32(ordinal) 1的转换。第三个坑是字段对齐SELECT *读出来的列顺序依赖建表语句一旦某天调整了建表顺序所有按索引取值的代码都会静默出错所以读的时候一定要按列名取不要按索引取。实体类用[System.Serializable]标注还有一个额外好处后续用JsonUtility.ToJson做存档快照时系统可以直接序列化整个对象。注意JsonUtility不能直接序列化Dictionary如果进度数据里有“已解锁菜品ID→解锁时间”这类映射要先把字典转成 List 或字符串再放回state_json这是 Unity 序列化的老坑后面存档章节还会遇到。3. 顾客AI状态机的C#写法从进店排队到结账离店的状态流转3.1 五个状态的FSM骨架为什么用状态机而不是逐帧if判断餐厅经营的核心玩法循环都是围绕顾客的行为展开的顾客从生成到消失行为序列十分固定等位、点单、用餐、结账、离店。这种固定序列最适合用有限状态机表达。不用状态机而用一堆if (isWaiting) { ... } if (isOrdering) { ... }的写法刚开始看起来也能跑但每个系统都要修改顾客行为时就会互相打架比如顾客等待超时要走人这件事至少会牵扯到排队系统、点单系统和结算系统三个地方。用枚举驱动状态的 C# 写法很直接下面是一个可以放进场景里直接跑的骨架public enum CustomerState { Waiting, // 排队等空桌 Ordering, // 看菜单并下单 Dining, // 等菜、用餐 Paying, // 结账 Leaving // 离开并销毁 } public class CustomerFSM : MonoBehaviour { public CustomerState state CustomerState.Waiting; public int patience 100; private float stateTimer 0f; private OrderSystem orderSystem; private void Awake() { orderSystem FindObjectOfTypeOrderSystem(); } private void Update() { stateTimer Time.deltaTime; switch (state) { case CustomerState.Waiting: if (orderSystem.HasFreeTable()) { state CustomerState.Ordering; stateTimer 0f; } break; case CustomerState.Ordering: OrderSignal.Emit(this, orderSystem.CreateRandomOrder()); state CustomerState.Dining; break; case CustomerState.Dining: if (orderSystem.IsMealReady(this)) { state CustomerState.Paying; } break; case CustomerState.Paying: orderSystem.PayBill(this); state CustomerState.Leaving; break; case CustomerState.Leaving: Destroy(gameObject, 1.2f); break; } if (patience 0 state ! CustomerState.Leaving) { state CustomerState.Leaving; } } }这段代码里最关键的设计是OrderSignal.Emit这一行点单动作不是由顾客脚本直接调用 UI 或订单面板而是发一个静态事件出去谁关心谁监听。这样后面加“获得新订单音效”“订单列表刷新”“任务进度提醒”都只需要新增监听者不需要改动顾客脚本。整个状态机的流转只有一个入口和一种变更方式即Update里的switch要调试“顾客为什么卡在点单不动了”只需要看stateTimer和当前state两个字段。参数上patience是顾客总耐心值等待超时的判断建议放在OrderSystem里统一用Time.time - enterTime maxWaitTime算而不是每帧在顾客身上做减法因为前者更好调节实测里也不容易出现浮点累积误差。3.2 用事件驱动把游戏逻辑与UI解耦C#委托与事件的落地写法餐厅经营游戏最容易写乱的一段代码是把餐厅金币、顾客状态、订单列表全部集中在一个GameManager.Update里然后其他 UI 脚本每帧去GetComponent轮询。比如订单面板要每秒刷新一次订单数量金币文本要每秒重新读取一次两个人同时改一个类合并时冲突就能把人气到删库。C# 的委托和事件在这个量级的项目里是够用的不需要上 UniTask 或 Rx。声明一个静态事件类让逻辑层和表现层彻底分开public static class OrderSignal { public delegate void OrderHandler(CustomerFSM customer, OrderData order); public static event OrderHandler OnOrderPlaced; public static event OrderHandler OnOrderFinished; public static void Emit(CustomerFSM customer, OrderData order) { OnOrderPlaced?.Invoke(customer, order); } }事件的使用方是 UI 面板和音效管理器public class OrderUIPanel : MonoBehaviour { private void OnEnable() { OrderSignal.OnOrderPlaced HandleNewOrder; } private void OnDisable() { OrderSignal.OnOrderPlaced - HandleNewOrder; } private void HandleNewOrder(CustomerFSM customer, OrderData order) { // 刷新订单列表、显示新订单提示 } }这段代码里有两点必须遵守第一订阅事件必须在OnDisable里取消否则场景切换后组件被销毁但事件还握着它的引用轻则内存泄漏重则空引用报错第二事件回调里不要做耗时操作比如写数据库、读大文件因为Emit是同步调用所有监听者都会阻塞当前顾客帧帧率会肉眼可见地掉。如果确实需要落库在回调里只做标记真正写库放到帧末或顾客结账后的统一保存流程里。顺带一提很多参考代码喜欢用UnityEvent在 Inspector 里拖拽绑定这个对毕设来说反而少见因为答辩现场不会有人夸你 Inspector 接线接得好看但会有人问“如果 UI 没激活事件还会触发吗”这类问题。用纯 C# 事件回答起来最干净UI 是否激活不影响逻辑层运行事件只是通知UI 不订阅自然不刷新。3.3 翻台率、满意度、定价参数第一版数值怎么设数值设计得再完整参数拍脑袋都会让游戏变得不好玩。餐厅经营的第一版数值我建议从“顾客完整吃完一单要多久”开始反推。比如设定一单平均 40 秒其中等位 10 秒、点单 5 秒、烹饪 15 秒、用餐 10 秒那么顾客刷新间隔至少要大于 40 秒除以桌数。单桌场景每 45 秒生成一位顾客顾客永远在排队体验最差四桌场景每 12 秒生成一位刚好能形成连续客流。第一版参数可以直接照抄下面这张表后续再根据试玩感受微调参数第一版建议值调低的后果调高的后果顾客刷新间隔10~12 秒/批后厨积压满意度崩盘餐厅空转收入低单道菜烹饪时间3~8 秒游戏失去经营压力顾客等太久直接离店顾客等待超时20 秒顾客频繁流失队列过长节奏拖沓菜品定价/成本比1.5~2.0 倍赚钱太慢中期卡等级数值溢出解锁无意义满意度下降速度100 点/30 秒没人离店难度过低全员负评不可收拾满意度计算有一个比较容易漏掉的点离店顾客的满意度写进order_record但当前餐厅评分rep_score要用近期 20 单的平均值而不是历史全部订单平均值。否则前期吃了几单差评后期翻盘也很难把评分拉回来玩家会感觉系统不公平。这个逻辑写在结算函数里一行avg查询就能完成但答辩老师很吃这一套因为它展示了你理解“近期权重”这个概念哪怕你只用了一个简单的 LIMIT 子查询。4. Unity连接SQLite数据库读写封装、增删改查与存档实现4.1 为什么毕业设计选SQLite而不是MySQL部署与连接层面的取舍餐厅经营游戏如果让玩家存档最直接的方案是本地文件。MySQL 这类服务器数据库在这个项目里不是不能用但它要求答辩现场有一台能连的数据库服务器演示前还要先启动服务、建库、改连接字符串任何一个环节出问题现场演示就变成一场事故。SQLite 是文件型嵌入式数据库整个数据库就是一个.db文件跟随游戏存档走复制、备份、提交到毕设材料里都很方便。从数据量看餐厅经营的订单流水集中在一个学期内撑死几千条记录SQLite 单文件上限和并发性能都远超这个量级。从技术分看SQLite 支持标准 SQL 的增删改查、外键、事务、索引用来做毕业设计的“数据库”部分完全够格。而且在答辩时会有一个天然加分说法选择 SQLite 是因为它适合单机游戏本地持久化场景避免引入服务器运维复杂度让玩家数据和游戏包一体这体现的是根据应用场景选技术的能力不是只会堆技术栈。Unity 接入 SQLite 的常见做法有两种一种是使用 Unity 自带的Mono.Data.Sqlite在 Package Manager 里启用 SQLite 模块另一种是引入第三方sqlite-net之类的库。对本科毕设而言前者最稳因为它是 Unity 官方模块DLL 和脚本后端兼容性不用自己维护。如果你在 Unity 2022.3 LTS 上用Mono.Data.Sqlite编译报错多数情况是工程没有启用对应模块去 Project Settings 的 Player Settings 里把 Scripting Backend 从 IL2CPP 临时切到 Mono 也可以绕开一部分兼容问题但 Android 打包还是建议按第五章的方法处理原生库。4.2 SQLiteHelper的完整封装连接字符串、建表、查询与写入接入 SQLite 的第一步是把连接和公共读写收口到一个静态类里避免每个脚本都自己new SqliteConnection否则连接没释放、数据库被占用这类问题会在后期集中爆炸。下面是最小可用的封装using Mono.Data.Sqlite; using System.Collections.Generic; using System.IO; public static class SQLiteHelper { private static string dbPath Path.Combine(Application.persistentDataPath, restaurant.db); public static SqliteConnection Open() { var conn new SqliteConnection(Data Source dbPath ;Version3;); conn.Open(); using (var cmd conn.CreateCommand()) { cmd.CommandText PRAGMA foreign_keys ON;; cmd.ExecuteNonQuery(); } return conn; } public static void Execute(string sql, params SqliteParameter[] parameters) { using (var conn Open()) using (var cmd conn.CreateCommand()) { cmd.CommandText sql; if (parameters ! null) cmd.Parameters.AddRange(parameters); cmd.ExecuteNonQuery(); } } public static ListT QueryT(string sql, System.FuncSqliteDataReader, T mapper) { var list new ListT(); using (var conn Open()) using (var cmd conn.CreateCommand()) { cmd.CommandText sql; using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(mapper(reader)); } } } return list; } }dbPath用Application.persistentDataPath是刻意的在编辑器和 Windows 打包下这个路径指向用户目录可写在 Android 和 iOS 上它指向沙盒存储可写而Application.dataPath在打包后是只读的。数据库文件如果放在 StreamingAssets 或工程目录里打包后只能读不能写直接导致存档失败这一点是本章和第五章避坑内容的共同重点。增删改查的四个操作都走Execute和Query两个方法。例如新增菜品SQLiteHelper.Execute( INSERT INTO food (name, price, cost, cook_time, unlock_level) VALUES (n, p, c, t, l);, new SqliteParameter(n, 招牌汉堡), new SqliteParameter(p, 25f), new SqliteParameter(c, 12f), new SqliteParameter(t, 4f), new SqliteParameter(l, 1) );用参数而不是字符串拼接 SQL原因只有一个订单数据里的菜品名是玩家输入或配置导入的直接用用户输入拼 SQL 很容易把一个“”变成语法错误甚至被反问 SQL 注入。虽然是单机游戏但答辩老师看到SqliteParameter会直接加分。查询时用Query加一个 mapper 委托比如查最近 20 单满意度平均值var avg SQLiteHelper.Query( SELECT AVG(satisfaction) FROM order_record ORDER BY id DESC LIMIT 20;, r System.Convert.ToSingle(r[0]) );注意这里的LIMIT 20语义是“最近的 20 行”因为id是自增主键大 id 就是晚发生的订单如果前面表设计时没有自增 id而是用时间字符串排序就要小心时区问题这也正是 2.2 里用AUTOINCREMENT的另一个理由。4.3 存档与读档把整个游戏状态序列化为一行JSON餐厅经营的存档点有三个退出游戏时、切后台时、每完成一单结算时。前两个是防意外第三个是防玩家强杀进程。很多同学只在退出时写一次库Windows 上表现良好一到 Android 上手机只要一锁屏进程被杀再打开就回到上一关存档投诉立刻就来。存档的最小做法是把游戏运行时的所有可变状态收敛到一个GameState对象用 Unity 自带的JsonUtility序列化成字符串再写进save_data表那一行。实体类写法如下[System.Serializable] public class GameState { public float money; public float repScore; public int unlockedLevel; public ListFoodItem foods; public string updatedAt; }保存和读取分别是两个对称的函数public static void SaveGame(GameState state) { state.updatedAt System.DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss); string json JsonUtility.ToJson(state); SQLiteHelper.Execute( UPDATE save_data SET moneym, rep_scorer, unlocked_levell, state_jsonj, updated_atdatetime(now,localtime) WHERE id1;, new SqliteParameter(m, state.money), new SqliteParameter(r, state.repScore), new SqliteParameter(l, state.unlockedLevel), new SqliteParameter(j, json) ); } public static GameState LoadGame() { var rows SQLiteHelper.Query( SELECT state_json FROM save_data WHERE id1;, r r.GetString(0) ); if (rows.Count 0) { return CreateDefaultState(); // 首次启动插入初始存档 } return JsonUtility.FromJsonGameState(rows[0]); }把整个状态塞进一个 JSON 字段的优点是极其省事缺点是不能在 SQL 里按某个子字段做条件查询比如“查金币大于多少的存档”做不到。但对单机餐厅游戏来说没有任何功能需要这么做所以这是一个合理的取舍。使用JsonUtility时同样要注意 2.3 提到的限制不要让它序列化Dictionary、DateTime、自动属性ListT和普通字段是最稳的组合。如果你发现自己把public DateTime saveTime { get; set; }写进存档类序列化出来会是一串空值这是 Unity 序列化的经典陷阱。数据库初始化也要善用CREATE TABLE IF NOT EXISTS在Awake或场景启动时统一调用一次建表脚本避免不同机器上首次运行时因为缺表直接抛“no such table”。同时给save_data插入一行初始数据的时机要提前到建表之后、主菜单加载之前否则主菜单显示的金币会取到一个空结果集。我一般会用“启动时检查行数为 0 就插入初始行”的方式处理比直接写一条 INSERT 更安全因为不会重复插入。5. 避坑与排查UnitySQLite从编辑器到答辩演示的5个翻车现场5.1 水印编辑器或打包画面上出现 Unity Personal / Trial 水印现象游戏跑起来以后画面角落会叠一个小小的“Unity Personal”或“Trial version”水印平时开发不显眼录进演示视频里非常掉价。有些同学在网上下载的 Unity 版本本身带试用标记或者安装时没有激活许可证水印就会一直存在。原因Unity 许可证未激活或激活的是免费 Personal 试用版且项目规模属于商业使用范围之外的试用授权。这不是引擎缺陷是授权状态问题。解决打开 Unity Hub登录自己的 Unity 账号在 Preferences 里激活免费的 Personal License学校机房可以请管理员统一批量激活。激活完成后重启编辑器检查菜单栏 Help → Manage License 是否显示正确的过期时间。录制演示视频前再花十秒看一眼画面四角比事后剪辑时发现水印再返工省事得多。别等到答辩前一天才录制那时候发现水印整个视频素材都要重录。5.2 数据库打不开编辑器正常Windows打包后报“unable to open database file”现象在编辑器里点 Play 一切正常打包成 exe 后一进存档界面就弹窗报错错误信息是SqliteException: unable to open database file。原因代码里用了Application.dataPath拼接数据库路径。编辑器运行时dataPath指向工程 Asset 目录可读写打包后dataPath指向只读的程序安装目录SQLite 想创建或写文件时直接被系统拒绝。解决统一改成Application.persistentDataPath这个路径在 Windows 上位于用户目录的 AppData 下可读可写。老项目里还残留dataPath的地方可以全局搜索替换改完以后再次打包验证。另外一个连带问题是如果希望首次启动能提供一张预设数据库比如预置菜品数据可以把restaurant.db放进 StreamingAssets首次启动时把它复制到persistentDataPath之后再读写都指向该副本。直接读 StreamingAssets 里的数据库是不可行的因为它是只读的。5.3 Android真机闪退DllNotFoundException: sqlite3现象Android 打包后安装到手机上进入存档模块立刻闪退Logcat 里能看到DllNotFoundException: sqlite3。原因Unity 自带Mono.Data.Sqlite在 Android 的 IL2CPP 后端下依赖原生sqlite3.so库默认构建没有把所有 ABI 的库都打进去或者脚本后端裁剪把托管层调用链裁掉了。这个问题在编辑器里测不出来只有真机才会触发。解决最稳的路线是把 Player Settings 的 Scripting Backend 保持 Mono虽然包体会大一些但对毕设级别项目完全可接受如果坚持用 IL2CPP需要确认 Plugins/Android 下放置了对应的sqlite3.so以及相关依赖并保证 ARM64 和 ARMv7 都有配套版本。这里务必在答辩前用真机测试不要在 Android 模拟器上测模拟器和真机的 ABI 与存储权限表现并不一致。一条血泪经验是这个坑只要踩到一次后面每换一台真机都要重测所以数据集和打包配置定稿后就不要再频繁切脚本后端。5.4 重开场景金币清零存档只写了内存没落库现象游戏内金币正常变化退出游戏再打开主菜单金币和等级全部回到初始值但订单表里明明有历史记录。原因金币和等级只存在运行时静态变量里订单记录却是实时写库的所以库里能看到订单却看不到进度。这是最常见的“数据不同步”事故本质是保存时机设计错误。解决把自动保存挂到三个时机上一是OnApplicationQuit二是OnApplicationPause三是每单结账后的结算回调。注意 Android 上OnApplicationQuit不一定会被可靠调用所以切后台时的OnApplicationPause更重要。同时SaveGame里的UPDATE语句是用WHERE id1更新的如果首次插入逻辑写得不对重复插入第二行会导致CHECK(id1)约束冲突报错信息会是UNIQUE constraint failed: save_data.id这时去查save_data表多半能看到两行初始数据删掉多余的一行再启动即可恢复。5.5 演示视频录制卡顿游戏和录屏同机抢资源现象用 OBS 或者 Windows 自带录屏录制游戏片段回放画面掉帧、鼠标轨迹跳变甚至音频和画面不同步。原因演示机器同时运行 Unity 游戏还在跑实时渲染和后处理和录屏软件CPU/GPU 都被吃满游戏如果开着垂直同步帧率会被锁在 30 或 60但录屏端又在不断请求新帧两方互相争抢画面自然不稳。解决录制前把项目 Quality 设置里阴影、抗锯齿、后处理全部降到 Low 或 Off再在游戏里用Application.targetFrameRate 30;固定帧率这样录屏软件拿到的帧间隔是稳定均匀的。OBS 的编码器选硬件编码输出分辨率保持 1920×1080不要开到 2K 或 1440p游戏画面和录制画面都用 30 帧。这段录像如果只是答辩演示用1080p30 的码率足以看清所有操作细节不必追求 4K60不然录出来的文件巨大上传到毕设材料里或答辩现场播放还容易卡。6. 演示视频与答辩提问用一镜到底讲清楚设计而不是操作演示视频是很多毕设最后才想起的东西但它恰恰是答辩老师最先看的东西。一个六分钟的演示视频结构比画面重要得多。我惯用的脚本模板是前 30 秒展示游戏主界面和数据库表结构中间四分钟按“顾客进店→点单→后厨烹饪→结账→金币变化→退出重进读档”的完整流程走一遍最后 30 秒打开数据库工具对着order_record表展示刚才那几单确实写进了数据库。最好中途不要剪辑一镜到底这样老师不会怀疑你有“作弊镜头”。时间画面解说要点0:00-0:30主菜单、进入游戏说明项目基于Unity与C#展示场景结构0:30-2:30顾客排队、点单引出状态机和事件系统一笔带过2:30-4:00后厨制作、结账讲解金币结算、满意度写入4:00-4:30退出并重进游戏证明存档读档闭环4:30-5:30用DB Browser打开SQLite展示新增订单记录与state_json解说词的要点是讲设计决策不是报菜名。比如点单演示别只说“顾客点了一个汉堡”要说“顾客从等待状态切换到点单状态时通过OrderSignal事件通知订单面板UI不需要轮询顾客状态”。这样一句话老师就拿到了评分表上“系统设计”那一栏的证据。答辩时最容易翻车的三个提问也提前准备一下第一个是“为什么用SQLite不用MySQL”按 4.1 的选型逻辑回答强调单机游戏、本地数据、降低部署复杂度这是合理的工程取舍不是技术落后第二个是“状态机卡死了怎么办”拆开回答等待超时兜底逻辑和每帧只有一个状态更新入口说明你有防止状态失控的手段第三个是“数据库损坏或存档丢了怎么办”回答里带一句“写入采用单连接、即时关闭且同一行做 UPDATE 覆盖不会频繁产生碎片”已经足够不需要真做一套备份恢复机制。我自己带毕设这些年给所有人定过一条规矩演示视频先录录完再写说明文档因为视频里暴露的问题永远比文档里自夸的内容多。把视频当作另一个评审人它会替学生提前发现掉帧卡顿、数据库路径错误、水印这些细节。这道工序做完答辩现场才能把时间省下来回答真正体现设计深度的问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表