
简介面向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 还是 MySqlConnectorUnity 不像 ASP.NET 那样自带数据库驱动你得自己把驱动 DLL 放进项目。两个主流选择对比维度MySql.Data官方 ConnectorMySqlConnector开源实现上手难度低NuGet 拉下来直接用略高要为 Unity 找对应版本的程序集异步支持有但历史包袱重原生 async性能更好SSL/安全特性8.0 后默认行为有坑见第 5 章更可控适合场景本地小工具、编辑器面板、内网后台线上频繁查询、需要高并发我的建议是如果你只是做运营查询工具、本地数据看板这类低频场景直接用 MySql.Data资料多、踩坑记录也多出了问题搜得到。如果要做线上游戏服务端的数据接口优先 MySqlConnector异步更干净。另外一条更稳的路线是“Unity 不直连数据库”——在中间加一层 HTTP APIASP.NET Core DapperUnity 只发请求收 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 Server127.0.0.1;Port3306;Databasegame_log;Uidadmin;Pwd123456; Charsetutf8mb4;SslModeNone;ConnectionTimeout5;Poolingtrue;; public static ListT QueryT(string sql, FuncMySqlDataReader, T mapper, params MySqlParameter[] args) { var list new ListT(); 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; } }参数说明每个都对应一类报错Server127.0.0.1强制走 TCP。写localhost在某些平台的 Mono 环境会尝试走 Unix socket这就是后面 error 2002 的根源。Port3306默认端口改了 MySQL 配置就同步改这里。Database目标库名连错库会报Unknown database。Uid/Pwd账号密码。本地测试不要用 root 直连业务库单独建一个只读账号更安全。Charsetutf8mb4中文乱码的关键参数。用了 utf8mb4 才能覆盖 emoji 和生僻字。SslModeNone本地 MySQL 默认没配 SSL 证书MySql.Data 8.0 默认Preferred会导致 SSL 握手失败开发环境直接关掉详见第 5 章。ConnectionTimeout5默认 30 秒连不上时卡死 UI 太久调短便于快速反馈。PoolingtrueMySql.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改用 ListT很多人图省事让查询直接返回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 端会省 IOUnity 端省 GC。如果表里有 TEXT/BLOB 大字段千万别整读进 UI 表格卡到你怀疑人生。排序也尽量在 SQL 里做ORDER BY level DESC, last_login ASC这种 mysql 排序写法交给数据库引擎比在 C# 里List.Sort快一个量级。4. 用 Simple TableUI 把 List 渲染成表格表头、行、滚动三件套数据准备好了到本项目的核心怎么把ListPlayerRow渲染成一个能滚、能刷新的表格 UI。Simple TableUI 的思路就三步——建表头、按行生成格子、撑高滚动区。4.1 先定结构ScrollRect 行容器还是 UI ToolkituGUI 方案下表格本质是一个ScrollRect里面挂一个Content容器代码按行往里塞Text。这套方案的优点是和旧项目 UI 风格统一、不引入新依赖缺点是行数多的时候每行都是独立 GameObject200 行以内没问题超过 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 RenderRowsT(ListT rows, FuncT, 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).AddComponentRectTransform(); 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.GetComponentText(); 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.GetBuiltinResourceFont(LegacyRuntime.ttf); #else return Resources.GetBuiltinResourceFont(Arial.ttf); #endif } }使用方式void RefreshTable() { string sql SELECT player_id, nickname, level, last_login, is_vip FROM player ORDER BY level DESC LIMIT 200; ListPlayerRow 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 行高、列宽、交替行色这些必调参数表格好不好看全在参数上。我常用的模板参数推荐值说明rowHeight56~72字体 22 时 64 最舒适小于 48 中文会挤headerHeight56表头比行高一点视觉上有分区感fontSize20~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: Cant connect to local MySQL server through socket /tmp/mysql.sock。原因连接串里写的是Serverlocalhost。在 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 和服务器协商。解决连接串加Charsetutf8mb4;连接建立后再执行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 通常没配证书握手失败直接断。解决开发环境显式指定SslModeNone;。生产环境别这么干SslModeRequired并配置 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只监听本机回环外部设备连不进来第二账号授权写的是userlocalhost换一台机器就不是 localhost 了第三连接池里的旧连接在断网后失效复用时报包序号错乱。解决远程访问的场景MySQL 配置改bind-address 0.0.0.0并开放防火墙 3306 端口账号改成user%授全局限定库的权限连接池上Poolingtrue时跑一个探活查询再重试或者干脆在工具类里做“失败一次就强制重连”的逻辑catch (MySqlException ex) when (ex.Message.Contains(Packet sequence)) { MySqlConnection.ClearPool(conn); // 清掉连接池里的坏连接 // 重新走一遍 Open ExecuteReader }这套组合拳能解决绝大多数“编辑器正常、发布物翻车”的玄学问题。6. 进阶异步查询、翻页排序与数据核对把这套 TableUI 用到生产编辑部工具可以同步查询但游戏里的后台面板必须把查询挪出主线程。最稳的做法是后台线程查询 主线程标记轮询而不是依赖异步回调private ListPlayerRow 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 的表格工具稳定运维了快三个年头希望帮到你。本文还有配套的精品资源点击获取