做 Unity 项目的时候突然要接数据库,第一反应多半是头大。当年我在某个截单工具类项目里遇到同样的需求——Unity 里展示后台记录、保存玩家操作日志、按条件筛选数据,这些都是典型的数据持久化场景。而选择“Unity 2020 + VS2017 连 MySQL”这套组合,当时也是对比了好几轮才定的。
先说结论:这方案完全可行,但坑也不少。最典型的坑就是 MySQL 数据驱动 DLL 的版本兼容问题,其次是 Unity 的 API Compatibility Level 配置,再就是连接字符串踩出一堆连接失败。这篇文章从头到尾把我实际跑通过的路走一遍,从环境选型、DLL 引入、连接配置,到完整脚本和常见问题排查,全程按照我踩坑之后的正确顺序来讲,保证是可复现的实操流程。
如果你正在做 Unity 客户端、需要直连 MySQL 做工具类应用或内部管理端,或者只是一个想搞明白“Unity 能不能连数据库”的新手,这篇能帮你少走至少一个星期的弯路。
1. 整体方案设计与选型逻辑
1.1 为什么是 Unity 2020 + VS2017
Unity 2020 是 LTS 版本里的分水岭。它不再像 2018 那样保留了大量老旧的 .NET 3.5 兼容层,默认建议的 API 兼容级别是 .NET Standard 2.0 和 .NET 4.x,这意味着可以使用更多现代 C# 语法和框架类型。而 VS2017 虽然老,但只要补丁打到 15.9 以上,配合 Unity 2020 开发时,无论是 MonoBehaviour 脚本模板、断点调试还是 Unity 类库智能提示,都正常流畅。换句话说,这对组合不需要额外装 VS2019 或 VS2022 也能开发调试。
有一点要明白:VS 在这里只是编写代码和调试 C# 脚本的工具,真正的运行时依赖来自 Unity 自带的 Mono 或 IL2CPP 运行时,以及你放进项目的 MySql.Data.dll。因为 Unity 的托管运行时会用一个固定的加载器识别程序集,所以版本兼容问题的核心在于:MySql.Data 这个程序集编译时针对的是哪个 .NET 版本,是否和你工程中设定的 API Compatibility Level 匹配。
我实际测试下来,最稳的组合是这样的:
- Unity 2020.3.x
- VS2017 15.9 及以上
- MySQL 5.7 或 8.0(本地库或远程库均可)
- MySQL Connector/NET 6.9.9 或 8.0.18
1.2 为什么选 MySQL 而不是 SQLite 或其它
先聊一个容易纠结的问题:Unity 本地持久化用 SQLite 不香吗?确实,如果是纯单机、数据量小、逻辑简单的场景,SQLite 很合适。但一旦出现多人共享同一份数据、后台需要同步分析、或者客户端崩溃后数据仍然要保留在服务端,那么 SQLite 就力不从心了。
MySQL 在这类场景里的定位更像一个独立的“数据仓库”:客户端通过 TCP 协议把 SQL 语句发到 MySQL 服务端,由服务端解析执行后返回结果。Unity 直连 MySQL 意味着可以在编辑器菜单里直接往库里搬数据,也可以在游戏内部做一个管理界面,甚至可以做存档同步的雏形。这种直连方案本身不复杂,复杂度主要来自连接稳定性和程序集兼容性。
1.3 技术栈全景
整套链路如下:
- Unity 游戏引擎:负责场景渲染、UI 展示、用户交互
- C# 脚本:通过 MySql.Data.dll 提供的 ADO.NET 接口发起数据库操作
- MySQL 服务端:接收 SQL 语句,执行数据读写
- VS2017:负责写代码、调试断点
数据流向很简单:Unity 脚本 -> MySqlConnection -> 网络 -> MySQL Server -> 返回结果集 -> Unity 处理并刷新 UI。
很多教程只告诉你“放一个 DLL、写一段代码”,但这只解决了一个点。真正的工程化落地要把版本匹配、错误分类、异常兜底、异步优化都考虑到,否则只能算“偶尔能跑通”。
2. 环境准备与关键资源
2.1 基础软件安装
在你开始贴代码之前,请先检查下面几个环境项,缺任何一个都会在运行时暴雷。
第一,VS2017 必须升级到 15.9.x。只装初始版的 VS2017 缺少对 Unity 2020 的接口适配,打开脚本文件时可能不识别 Unity API,断点调试也会失效。升级很简单,去 Visual Studio Installer 中更新即可。
第二,Unity 2020.3 安装时建议勾选 Windows Build Support (IL2CPP) 和 .NET 4.x 支持。其实 Unity 2020 对 .NET 4.x 的支持默认就是开启的,但如果你在 Player Settings 中手动调低到 .NET Standard 2.0,部分 MySql.Data 的接口可能不可用。
第三,MySQL 服务端建议安装 5.7 或 8.0 版本。如果只是本地开发,用默认的 root 账号无非就是防火墙和密码加密规则两个坑,后面会讲。生产环境建议单独建账号。
2.2 下载 MySQL Connector/NET
这是最容易踩坑的环节。Connector/NET 分很多版本,Unity 并不能直接兼容所有版本。
我的建议是:
- 如果你选择 MySQL 8.0,下载 Connector/NET 8.0.18 或相近版本
- 如果你选择 MySQL 5.7,直接用 Connector/NET 6.9.9
为什么选 8.0.18?因为从 Connector/NET 8.0.x 开始,默认的认证协议变成了 caching_sha2_password,老版本的 MySql.Data.dll 根本不认识这个协议,一连接就报错。8.0.18 之后的一些版本虽然功能更多,但在 Unity 的 Mono 环境下偶发反射或加载问题。所以锁定 8.0.18 是最省心的。
下载时不要选 Installer,选 Zip 包,因为只需要里面的 DLL 文件,不需要全局安装。解压后找这几个关键文件:
- MySql.Data.dll
- 依赖的 Google.Protobuf.dll(8.0 版本需要)
- System.Buffers.dll 等(如果目标框架需要)
实际需要的依赖数量取决于你当前的 API Level,后面第 3 节会说明怎么判断。
2.3 准备一个测试数据库和表
打开命令行或图形工具,执行这一段 SQL,建一个后续测试用的库和表:
CREATE DATABASE IF NOT EXISTS unity_test CHARACTER SET utf8mb4; USE unity_test; CREATE TABLE IF NOT EXISTS player ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, score INT DEFAULT 0, login_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO player (name, score) VALUES ('A', 100); INSERT INTO player (name, score) VALUES ('B', 200);这一步的意义是用最小的数据量验证链路通不通。实际项目中表结构会更复杂,但测试阶段越简单越好。
3. 核心细节:DLL 引入与兼容性配置
3.1 DLL 必须放进 Assets/Plugins
很多人喜欢把 MySql.Data.dll 放在 Assets 根目录,或者放在某个子目录里。这在编辑器里有时能跑,但一旦打包或者做 iOS/Android 构建,就会出现找不到类型或者程序集加载失败。正确做法是放在Assets/Plugins下:
Assets/ └── Plugins/ ├── MySql.Data.dll ├── Google.Protobuf.dll └── System.Buffers.dllPlugins 目录是 Unity 的固定查找目录,程序集在打包时会按平台自动处理。如果你的项目未来还要打包到 Android,注意在 Plugins 里给 Android 平台设置独立的 DLL 导入设置,否则默认可能被排除。
顺手提醒一下:不要直接把整个 MySQL Connector 目录拷贝进来,里面包含大量无关 DLL 和 XML 注释文件,会增加程序集加载的反射开销。只保留运行期必须的那几个。
3.2 Player Settings 的 API Compatibility Level
这个配置位于Edit -> Project Settings -> Player -> Other Settings -> Configuration -> Api Compatibility Level。
Unity 2020 提供两个主要选项:
- .NET Standard 2.0
- .NET 4.x
如果你选 .NET Standard 2.0,那么 MySql.Data 8.0.x 大概率能跑,但某些 API(比如完整 DataSet 支持)会被削弱。我实际测试下来,用 .NET 4.x 最省事,因为 Connector/NET 针对 .NET Framework 编译的核心路径在 Mono 下兼容性最好。
3.3 为什么版本匹配这么敏感
我们打个比方:MySql.Data.dll 相当于一份“翻译手册”,Unity 里的 C# 脚本要靠它翻译成 MySQL 听得懂的 TCP 数据包。如果翻译手册里用了 Unity 不认识的语法(比如更高版本的 .NET API),Unity 加载时就罢工,表现就是找不到命名空间、类型加载失败、或者运行时抛TypeLoadException。
所以在决定 Connector 版本之前,先确认两件事:
- Unity 设置的 API Level
- 目标 MySQL 服务端的认证协议
这两个点决定了你要用哪个版本的 DLL,以及需要补哪些依赖。
3.4 连接字符串的正确写法
连接数据库的入口是 MySqlConnection,关键在于 ConnectionString。最基础但稳妥的格式是:
Server=127.0.0.1;Port=3306;Database=unity_test;User Id=root;Password=你的密码;Charset=utf8;Connection Timeout=5;几个参数的解释:
- Server:建议写 127.0.0.1,不要写 localhost。在部分系统上 localhost 可能被解析成 IPv6 的 ::1,而 MySQL 没有监听 IPv6,导致连接直接超时
- Port:默认 3306,如果服务端改过端口就要同步
- Charset=utf8:防止中文乱码
- Connection Timeout:建议给 5 秒或更短,避免数据库不可达时 Unity 卡住
4. 实操过程:从连接测试到常见操作
4.1 第一个脚本:连接测试
创建一个 C# 脚本MySqlConnectionTest.cs,然后丢到场景里的任意物体上。在编辑器里直接运行,看到日志输出就算成功。
using System; using UnityEngine; using MySql.Data.MySqlClient; public class MySqlConnectionTest : MonoBehaviour { private void Start() { string connStr = "Server=127.0.0.1;Port=3306;Database=unity_test;User Id=root;Password=你的密码;Charset=utf8;Connection Timeout=5;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { try { conn.Open(); Debug.Log("连接成功"); } catch (Exception ex) { Debug.LogError("连接失败: " + ex.Message); } } } }这里重点注意using语句。ADO.NET 的连接资源是有限且昂贵的,用完一定要释放。用using能保证连接对象在作用域结束时自动调用 Dispose,避免数据库连接泄漏。
我第一次跑这个脚本时,卡在了MySql.Data.MySqlClient找不到命名空间。原因就是 DLL 放在了 Assets 根目录但因为某些导入设置没生效。后来移进 Plugins 才稳定。
4.2 增强版:封装一个 DbManager
连接测试通过后,接着别急着写一大堆 SQL,先封装一个简单的数据库管理器。这个类可以在任何脚本里复用。
using System; using System.Data; using UnityEngine; using MySql.Data.MySqlClient; public class DbManager { private string connectionString; public DbManager(string server, string database, string user, string password, int port = 3306) { connectionString = string.Format( "Server={0};Port={1};Database={2};User Id={3};Password={4};Charset=utf8;Connection Timeout=5;", server, port, database, user, password); } public DataTable Query(string sql) { DataTable dt = new DataTable(); try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); using (MySqlCommand cmd = new MySqlCommand(sql, conn)) using (MySqlDataAdapter adapter = new MySqlDataAdapter(cmd)) { adapter.Fill(dt); } } } catch (Exception ex) { Debug.LogError("查询出错: " + ex.Message); } return dt; } public int ExecuteNonQuery(string sql) { int affected = 0; try { using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { affected = cmd.ExecuteNonQuery(); } } } catch (Exception ex) { Debug.LogError("执行出错: " + ex.Message); } return affected; } }这里有意用了DataTable和MySqlDataAdapter,方便后面直接绑定 UI 数据源。如果只是想读取单值,可以用cmd.ExecuteScalar(),省掉 DataTable 的开销。
4.3 数据读取与 UI 展示
为了验证链路,我习惯做一个极简 UI:在场景里放一个 Text 和一个 Button,点击按钮后把 player 表查询结果显示在面板上。
using UnityEngine; using UnityEngine.UI; public class DataPanel : MonoBehaviour { public Text resultText; private DbManager db; private void Start() { db = new DbManager("127.0.0.1", "unity_test", "root", "你的密码"); } public void OnClickLoadData() { DataTable table = db.Query("SELECT name, score FROM player ORDER BY score DESC"); if (table == null || table.Rows.Count == 0) { resultText.text = "暂无数据"; return; } string display = ""; foreach (DataRow row in table.Rows) { display += row["name"] + ":" + row["score"] + "\n"; } resultText.text = display; } }Button 的 onClick 事件直接在 Unity 编辑器里拖绑定即可。这一步做完,说明 Unity 与 MySQL 的数据链路已经完全打通:点击按钮 -> 发起 SQL -> MySQL 执行 -> 返回结果 -> UI 刷新。
有一个容易忽略的细节:Text 里显示中文时如果出现乱码,先检查 MySQL 表本身的数据编码,再确认连接串里的 Charset 是否等于 utf8,最后看场景字体是否缺失中文字形,这三点按顺序排查。
4.4 增删改操作示例
光查询还不够,实际项目肯定要写数据。举个例子,模拟玩家得分更新的操作:
public void AddPlayerScore(string name, int addScore) { int newScore = 0; object result = db.ExecuteScalar("SELECT score FROM player WHERE name='" + name + "'"); if (result != null && result != DBNull.Value) { newScore = Convert.ToInt32(result) + addScore; } string sql = "UPDATE player SET score = " + newScore + " WHERE name='" + name + "'"; db.ExecuteNonQuery(sql); }先别急着学这段写法,我在第 6 节会重点强调 SQL 注入的问题。上面的拼接字符串仅在内部测试时使用,任何面向外部输入的功能都不要这么写,要用参数化查询。
增删改的通用套路就三步:拼 SQL、构造 MySqlCommand、调用 ExecuteNonQuery。这里我补充一个带参数的正确示例:
public void UpdateScore(string name, int addScore) { string sql = "UPDATE player SET score = score + @addScore WHERE name = @name"; using (MySqlConnection conn = new MySqlConnection(connectionString)) { conn.Open(); using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@addScore", addScore); cmd.Parameters.AddWithValue("@name", name); cmd.ExecuteNonQuery(); } } }两者一比就明白了:参数化之后,特殊字符不会被当成 SQL 语句执行,传什么都是纯数据。这是底线,不要省。
4.5 编辑器菜单快速测试
有时候你不想启动游戏,只想在编辑器里点个按钮验证数据库连通性。我给 DbManager 加了一个编辑器测试入口:
#if UNITY_EDITOR using UnityEditor; public static class DbEditorTool { [MenuItem("Tools/Test MySQL")] public static void TestConnection() { DbManager db = new DbManager("127.0.0.1", "unity_test", "root", "你的密码"); DataTable dt = db.Query("SELECT COUNT(*) AS total FROM player"); Debug.Log("当前记录数: " + dt.Rows[0]["total"]); } } #endif这样不用进 Play Mode,直接在 Unity 菜单栏点Tools -> Test MySQL就能看到结果。开发阶段排查数据库问题时效率极高。
5. 踩坑实录:常见问题与排查技巧
这一节是我最想写的部分。因为这套组合的连接错误五花八门,但我梳理下来基本只有六类,每一类都有对应的排查路径。
5.1 “找不到 MySql.Data” 或 “命名空间不存在”
- 现象:代码里
using MySql.Data.MySqlClient;一行标红,或者编译报错。 - 原因:DLL 没被正确加载。最常见的是 DLL 不在 Plugins 目录,或者 DLL 的导入设置被 Unity 排除。
- 排查:在 Project 窗口选中 MySql.Data.dll,查看 Inspector 里的 Platform Settings,确保 Editor 和 Standalone 平台都是勾选状态。
- 解决:把它移入 Assets/Plugins,并重启 Unity。
5.2 认证协议报错
- 现象:运行时报如下错误:
Authentication method 'caching_sha2_password' not supported - 原因:MySQL 8.0 默认使用 caching_sha2_password 认证协议,而你使用的 MySql.Data 版本太老,或者没启用对应支持。
- 解决:方案一,换用 Connector/NET 8.0.18;方案二,在 MySQL 里把当前用户的认证协议改成 mysql_native_password:
我记得当时顺手测过两种方案,最终在项目里选了 Connector 版本对齐,而不是改认证协议,因为改老协议在集群迁移时会有额外兼容成本。ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;
5.3 连接超时
- 现象:
connect timeout expired或者Unable to connect to any of the specified MySQL hosts - 原因:网络不通、IP 错误、端口错误、服务端没启动、防火墙拦截。
- 排查步骤:
- 先确认服务端 MySQL 服务已启动
- 在同机命令行执行
ping 127.0.0.1确认网络栈正常 - 用
telnet 127.0.0.1 3306测试端口连通性 - 如果远程连接,检查服务端是否绑定了对外 IP,以及防火墙 3306 端口是否放行
- 在连接串中把 Server 换成 127.0.0.1 而不是 localhost
5.4 中文乱码
- 现象:数据库里中文正常,但读到 Unity 显示成乱码;或者写入的中文变成问号。
- 原因:连接串未指定 Charset,或者表结构本身不是 utf8/utf8mb4。
- 解决:连接串加
Charset=utf8;,建表时用CHARACTER SET utf8mb4。MySQL 8.0 默认就是 utf8mb4,通常不会出问题,但 5.7 的默认可能是 latin1。
5.5 打包后无法运行
- 现象:编辑器里一切正常,但打包成 exe 后点击功能无响应,日志提示找不到程序集。
- 原因:MySql.Data.dll 及依赖 DLL 没有随构建产物打包,或 SDK 平台选错。
- 解决:确认所有 DLL 在 Assets/Plugins 下,然后在 Player Settings 里查看每个插件的 Platform 设置,勾选需要支持的平台,并确认
Any Platform开启。重新构建并运行一次,把日志路径打开看看有没有加载失败的记录。
5.6 Unity 主线程卡顿
- 现象:大量数据查询时,画面掉帧严重,甚至整个窗口无响应。
- 原因:数据库操作是同步阻塞的,在 OnGUI 或 Update 里经常调用就会卡住主线程。
- 解决:把耗时操作放到协程或线程池。协程依然跑在主线程,但至少允许一帧一帧返回,配合 Timeout 不会造成无限制阻塞;线程池能彻底避免阻塞主线程,但要注意 Unity API 只能在主线程访问,所以线程池里只做数据读取,拿到结果后再转回主线程更新 UI。
简单给一个协程版封装示意:
public IEnumerator QueryAsync(string sql, Action<DataTable> onComplete) { DataTable dt = null; bool done = false; new Thread(() => { dt = db.Query(sql); done = true; }).Start(); while (!done) { yield return null; } if (onComplete != null) { onComplete(dt); } }用线程时,DataTable 的构造和填充都在后台线程完成,主线程只接管结果,这种方法我试过多次,在数据量几百到几千行时都很稳定。
6. 性能优化与安全加固
6.1 连接复用和连接池
直连 MySQL 时,每次 Open 和 Close 在底层都涉及 TCP 握手。如果频繁操作,性能会非常难看。但 ADO.NET 默认开着连接池,同一个连接字符串会自动复用连接,所以理论上不需要手动缓存连接对象。
不过有个隐性坑:连接池会保持空闲连接,如果 MySQL 服务端 wait_timeout 小于你的任务间隔,连接会被服务端掐断。此时再使用池中的连接,会抛 “Packet out of order” 之类诡异错误。解决方法是设置Pooling=true;并给连接加Connection Lifetime参数,或者捕获到连接异常后重试一次。最简单粗暴的做法是:每次操作都新建 MySqlConnection 对象,依赖连接池;出现断连错误时,先清空连接池再重试。
6.2 编辑器运行时的频繁调用
如果你在编辑器里做了一个实时刷新数据的窗口,千万别在 Update 里每帧查库。建议做一个刷新间隔控制,比如至少 1 秒才查一次,或者干脆只在用户操作的时候才查。数据库不是为游戏主循环设计的,每帧查询是给自己找麻烦。
6.3 使用参数化查询
这部分再怎么强调都不过分。SQL 注入的经典场景就是把用户输入直接拼进 SQL 字符串里,比如:
string sql = "SELECT * FROM player WHERE name = '" + input + "'";只要 input 传入' OR '1'='1,整张表就会暴露。正确写法是所有外部变量都用参数传入:
string sql = "SELECT * FROM player WHERE name = @name"; cmd.Parameters.AddWithValue("@name", input);这是底线级的要求,跟 Unity 无关,任何数据库代码都要默认参数化。
6.4 敏感信息不要硬编码
数据库密码直接写在脚本里,在个人项目里问题不大,但只要是多人协作或者做正式发布,就必须处理。推荐的简单做法是:
- 用 ScriptableObject 存连接配置,但不要放进版本控制中
- 把配置做成独立文件(如 JSON),运行时读取,使用时解密
- 或者在编辑器里通过环境变量读取,不要出现在代码库中
更彻底的做法是建立代理层,让 Unity 只访问服务端提供的 API,服务端再操作数据库。虽然这样会增加开发量,但在安全要求高的场景下,这是唯一靠谱的方案。直连方案更适合内部工具、原型验证、局域网管理端这类信任域清晰的应用。
6.5 日志与异常处理
数据库操作天然容易出错,尤其在客户端环境。除了在 DbManager 里捕获异常,还要记录足够上下文,比如:
- 失败的 SQL 语句
- 错误码和异常类型
- 数据库地址和表名
这样出现问题后,通过日志才能快速定位。另外,不要在异常里暴露连接字符串中的密码,否则日志文件泄露就风险很大。可以在打印异常前,把连接字符串中的 Password 字段替换为***。
7. 后续扩展建议
这套方案跑通之后,你会发现它不只是“Unity 连上了 MySQL”而已。顺着这条链路,还能做不少有价值的扩展:
一是做档案型数据的可视化:比如游戏运营后台,Unity 里展示用户行为数据、充值记录、关卡进度。与其开发一套网页后台,直接用 Unity 编辑器做一个工具窗口会更便捷。
二是做配置表的热更新:策划在 MySQL 里改数值,客户端启动时自动拉取最新配置。配合 ScriptableObject 缓存,可以省去频繁发版的麻烦。
三是做跨端同步的雏形:登录时从库中拉取存档,退出时把进度写回。虽然真正上线时不建议客户端直连数据库,但作为项目组内部开发和测试工具,这套直连方案在开发效率上的优势非常明显。
我个人的体会是,数据库连接这类技术,卡住你的往往不是代码逻辑,而是那些藏在版本、协议、路径里的隐性问题。把 DLL 放对、API Level 调对、连接串写对、认证协议配对,四关一过,剩下的就是写 SQL 的业务活。如果看完这篇还遇到没列到的问题,先把异常信息完整打出来,多数时候答案就在那三五行报错里。