想象一下你正在玩一款RPG游戏,辛辛苦苦打了3个小时,角色升到30级,收集了一堆稀有装备。突然游戏崩溃,重新打开后发现所有进度都消失了——这种体验绝对让人崩溃。这就是数据持久化要解决的核心问题。
在游戏开发中,数据持久化指的是将游戏运行时产生的数据(如玩家等级、装备、成就等)永久保存到存储介质中。常见方案有PlayerPrefs、本地文件存储和数据库存储。其中MySQL数据库特别适合需要复杂查询、多设备同步或服务端存储的中大型游戏项目。
我去年开发一款MMO手游时就吃过亏,最初用JSON文件存储玩家数据,当同时在线人数超过500时,服务器就开始出现读写冲突。后来全面迁移到MySQL,不仅解决了并发问题,还能实现好友系统、排行榜等复杂功能。下面我就把实战中积累的MySQL集成经验完整分享出来。
官方MySQL安装包其实有个隐藏坑点:如果直接安装最新版(如8.0+),可能会遇到Unity连接失败的问题。这是因为新版默认使用caching_sha2_password加密方式,而旧版Unity插件可能不支持。
推荐方案:
安装完成后,建议用MySQL Workbench创建一个专门用于游戏的数据库:
sql复制CREATE DATABASE game_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
这里用utf8mb4字符集是为了支持emoji等特殊字符,很多手游的玩家昵称会用到。
官方mysql-connector-net有多个版本分支,我测试过多个组合后发现:
关键操作:
mysql.data.dllPlugins文件夹(注意大小写)原始代码直接将数据库密码硬编码在脚本里,这在实际项目中非常危险。我推荐使用ScriptableObject来存储敏感配置:
csharp复制[CreateAssetMenu]
public class DatabaseConfig : ScriptableObject {
public string host = "127.0.0.1";
public string port = "3306";
public string database = "game_db";
public string username = "game_user"; // 不要用root账号!
public string password;
}
使用时通过资源加载获取配置,这样密码不会出现在代码仓库中。更专业的做法是使用环境变量或在服务器启动时注入配置。
频繁开关数据库连接会严重影响性能。实测在100个玩家同时登录的场景下,使用连接池可以将查询速度提升3倍:
csharp复制string constr = $"server={host};port={port};database={database};user={username};password={password};pooling=true;min pool size=5;max pool size=15;";
参数说明:
pooling=true 启用连接池min pool size 保持的最小连接数max pool size 最大连接数(根据游戏预期在线人数调整)原始代码直接拼接SQL字符串非常危险,玩家输入' OR '1'='1就会导致数据泄露。正确做法是使用参数化查询:
csharp复制public PlayerData GetPlayer(string playerName) {
string sql = "SELECT * FROM players WHERE name=@name";
MySqlCommand cmd = new MySqlCommand(sql, mysql);
cmd.Parameters.AddWithValue("@name", playerName);
using(var reader = cmd.ExecuteReader()) {
if(reader.Read()) {
return new PlayerData {
id = reader.GetInt32("id"),
level = reader.GetInt32("level"),
// 其他字段...
};
}
}
return null;
}
当需要初始化大量NPC数据时,单条插入效率极低。MySQL支持批量插入语法:
csharp复制public void BatchInsertItems(List<Item> items) {
StringBuilder sb = new StringBuilder("INSERT INTO items (id,name,type) VALUES ");
for(int i=0; i<items.Count; i++) {
sb.Append($"({items[i].id}, '{items[i].name}', {items[i].type})");
if(i < items.Count-1) sb.Append(",");
}
MySqlCommand cmd = new MySqlCommand(sb.ToString(), mysql);
cmd.ExecuteNonQuery();
}
实测插入1000条数据时,批量插入比单条插入快50倍以上。记得控制单次批量数据量不要超过1MB。
玩家交易装备时需要保证原子性操作:
csharp复制public bool TradeItem(int fromPlayer, int toPlayer, int itemId) {
using(var transaction = mysql.BeginTransaction()) {
try {
// 扣除卖方物品
var cmd1 = new MySqlCommand("UPDATE inventory SET count=count-1 WHERE player_id=@pid AND item_id=@iid", mysql);
cmd1.Parameters.AddWithValue("@pid", fromPlayer);
cmd1.Parameters.AddWithValue("@iid", itemId);
cmd1.ExecuteNonQuery();
// 增加买方物品
var cmd2 = new MySqlCommand("INSERT INTO inventory(player_id,item_id) VALUES(@pid,@iid)", mysql);
cmd2.Parameters.AddWithValue("@pid", toPlayer);
cmd2.Parameters.AddWithValue("@iid", itemId);
cmd2.ExecuteNonQuery();
transaction.Commit();
return true;
} catch {
transaction.Rollback();
return false;
}
}
}
在Unity编辑器中添加简单性能统计:
csharp复制System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
// 执行SQL查询...
sw.Stop();
Debug.Log($"查询耗时: {sw.ElapsedMilliseconds}ms");
如果发现某个查询超过100ms,就应该考虑:
在MySQL客户端运行:
sql复制EXPLAIN SELECT * FROM players WHERE level > 50;
重点关注:
type列:最好达到ref或rangerows列:扫描行数越少越好Extra列:出现Using filesort就需要优化游戏运营中最怕数据丢失。我建议设置自动备份:
backup.sh:bash复制mysqldump -u game_user -p game_db > /backups/game_db_$(date +%Y%m%d).sql
bash复制0 3 * * * /path/to/backup.sh
csharp复制public void ManualBackup() {
string backupCmd = $"mysqldump -h {host} -u {username} -p{password} {database} > backup.sql";
System.Diagnostics.Process.Start("cmd.exe", "/C " + backupCmd);
}
记得定期测试备份文件的恢复流程,我遇到过备份文件损坏导致无法恢复的情况。