简介:面向Unity开发者的MySQL数据库连接与表格显示示例包,基于Unity 2020.3.45f1构建,以Simple TableUI为视觉载体,演示从数据库读取表数据、转换为可在Unity界面中滚动展示的表格,并针对该插件使用中的常见报错提供解决思路。资源为一个zip压缩包,整体大小约24.36MB,适用于已有C#与Unity基础、需要为游戏或工具界面快速接入数据库功能的开发者;它既适合入门者按示例理解全流程,也适合正在排查Simple TableUI异常的中级开发者对照检查,尤其适合后台管理类工具、数据看板或带有实时数据列表的游戏界面。目前已有742人浏览/学习。通过本包可以掌握C#连接MySQL的环境配置、连接字符串写法、SELECT语句执行与DataTable/List转换等核心技能,同时借鉴插件错误修复思路,规避数据绑定失败、刷新时序错乱、滚动性能欠佳等典型坑点,加速实际项目的数据库模块集成。
1. Unity 连接 MySql 显示表格(Simple TableUI):一句话讲清楚它解决了什么
把 Unity 连接 MySql 显示表格(Simple TableUI)这件事做成一套能直接复用的组件,是我给运营后台做数据看板时被逼出来的。需求本身不复杂:从 MySQL 查一张玩家表,在 Unity 界面里以表格形式展示,支持刷新和翻页。可真正动手会发现,驱动选型、连接串、中文编码、滚动视图刷新,每个环节都能翻车。这篇文章会把从驱动选择、最小查询到 TableUI 渲染、避坑清单这条路完整走一遍,适合要给 Unity 编辑器工具或游戏内后台接 MySQL 的开发者,也适合刚按着 mysql 安装教程配好环境、还不知道怎么在 Unity 里跑通第一条查询的人。核心结论先放这儿:难点不在“显示”,而在“连接链路要提前理顺,表格刷新不能无脑重建”。
2. 连接链路先打通:MySql 驱动、连接串与第一个查询脚本
做表格 UI 之前,必须先把 Unity 到 MySQL 的数据通道打通并验证过。很多新手一上来就写 UI,结果查不出数据,回头改 UI 改半天,最后发现是连接串少了个参数。所以这一章只解决一个问题:让 Unity 里的 C# 脚本能稳定地执行一条 SQL 并拿回结果。
2.1 驱动选型:MySql.Data 还是 MySqlConnector
Unity 不像 ASP.NET 那样自带数据库驱动,你得自己把驱动 DLL 放进项目。两个主流选择:
| 对比维度 | MySql.Data(官方 Connector) | MySqlConnector(开源实现) |
|---|---|---|
| 上手难度 | 低,NuGet 拉下来直接用 | 略高,要为 Unity 找对应版本的程序集 |
| 异步支持 | 有,但历史包袱重 | 原生 async,性能更好 |
| SSL/安全特性 | 8.0 后默认行为有坑(见第 5 章) | 更可控 |
| 适合场景 | 本地小工具、编辑器面板、内网后台 | 线上频繁查询、需要高并发 |
我的建议是:如果你只是做运营查询工具、本地数据看板这类低频场景,直接用 MySql.Data,资料多、踩坑记录也多,出了问题搜得到。如果要做线上游戏服务端的数据接口,优先 MySqlConnector,异步更干净。另外一条更稳的路线是“Unity 不直连数据库”——在中间加一层 HTTP API(ASP.NET Core + Dapper),Unity 只发请求收 JSON。对于内网工具直连没问题,但如果目标是公网或多人同时用,直连 MySQL 会把账号密码暴露给所有客户端,这属于架构问题而不是标题范围内的实现问题,本文不展开。
把驱动 DLL 放进 Unity 后的第一个关键设置:菜单Edit > Project Settings > Player > Other Settings > Api Compatibility Level,选.NET Framework或.NET Standard 2.1,不要用默认的.NET Standard 2.0之外的低版本子集,否则程序集会报TypeLoadException或找不到System.Data。
2.2 最小连接串与查询封装
连接串是第一个黑匣子,报错七成出在这里。先看最小可用版本:
using System; using System.Collections.Generic; using MySql.Data.MySqlClient; public static class MySqlHelper { // 本地开发用;生产环境不要硬编码,放到 StreamingAssets 配置里 private static string connStr = "Server=127.0.0.1;Port=3306;Database=game_log;Uid=admin;Pwd=123456;" + "Charset=utf8mb4;SslMode=None;ConnectionTimeout=5;Pooling=true;"; public static List<T> Query<T>(string sql, Func<MySqlDataReader, T> mapper, params MySqlParameter[] args) { var list = new List<T>(); using (var conn = new MySqlConnection(connStr)) { conn.Open(); // 打开连接 using (var cmd = new MySqlCommand(sql, conn)) { if (args != null) cmd.Parameters.AddRange(args); // 参数化,防拼接 SQL using (var reader = cmd.ExecuteReader()) // 执行查询 { while (reader.Read()) list.Add(mapper(reader)); // 每行映射成 T } } } return list; } }参数说明,每个都对应一类报错:
Server=127.0.0.1:强制走 TCP。写localhost在某些平台的 Mono 环境会尝试走 Unix socket,这就是后面 error 2002 的根源。Port=3306:默认端口,改了 MySQL 配置就同步改这里。Database:目标库名,连错库会报Unknown database。Uid/Pwd:账号密码。本地测试不要用 root 直连业务库,单独建一个只读账号更安全。Charset=utf8mb4:中文乱码的关键参数。用了 utf8mb4 才能覆盖 emoji 和生僻字。SslMode=None:本地 MySQL 默认没配 SSL 证书,MySql.Data 8.0 默认Preferred会导致 SSL 握手失败,开发环境直接关掉(详见第 5 章)。ConnectionTimeout=5:默认 30 秒,连不上时卡死 UI 太久,调短便于快速反馈。Pooling=true:MySql.Data 默认开启连接池。好处是复用连接,坏处是池里的半死连接会报 “Packet sequence number wrong”,这类错误多半是先连上又断网导致的。
写完封装先在脚本里跑一条最简单的 SQL 验证:
void Start() { var rows = MySqlHelper.Query( "SELECT player_id, nickname FROM player LIMIT 5", r => $"{Convert.ToString(r["player_id"])}|{Convert.ToString(r["nickname"])}" ); foreach (var row in rows) Debug.Log(row); }这条能跑通,说明驱动、连接串、账号权限三条链路都通了,再去写 UI。
2.3 先用 Workbench 验数据,别让 Unity 背锅
我第一次做这类需求时犯过一个低级错误:Unity 里连不上,就在 C# 代码里反复调参数,调了一个小时,最后发现是 MySQL 服务根本没启动。所以我现在养成的习惯是:任何连接问题,先打开图形化客户端验证,再回 Unity 查代码。
用 mysql workbench 做三步验证:
- 用同一个账号密码连接同一个库。能登录,说明账号和 bind-address 没问题;登不上,看报错号,error 2002 / 1045 / 1130 各有各的修法。
- 在 Workbench 里跑一遍要查的 SQL,确认表和字段名没拼错,Mysql 的表名在 Linux 下是大小写敏感的。
- 检查服务监听状态。命令行执行:
# 看 3306 是否在监听 netstat -an | grep 3306 # 直接命令行登录验证,报错信息比 Unity 里更直白 mysql -u admin -p -h 127.0.0.1 -P 3306 game_log注意:如果你刚照 mysql 安装配置教程装完 MySQL 8,默认 root 的认证插件是caching_sha2_password,老版本 MySql.Data 驱动不认识这个插件,会报Authentication method 'caching_sha2_password' not supported。解决方式有两种:给业务账号指定mysql_native_password,或者升级到支持新认证协议的驱动版本。这个坑非常常见,属于“装好数据库不等于能连上”。
3. 把查询结果变成 UI 能用的数据:行模型与格式映射
连接通了,接下来不是直接往 UI 上塞数据,而是先定义一个稳定的数据层。这一步做不好,后面表格渲染出来的全是System.Data.DataRowView这种没法直接用的对象。
3.1 为什么不用 DataTable,改用 List<T>
很多人图省事让查询直接返回DataTable,然后遍历Rows填充 UI。在小数据量场景能跑,但有两个隐患:一是DataTable内部全是反射和索引访问,连续刷新几十次会积累明显的 GC 压力;二是DataTable和 UI 之间没有任何类型约束,字段改名、类型变更都要等到运行时才炸。改成一个具体的行模型类,编译期就能发现大部分问题,刷新时对象短小,GC 压力也可控。
以玩家表为例,先定义模型:
public class PlayerRow { public string PlayerId; // varchar public string Nickname; // varchar,允许 NULL public int Level; // int public DateTime LastLogin;// datetime public bool IsVip; // tinyint(1) }字段类型跟我执行 SQL 拿到的列一一对应,读代码的人不用去翻表结构就知道这一行是什么。
3.2 字段映射与空值、日期、枚举处理
这是最容易写出“看起来对但边界全错”的部分。MySqlDataReader 拿出来的值是object,直接ToString()会在 DBNull 上报错,日期格式化也常常不对。我的映射写法:
private PlayerRow MapRow(MySqlDataReader r) { PlayerRow row = new PlayerRow(); row.PlayerId = Convert.ToString(r["player_id"]); row.Nickname = r["nickname"] == DBNull.Value ? "--" : Convert.ToString(r["nickname"]); row.Level = Convert.ToInt32(r["level"]); row.LastLogin = r["last_login"] == DBNull.Value ? DateTime.MinValue : Convert.ToDateTime(r["last_login"]); row.IsVip = Convert.ToInt32(r["is_vip"]) == 1; // tinyint(1) 转 bool return row; }三条规则:
- 可空字段必须判断
DBNull.Value,不然整行渲染直接抛异常,表格空白。 - 日期在数据库是
datetime,不要拿ToString()直接拼,统一转DateTime后再格式化,展示层决定格式而不是数据库决定。 DECIMAL字段用decimal接,不要用float,金额精度会出问题;TINYINT(1)按0/1转bool,别当成整数显示。
给 UI 层用的格式化方法单独写一层,和MapRow分开:
private string[] FormatPlayer(PlayerRow row) { return new string[] { row.PlayerId, row.Nickname, row.Level.ToString(), row.LastLogin == DateTime.MinValue ? "--" : row.LastLogin.ToString("yyyy-MM-dd HH:mm") }; }这样数据模型管“存什么类型”,格式化只负责“显示成什么样子”。以后想改时间格式、加单位后缀,只动FormatPlayer,不动查询逻辑。
这里补一条查询经验:不要SELECT *,只取要显示的列。MySQL 端会省 IO,Unity 端省 GC。如果表里有 TEXT/BLOB 大字段,千万别整读进 UI 表格,卡到你怀疑人生。排序也尽量在 SQL 里做,ORDER BY level DESC, last_login ASC这种 mysql 排序写法交给数据库引擎,比在 C# 里List.Sort快一个量级。
4. 用 Simple TableUI 把 List 渲染成表格:表头、行、滚动三件套
数据准备好了,到本项目的核心:怎么把List<PlayerRow>渲染成一个能滚、能刷新的表格 UI。Simple TableUI 的思路就三步——建表头、按行生成格子、撑高滚动区。
4.1 先定结构:ScrollRect + 行容器,还是 UI Toolkit
uGUI 方案下,表格本质是一个ScrollRect,里面挂一个Content容器,代码按行往里塞Text。这套方案的优点是和旧项目 UI 风格统一、不引入新依赖;缺点是行数多的时候每行都是独立 GameObject,200 行以内没问题,超过 1000 行掉帧明显。
Unity 2021+ 的 UI Toolkit 提供了ListView,自带元素复用和虚拟化,千行表格也能扛住。但 UI Toolkit 的样式表(USS)写法对习惯了 uGUI 的团队有学习成本,而且很多老项目里 UI Toolkit 和 uGUI 混用会有层级渲染问题。我的取舍标准:500 行以内用 uGUI 纯代码生成,简单直接;超过 500 行或者需要频繁全量刷新,改用 UI ToolkitListView或给 uGUI 行对象做对象池。
下面给的是 uGUI 纯代码版,因为它最能说明 TableUI 的原理,也最容易抄。
4.2 最小 TableUI 组件:可直接抄的代码
先搭预制体结构,层级关系是这样:
Canvas (Screen Space - Overlay) └─ Panel ├─ HeaderRow (headerRoot,高 56) ├─ ScrollView (ScrollRect) │ ├─ Viewport (带 Mask) │ │ └─ Content (bodyRoot) │ └─ Scrollbar Vertical └─ RefreshButtonbodyRoot就是ScrollRect的Content,代码运行前它是个空 RectTransform。组件代码:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class SimpleTableUI : MonoBehaviour { public RectTransform headerRoot; // 表头容器 public RectTransform bodyRoot; // ScrollRect 的 Content public float rowHeight = 64f; // 行高,和字体大小联动 public float headerHeight = 56f; private string[] columnNames; private float[] columnWidths; public void SetColumns(string[] names, float[] widths) { if (names.Length != widths.Length) return; columnNames = names; columnWidths = widths; // 清掉旧表头,从后往前删避免索引错位 for (int i = headerRoot.childCount - 1; i >= 0; i--) Destroy(headerRoot.GetChild(i).gameObject); float x = 0f; for (int i = 0; i < names.Length; i++) { CreateCell(names[i], headerRoot, x, widths[i], headerHeight, TextAnchor.MiddleCenter); x += widths[i]; } headerRoot.sizeDelta = new Vector2(x, headerHeight); bodyRoot.sizeDelta = new Vector2(x, bodyRoot.sizeDelta.y); } public void RenderRows<T>(List<T> rows, Func<T, string[]> formatter) { // 清空旧行,同样从后往前 for (int i = bodyRoot.childCount - 1; i >= 0; i--) Destroy(bodyRoot.GetChild(i).gameObject); for (int r = 0; r < rows.Count; r++) { string[] vals = formatter(rows[r]); RectTransform row = new GameObject("Row_" + r).AddComponent<RectTransform>(); row.SetParent(bodyRoot, false); row.anchorMin = new Vector2(0f, 1f); row.anchorMax = new Vector2(1f, 1f); // 横向拉伸,纵向锚顶 row.pivot = new Vector2(0f, 1f); row.sizeDelta = new Vector2(0f, rowHeight); row.anchoredPosition = new Vector2(0f, -r * rowHeight); float x = 0f; for (int c = 0; c < columnWidths.Length && c < vals.Length; c++) { CreateCell(vals[c], row, x, columnWidths[c], rowHeight, TextAnchor.MiddleLeft); x += columnWidths[c]; } } // 关键:撑高 Content,否则 ScrollRect 不知道内容多长 bodyRoot.sizeDelta = new Vector2(bodyRoot.sizeDelta.x, rows.Count * rowHeight); } private void CreateCell(string content, RectTransform parent, float x, float w, float h, TextAnchor anchor) { var go = new GameObject("cell", typeof(Text)); var txt = go.GetComponent<Text>(); txt.text = content; txt.font = GetDefaultFont(); txt.fontSize = 22; txt.alignment = anchor; txt.color = Color.black; txt.raycastTarget = false; // 文字不挡鼠标事件 var rt = txt.rectTransform; rt.SetParent(parent, false); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.zero; rt.pivot = new Vector2(0f, 1f); rt.anchoredPosition = new Vector2(x, 0f); rt.sizeDelta = new Vector2(w, h); } private static Font GetDefaultFont() { #if UNITY_2022_1_OR_NEWER return Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"); #else return Resources.GetBuiltinResource<Font>("Arial.ttf"); #endif } }使用方式:
void RefreshTable() { string sql = "SELECT player_id, nickname, level, last_login, is_vip " + "FROM player ORDER BY level DESC LIMIT 200"; List<PlayerRow> rows = MySqlHelper.Query(sql, MapRow); tableUI.SetColumns( new[] { "玩家ID", "昵称", "等级", "最近登录" }, new[] { 160f, 180f, 80f, 220f } ); tableUI.RenderRows(rows, FormatPlayer); }逻辑说明:SetColumns先建表头并记录列宽,RenderRows每次先清空再重建。行对象用锚顶的方式排布,第 r 行的anchoredPosition.y = -r * rowHeight,天然从上往下排列。最后一步必须更新bodyRoot的高度,否则数据超过视口高度时滚动条不生效——这是新手最容易漏的一行,漏了的表现是“数据只有一屏,滚不动”。
GetDefaultFont里用了#if UNITY_2022_1_OR_NEWER宏判断,因为 Unity 2022 起内置字体从 Arial 改名为 LegacyRuntime,老写法在 2023 上直接抛ArgumentException。这个细节属于 Unity 扩展开发最常见的“版本断点”。
4.3 行高、列宽、交替行色这些必调参数
表格好不好看,全在参数上。我常用的模板:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| rowHeight | 56~72 | 字体 22 时 64 最舒适;小于 48 中文会挤 |
| headerHeight | 56 | 表头比行高一点,视觉上有分区感 |
| fontSize | 20~24 | / |
| 列宽 | 按内容定 | 短 ID 100,昵称 160~200,时间 200+ |
| 交替行色 | 0xFFFFFF / 0xF5F7FA | 偶数行加浅底色,长表格不串行 |
| 总列宽 | 需要时超过视口 | 列数多让 ScrollRect 同时开 Horizontal |
还有三个容易忽略的点:
Viewport必须挂Mask或RectMask2D,不然行内容溢出到外面。Content上不要挂ContentSizeFitter,它会跟手动设置的sizeDelta打架,每次刷新布局抖动。- 想做整行点击,不要依赖单个 Text 的点击,给行对象挂一个
Button,把行索引存在int字段里,在onClick里取回数据源。
交替行色我一般放在RenderRows里,用r % 2判断后给行对象加一个Image背景色,注意Image的raycastTarget也设为 false,只当背景用。
5. Simple TableUI 连接实战避坑:5 条能直接救场的踩坑记录
这一章写全是真实高频的坑,按“现象 → 原因 → 解决”给,照着对号入座。
5.1 error 2002 (HY000):连不上本地 socket,而不是连不上数据库
- 现象:Unity 里执行查询抛
MySqlException: Can't connect to local MySQL server through socket '/tmp/mysql.sock'。 - 原因:连接串里写的是
Server=localhost。在 Windows 上 localhost 会走 TCP,在 macOS / Linux 的 Mono 环境下,MySql.Data 会尝试走 Unix socket 文件,而 MySQL 的 socket 路径要么不对,要么服务根本没监听这个路径。 - 解决:连接串改成
127.0.0.1强制走 TCP。同时确认服务在跑:
netstat -an | grep 3306 mysql -u admin -p -h 127.0.0.1 -P 3306 game_log命令行能连上而 Unity 连不上的,基本都是 localhost 解析差异;命令行也连不上的,去查 MySQL 服务状态,不是 Unity 的锅。
5.2 中文乱码:连接串少了一个参数
- 现象:表格里中文全变成
???或繁体乱码,英文正常。 - 原因:客户端连接字符集和数据库/表字符集不一致。MySQL 8 默认库字符集是 utf8mb4,但 MySql.Data 连接时如果没指定
Charset,会按老版本默认的 latin1 和服务器协商。 - 解决:连接串加
Charset=utf8mb4;,连接建立后再执行SET NAMES utf8mb4兜底:
conn.Open(); using (var cmd = new MySqlCommand("SET NAMES utf8mb4", conn)) cmd.ExecuteNonQuery();另外检查表本身的字符集:SHOW TABLE STATUS LIKE 'player';。表还是 latin1 的,光改连接串也没用,得ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4;。
5.3 SSL 连接错误:本地库没配证书
- 现象:连接时抛错,关键词是
The host ... does not support SSL connections或SSL Connection Error。 - 原因:MySql.Data 8.0 起
SslMode默认是Preferred,会先尝试 SSL 握手。本地 MySQL 通常没配证书,握手失败直接断。 - 解决:开发环境显式指定
SslMode=None;。生产环境别这么干,SslMode=Required并配置 CA 证书。这里要区分:错误信息里带 SSL 字样的,都不是账号密码问题,别去改 Uid。
5.4 刷新表格后滚动条自己回到顶部
- 现象:表格有 200 行,滚到底部点刷新,数据是新的了,但视图跳回第一行,用户每次都要重新滚。
- 原因:
RenderRows把bodyRoot的子节点全删了,Content 高度先归零再重建,ScrollRect的normalizedPosition被重置。 - 解决:刷新前记录位置,重建后等一帧再恢复:
float pos = scroll.verticalNormalizedPosition; // 刷新前记录 tableUI.RenderRows(rows, FormatPlayer); StartCoroutine(RestoreScroll(pos)); IEnumerator RestoreScroll(float pos) { yield return null; // 等 Content 高度更新完 scroll.verticalNormalizedPosition = pos; }如果刷新的数据量没变,直接复用行对象只改 Text 内容,比全删全建更快,滚动位置天然不丢。
5.5 打包后连不上库:权限、监听地址和连接池
- 现象:Editor 里一切正常,打包成 Windows 可执行文件或 Android APK 后连不上,报超时或
Packet sequence number wrong。 - 原因:三个叠加。第一,MySQL 默认
bind-address = 127.0.0.1,只监听本机回环,外部设备连不进来;第二,账号授权写的是'user'@'localhost',换一台机器就不是 localhost 了;第三,连接池里的旧连接在断网后失效,复用时报包序号错乱。 - 解决:远程访问的场景,MySQL 配置改
bind-address = 0.0.0.0并开放防火墙 3306 端口;账号改成'user'@'%'授全局限定库的权限;连接池上Pooling=true时跑一个探活查询再重试,或者干脆在工具类里做“失败一次就强制重连”的逻辑:
catch (MySqlException ex) when (ex.Message.Contains("Packet sequence")) { MySqlConnection.ClearPool(conn); // 清掉连接池里的坏连接 // 重新走一遍 Open + ExecuteReader }这套组合拳能解决绝大多数“编辑器正常、发布物翻车”的玄学问题。
6. 进阶:异步查询、翻页排序与数据核对,把这套 TableUI 用到生产
编辑部工具可以同步查询,但游戏里的后台面板必须把查询挪出主线程。最稳的做法是后台线程查询 + 主线程标记轮询,而不是依赖异步回调:
private List<PlayerRow> pendingRows; private bool queryDone; void Update() { if (!queryDone) return; tableUI.RenderRows(pendingRows, FormatPlayer); // 回主线程渲染 queryDone = false; } void StartQuery() { ThreadPool.QueueUserWorkItem(_ => { pendingRows = MySqlHelper.Query(sql, MapRow); // 数据库读取在后台线程 queryDone = true; // 标记位让主线程取 }); }注意:pendingRows只由后台线程写入,主线程在Update里读,不要两边同时写同一个集合。这种模式比async/await在 Unity 里更可控,因为 Unity 的同步上下文在多线程模式下不总是把 await 续体弹回主线程,用标记位最保险。
翻页在 SQL 层做,别一次全拉。列表总数用SELECT COUNT(*)单独查,数据用LIMIT @offset, @count查当前页,每页 100 行。表头点击排序,就在 SQL 里拼ORDER BY,白名单校验列名防止注入。如果你要的是“把远程库的这张表同步到本地”这种场景,更优解是定期用mysqldump -h 远程IP -u user -p db table > table.sql导出再导入本地,而不是让 UI 直连线上库——线上库要留连接给业务,别给表格工具挤爆了。
我的验证习惯:每条改完的 SQL 先在 Workbench 跑一遍,对比 Unity 里显示的条数、首行末行,一致才算过;查询耗时用Stopwatch打印到 Console,超过 300ms 就检查是不是漏了索引。长时间挂机的工具还要处理 MySQL 的wait_timeout,连接空闲超过 8 小时会被服务端杀掉,下次查询前先SELECT 1探活。这套办法从 2019 年用到今天,帮我把 Unity 直连 MySQL 的表格工具稳定运维了快三个年头,希望帮到你。
本文还有配套的精品资源,点击获取