说实话,潜意识里觉得 Unity 和 MySQL 应该自动就能握手,实际做下来根本不是这样。我手里的基础环境是 Unity 2020 配 VS2017,目标是把账号注册、排行榜、玩家行为日志这些数据落到 MySQL 里。光是从“点下 Play”到“真正读到第一行记录”,中间就撞了好几堵墙:DLL 冲突、认证插件不支持、中文乱码、编辑器能连但真机又连不上。这篇实录我把整个过程完整重播一遍,包括版本匹配、连接串参数、报错对应的修复方法,照这个顺序走,半天内跑通基本没问题。
1. 这套组合到底解决什么问题
1.1 Unity 项目里什么时候才需要 MySQL
纯单机小游戏,用 PlayerPrefs 或者 JSON 存档完全够用,硬上数据库反而是给自己添乱。但一旦业务里面有跨设备数据、排行榜、账号系统、运营后台统计,本地文件就扛不住了。我以前在做的一个模拟经营类小项目,玩家数据除了本地缓存,后台还要留一份用于找回,另外每天要算一次排行榜,这个时候数据库就成了必需品。
选 MySQL 而不是 SQLite,原因也很简单:SQLite 本质是文件型数据库,多端同时写入会出锁问题,需要后台服务包一层才能暴露成接口,开发成本反而高。MySQL 本身就是独立的服务器进程,只要网络能通,客户端就能直接连上去读写。对 Unity 客户端里的开发阶段来说,这种直连模式最省事,调试也直观,动不动就能在 Navicat 里看到表结构变化。
VS2017 还在用,不是因为它新,而是老项目交接过来后一直关联的是这个版本,整个团队的工程规范也都在上面。Unity 2020 对外部脚本编辑器的兼容做得不错,VS2017 虽然老,但写 C# 7.3 语法的脚本没有任何问题,调试器也能正常挂上,没必要为了 IDE 升级去碰一整套工程配置。
1.2 Unity 连 MySQL 的底层链路
别把“在 Unity 里连数据库”当成一个黑箱功能,它的底层逻辑其实很常规:
C# 脚本要访问 MySQL,中间必须有一个驱动程序,官方提供的叫 Connector/NET,它把 C# 的方法调用封装成 MySQL 的客户端协议,再通过 TCP 网络发送给服务端进程。链路是这样的:
Unity C# 脚本 → MySql.Data.dll 驱动 → TCP 网络 → MySQL 服务端 → 查询结果原路返回这一步的难点不在驱动本身,而在 Unity 的运行时环境。Unity 脚本默认运行在 Mono 或 IL2CPP 环境下,它对 .NET 程序集的加载和标准控制台程序不一样,DLL 版本选错、程序集依赖缺失、API 兼容级别不匹配,都会直接导致运行时崩溃。这也是为什么网上教程很多,照抄却常常跑不通,环境不一样,坑自然不一样。
2. 动手前的版本匹配与环境准备
2.1 版本兼容性对照表
先看结论,我最终跑通的组合是:
| 组件 | 最终使用版本 | 说明 |
|---|---|---|
| Unity | 2020.3.x LTS | 脚本运行时固定为 .NET 4.x,不用改 |
| VS2017 | 15.9.x | 只做代码编辑和调试,负责编译的还是 Unity |
| MySQL Server | 8.0.x | 默认认证插件 caching_sha2_password |
| MySQL Connector/NET | 8.0.x | 必须在 8.0 及以上,否则认证不通过 |
| MySql.Data.dll | 随 Connector/NET 提供 | 按 API 兼容级别选择对应文件夹里的版本 |
这个表里最值得划重点的是 Connector/NET 版本。MySQL 8.0 把默认的密码认证方式从 mysql_native_password 换成了 caching_sha2_password,旧版 Connector/NET 6.x 根本不认识这个认证插件,一连接就会报错。要么升级驱动到 8.x,要么把账号改成老认证方式,我建议直接用新版驱动,因为后续 MySQL 版本只会越来越往新认证方式迁移。
2.2 MySQL 服务端账号与远程访问配置
服务端准备分两步。第一步是建库建表,第二步是建一个给 Unity 连接用的账号。
建库的时候直接把字符集定成 utf8mb4,不然后期中文数据进来就是一片问号:
CREATE DATABASE IF NOT EXISTS game_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE TABLE player_score ( id INT AUTO_INCREMENT PRIMARY KEY, nickname VARCHAR(50) NOT NULL, score INT NOT NULL, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;账号权限不建议用 root 直接连,太危险。单独建一个最小权限账号,只给这一个库的权限:
CREATE USER 'unity_user'@'localhost' IDENTIFIED BY 'yourStrongPass'; GRANT ALL PRIVILEGES ON game_db.* TO 'unity_user'@'localhost'; FLUSH PRIVILEGES;如果只是电脑上调试,localhost就够了。如果之后要在安卓真机或者另一台设备上连,需要把 host 改成百分号通配,并检查 MySQL 的 bind-address 配置:
CREATE USER 'unity_user'@'%' IDENTIFIED BY 'yourStrongPass'; GRANT ALL PRIVILEGES ON game_db.* TO 'unity_user'@'%'; FLUSH PRIVILEGES;另外还要在 MySQL 配置文件 my.ini 里确认bind-address=0.0.0.0,让服务端监听所有网卡,否则外部设备根本访问不到。开发机上如果开了防火墙,顺手把 3306 端口的入站规则放行,这一步不做,后面真机连不上排查半天都找不到原因。
2.3 Unity 工程里放哪些 DLL
安装完 Connector/NET 8.0.x 之后,在你的 Unity 工程里新建一个Assets/Plugins/MySQL文件夹,到 Connector 的安装目录下复制 DLL 文件进去。
Connector/NET 8.x 在 Windows 默认会安装在 “Program Files (x86)” 下,其中Assemblies目录里会有netstandard2.0、net45、net48等子文件夹,怎么选取决于你在 Unity 的 Player Settings 里设置的 Api Compatibility Level:
- API 级别选
.NET Standard 2.0→ 从netstandard2.0目录拿 DLL - API 级别选
.NET Framework→ 从net45或net48目录拿 DLL
通常至少需要两个文件:MySql.Data.dll和Google.Protobuf.dll。后面这个容易被漏掉,MySQL 8.0 新版认证协议在握手阶段要经过 protobuf 序列化,缺少它会抛 FileNotFoundException。如果拿不准还要哪些,直接把Assemblies对应目录下的所有 DLL 整包复制进工程,让编译器自己解析依赖,这是最省事的办法。注意别把多个版本的 MySql.Data.dll 混在一起放,Unity 对同名程序集只认第一个加载的,混版本会引发各种诡异问题。
还有一点,要在 Unity 里给这个 Plugins 子目录选中文件的平台设置。通常 MySql.Data 不需要专门屏蔽平台,默认全平台勾选就行。如果你只做 Windows 编辑器调试,也不用特别改。真机上要跑的话,这里反而要仔细一点,后面章节会展开。
2.4 Player Settings 里的两个关键开关
在动手写代码之前,打开 Unity 的 Edit → Project Settings → Player,找到 Other Settings 里的 Configuration,有两项必须确认。
第一项,Scripting Runtime Version。Unity 2020 里默认已经是 .NET 4.x Equivalent,这个不用动。如果是从老项目升上来的,看到版本是 .NET 3.5 就要改过来,否则驱动加载时直接报“程序集不受支持”。
第二项,Api Compatibility Level。推荐直接选.NET Standard 2.0,然后配合netstandard2.0目录下的驱动 DLL。这个组合在 Unity 生态里兼容性最稳,既不会出现 net45 目录里某些 API 无法解析的情况,也避免了 .NET Framework 模式下对系统数据库组件误引用导致的问题。
做完这些配置,重新启动一次 Unity,让它重新编译一次全部程序集。很多时候 DLL 没被正确加载,只是因为编辑器进程还锁着旧程序集,重启能解决不少玄学问题。
3. 从零写一个能跑通的连接脚本
3.1 最小连接测试脚本
环境都齐了,代码就是最简单的连接 + 读表。新建一个 C# 脚本,写在某个启动物体上:
using UnityEngine; using MySql.Data.MySqlClient; using System; public class MysqlTest : MonoBehaviour { private string connStr = "Server=127.0.0.1;Port=3306;Database=game_db;" + "User ID=unity_user;Password=yourStrongPass;" + "Charset=utf8mb4;SslMode=None;Connection Timeout=5;"; private void Start() { TestQuery(); } private void TestQuery() { using (MySqlConnection conn = new MySqlConnection(connStr)) { try { conn.Open(); Debug.Log("连接成功:已打开 MySQL 会话"); string sql = "SELECT id, nickname, score FROM player_score ORDER BY score DESC LIMIT 10"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) using (MySqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { Debug.Log($"玩家:{reader["nickname"]},分数:{reader["score"]}"); } } } catch (Exception ex) { Debug.LogError("数据库异常:" + ex.Message); } } } }注意代码里三件事。
第一,所有数据库对象都用了 using 包装,因为 MySqlConnection、MySqlCommand、MySqlDataReader 都实现了 IDisposable,离开作用域自动释放连接,避免长时间占用连接池。
第二,加了一个Connection Timeout=5,这样万一服务器没起来,最多等 5 秒就报错,不会让 Unity 编辑器卡死在那里。
第三,捕获异常时把 ex.Message 完整打出来。第一次调试的时候,你绝对不想只看一个“连接失败”四个字然后自己去猜原因。
3.2 连接字符串里的参数到底怎么选
连接字符串这块值得单独讲,因为里面每个参数都有讲究。
Server=127.0.0.1优先使用 IP 而不是 localhost,原因是 localhost 在某些系统里会解析成 IPv6 的 ::1,如果 MySQL 只监听了 IPv4,会出现连接明明配好了却报“无法连接到服务器”。直接用 127.0.0.1,干脆利落。
SslMode=None是开发阶段的妥协。MySQL 8.0 默认开启 SSL 要求,如果不加这个参数,连接时驱动会尝试协商加密通道,而本地开发环境的 MySQL 可能没配证书,导致握手失败。发布线上环境时不要抄这个设置,那时候应该用 VerifyCA 或者至少 Required。
Charset=utf8mb4这个参数要和数据库表字符集保持一致。只设一边经常会出现代码里存的是中文、库里查出来是乱码的情况。驱动层面的字符集和表层面的字符集双对齐,才能确保中文数据从入库到读取全链路不损坏。
3.3 插入数据时一定要用参数化
除了查询,还免不了要写入数据。插入语句里最容易犯的错误是字符串拼接。把变量直接拼到 SQL 里,看起来挺顺手,但实际上既挡不住引号导致的语法错误,也挡不住 SQL 注入。正确写法是用参数化:
private void InsertPlayer(string nickname, int score) { const string sql = "INSERT INTO player_score(nickname, score) VALUES(@nick, @score)"; using (MySqlConnection conn = new MySqlConnection(connStr)) using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@nick", nickname); cmd.Parameters.AddWithValue("@score", score); conn.Open(); cmd.ExecuteNonQuery(); } }这里用的是 AddWithValue,简单直接。但有个细节:如果表中 score 字段是 INT,而外面传进来的是一个字符串数字,AddWithValue 可能推断成 VARCHAR 类型,部分 MySQL 版本会报类型转换错误。遇到这个情况就把 AddWithValue 换成 Add,指定 SqlDbType:
cmd.Parameters.Add("@score", MySqlDbType.Int32).Value = score;这种写死类型的做法更稳妥,尤其在处理日期、布尔、大数值字段时,能避免很多隐式转换翻车。
3.4 别在 Update 里同步访问数据库
很多人写完连接代码后会顺手想在 Update 里每秒刷一次排行榜数据,头一次写出来大多数是能跑的,但帧率和数据库请求冲突的问题马上就来。同步访问数据库会阻塞主线程,你那儿数据库查询只要多跑几十毫秒,画面就明显卡顿,典型的表现是排行榜弹窗打开时整个场景卡一下。
正确做法是使用异步查询或者把数据库操作放到独立线程里。Unity 2020 里可以用 System.Threading.Tasks 跑异步查询,但要在主线程上更新 UI 的话,需要把结果存下来,然后在 Update 里取出来应用。另一种方案是使用官方驱动提供的异步接口OpenAsync、ExecuteReaderAsync,它们本质是线程池里跑的,不阻塞主线程。写起来也没多复杂:
await conn.OpenAsync(); using (var cmd = new MySqlCommand(sql, conn)) using (var reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { // 收集结果 } }只要记住一点,异步方法里涉及的 UI 更新,必须回到主线程执行,否则 Unity 会卡死在安全检查上。
4. 报错排查实录:那些折腾人的坑怎么填
4.1 FileNotFoundException 找不到 MySql.Data
这个报错特别容易吓到新手,常见场景是自己明明把 DLL 放进 Plugins 了,Unity 里也看得见文件,运行起来还是报“System.IO.FileNotFoundException: MySql.Data.dll”。
最常见的原因是文件放完以后没重启 Unity,编辑器进程锁定了旧程序集,新文件根本没被加载。解决方法很简单:关掉 Unity 工程,重新打开,让资源数据库重新解析一遍。
另一个原因是 DLL 放置路径不对。Unity 只会把 Assets 目录下的程序集打进 AppDomain,放错地方编译器根本不鸟你。放进 Assets/Plugins 或它的子目录才是正确姿势,别直接丢在硬盘某处引用。
还有一种情况比较隐蔽,你拖进来的是 netcore 版本的 DLL,当前 Api Compatibility Level 却是 .NET Standard 2.0 或者 .NET Framework,两者不匹配,Unity 编译期报错明显但运行时异常却用找不到程序集来掩饰。检查 Plugins 目录里 DLL 的文件大小,如果小于几十 KB,很可能就是空壳版本引用。
4.2 Authentication plugin cannot be loaded
这个报错是 MySQL 8.0 用户绕不过去的坎。完整错误一般长这样:
Authentication method 'caching_sha2_password' failed Authentication plugin 'caching_sha2_password' cannot be loaded看到这个先别慌,原因只有一个:驱动版本无法识别 MySQL 8.0 的默认认证插件。你有两条修复路线。
路线一,升级驱动到 Connector/NET 8.0 及以上,同时带上 Google.Protobuf.dll。这是治本的办法,因为 MySQL 后续版本会逐步移除老认证方式,你不能永远停留在旧版。
路线二,给账号降级认证方式,让老驱动也能连:
ALTER USER 'unity_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourStrongPass'; FLUSH PRIVILEGES;这条路适合只想赶紧在本地跑通验证逻辑的临时方案。注意如果 MySQL 版本是 8.0.34 及以上,mysql_native_password 插件可能默认被禁用,老驱动就算账号改了也未必能协商成功,建议还是走升级路线。
4.3 Host ... is not allowed to connect 连接被拒绝
看见这个错误,说明网络上其实能到达服务器,只是服务器端的账号 host 权限把你挡住了。MySQL 的 user 表里存着每个账号允许登录的来源 IP,创建账号的时候'unity_user'@'localhost'只允许本机连接。如果第一次建账号用了 localhost,后来想从局域网内另一台电脑连,就会报这个错。
修复方式就是把账号 host 改成匹配来源 IP 的范围。最省事的做法是重建一个'unity_user'@'%'账号,百分号匹配所有 IP。注意改完一定要执行FLUSH PRIVILEGES,否则权限缓存不生效。
4.4 中文存进去变成了乱码
乱码问题的根源只有三个字:字符集不一致。排查思路是一条线走到底:
从数据库客户端一路查过去,先看库的字符集:
SHOW CREATE DATABASE game_db; SHOW CREATE TABLE player_score;如果显示的不是 utf8mb4,先改库和表的结构。然后看连接字符串是否带了Charset=utf8mb4,最后看你要写进去的字符串自身在 Unity 里是否正常。排除了这三层之后,乱码基本能解决。
有个容易忽略的边角情况是,如果你在 MySQL 里手动插入过中文,锁定的编辑器客户端默认用的字符集是 gbk 或者 latin1,那库里的表数据本来就已经坏了,和你的 Unity 代码逻辑没关系。先清掉脏数据,再按统一字符集重新插入。
4.5 编辑器里连得上,打包到真机就失效
这是我个人印象最深的一个坑。编辑器里一切正常,构建 Android 包装到手机上,应用启动直接崩溃或报连接超时。一层层查下来通常是三个原因叠加。
第一个是本机防火墙只允许了开发机 IP 访问 3306 端口,手机到服务器的链路直接断掉。在服务器防火墙里确认放行的 IP 范围是否包含手机当前所在网段。
第二个是 MySQL 账号的 host 限制。开发时用的'unity_user'@'localhost'只认本机,手机是外部 IP,必须用%通配账号。
第三个是权限问题。Unity 打包 Android 时默认不申请网络权限,需要到 Player Settings → Android → Internet Access 改成 Require。iOS 上则要注意传输安全设置,如果服务端没有 https 证书,可能还要在 Info.plist 里配置允许明文传输。
真机调试这个东西,不存在灵异事件,所有的“编辑器能行真机就废”背后都有一行配置没做。
4.6 连接超时卡住整个 Unity
连接超时有两种体验:一种是快速报错,一种是编辑器整个卡住转圈。后者通常是因为同步访问数据库时网络包丢失后 TCP 重传超时过长,默认连接超时不时几秒,而是几十秒甚至一分钟。解决方式也简单,连接字符串里显式加Connection Timeout=5,让失败快速暴露,别让编辑器跟着等死。
如果是移动端弱网环境,还可以考虑在代码层面先做连接池预热,程序启动后在后台线程提前建立一个连接,既能减少首次查询延迟,也能避免用户点击排行榜时现场创建连接造成的卡顿。
5. 实测跑通记录与代码组织建议
5.1 整套配置的最终跑通记录
按上面的步骤走完,我工程里的最终状态是:
- Unity 2020.3,Api Compatibility Level 为 .NET Standard 2.0
- MySQL Server 8.0,连接账号用
'unity_user'@'%' - Assets/Plugins/MySQL 目录下放置 Connector/NET 8.0 的 netstandard2.0 版本 DLL
- 连接串使用 IP + utf8mb4 + SslMode=None + 5 秒超时
跑起来后日志依次输出连接成功、查询记录列表,测试插入数据后再查一次,能看到新增行正常出现。整个过程流畅,没有明显卡顿。这个组合在 Windows 桌面编辑器和打包后的 Windows 独立版本上都验证通过,完全可用于项目开发阶段的数据库读写操作。
5.2 别把数据库操作散落在各个业务脚本里
连接串写死了,代码能跑了,但直接写 SQL 的姿势不能一直保持下去。项目稍微起来一点,到处都是 new MySqlConnection 的话,发布前换服务器地址时你会想把键盘吃了。
最小可维护的做法是做一个独立的 DatabaseManager 单例,集中管理连接字符串和常用的查询方法。界面层只调方法,不碰任何 MySql 相关类型。这样以后换数据库、改连接参数、升级驱动,都只需要动一个文件。
我在这个项目里实际做的是建了一个 GameDataService 类,对外暴露三个方法:
public class GameDataService { private static string connStr = "连接字符串统一放这里"; public List<PlayerScore> GetTopScores(int count) { } public void SavePlayerScore(string nickname, int score) { } public PlayerProfile GetProfile(string userId) { } }UI 层只依赖 List<PlayerScore>、PlayerProfile 这些纯数据类,完全不感知底层驱动。等以后项目大了,再在这个服务类后面加 Redis 缓存或者改走 HTTP 接口,都不用动上层代码。
5.3 后期扩展还可以再做的几件事
等基础直连跑顺了,有几个方向值得继续完善。
一是给 MySQL 服务端做主从复制或者定期备份,开发库自己随便祸害,线上数据可不能裸奔。二是设置连接池参数,把 Pooling 打开、最大连接数调得合理一点,避免大量请求同时涌入时打垮服务器。
三是把敏感信息从连接字符串里拆出来。现在的写法里密码直接明文躺在代码里,仅仅用于开发可以容忍,一旦要发版或者提交到协作仓库,务必要把连接串挪到加密配置文件或者环境变量里,这钱真不能省。
还有,Unity 版本和数据库驱动都在快速迭代,每隔一段时间应该重新评估一次当前运行组合,别让项目长期绑死在老版本上。我踩过的这些版本坑,本质上都是因为版本之间没有对齐,提前做版本规划永远比事后修报错省时间。
最后分享一个小习惯:每次改完配置或者写新的数据库方法,都先跑一遍最小查询用例,确认连接串和驱动正常工作,再往上叠加业务逻辑。这个习惯帮我省了大量“找了一圈才发现是环境挂了”的时间。头几次配置的时候老老实实按步骤走,跑通一次之后,整个链路在你的工程里就像一个常用工具,越用越顺手。