Godot+C#实战:通过SQLite实现游戏数据持久化存储

张开发
2026/4/5 19:54:41 15 分钟阅读

分享文章

Godot+C#实战:通过SQLite实现游戏数据持久化存储
1. 为什么选择SQLite作为Godot游戏的数据存储方案在开发Godot游戏时数据持久化是绕不开的话题。我尝试过多种存储方案从最简单的文本文件到二进制序列化最终发现SQLite是最平衡的选择。这个轻量级数据库引擎不需要单独安装服务单个文件就能存储结构化数据读写速度也完全能满足游戏需求。记得我第一次做RPG游戏存档功能时用JSON存储玩家数据。当物品数量超过100件时加载存档明显卡顿。后来改用SQLite同样的数据量几乎感受不到延迟。更重要的是SQLite支持事务处理这在防止存档损坏方面特别有用——比如游戏崩溃时不会出现只存了一半的尴尬情况。对于中小型游戏项目SQLite的性能完全够用。实测在Godot 4.5环境下每秒可以执行超过5万次简单查询。即使是包含复杂联查的场景也能保持流畅体验。另一个优势是跨平台支持无论玩家在Windows、Mac还是手机上数据文件都能完美兼容。2. 搭建GodotC#SQLite开发环境2.1 安装必要插件和依赖首先在Godot编辑器中打开AssetLib搜索Godot-SQLite并安装。这里有个坑要注意插件必须放在res://addons目录下否则Godot会找不到。安装完成后记得在项目设置的插件面板中启用它。接下来是关键步骤——为C#项目添加SQLite支持。打开项目根目录有.csproj文件的那个在命令行执行dotnet add package System.Data.SQLite.Core这个操作会自动修改.csproj文件添加必要的NuGet引用。我遇到过几次失败情况大多是.NET SDK版本不匹配导致的。建议使用Godot 4.5官方推荐的.NET 8.0版本兼容性最好。2.2 配置数据库连接桥接由于Godot-SQLite插件是用GDScript编写的我们需要建立C#到GDScript的桥接。创建一个名为SQLiteBridge.gd的脚本extends Node func _ready(): if ClassDB.class_exists(SQLite): var db ClassDB.instantiate(SQLite) db.path user://game.db if db.open_db(): print(数据库初始化成功) db.query( CREATE TABLE IF NOT EXISTS player_saves ( id INTEGER PRIMARY KEY, save_name TEXT, timestamp INTEGER, data BLOB ) ) else: print(数据库打开失败)这个桥接脚本会确保数据库文件在游戏启动时就被创建。注意使用user://路径很重要这是Godot提供的跨平台可写目录不同操作系统下会自动映射到正确位置。3. 实现游戏数据的CRUD操作3.1 设计数据访问层我习惯采用三层架构设计实体类→数据访问对象→业务逻辑。先定义玩家存档的实体结构public class GameSave { public int Id { get; set; } public string SaveName { get; set; } public long Timestamp { get; set; } public byte[] Data { get; set; } // 二进制序列化方法 public static byte[] Serialize(object obj) { using (var stream new MemoryStream()) { var formatter new BinaryFormatter(); formatter.Serialize(stream, obj); return stream.ToArray(); } } // 反序列化方法 public static T DeserializeT(byte[] data) { using (var stream new MemoryStream(data)) { var formatter new BinaryFormatter(); return (T)formatter.Deserialize(stream); } } }然后是核心的数据访问类封装所有SQL操作public static class SaveDAO { private static string GetDbPath() { return Path.Combine(OS.GetUserDataDir(), game.db); } public static bool CreateSave(GameSave save) { var sql INSERT INTO player_saves (save_name, timestamp, data) VALUES (name, time, data); try { using (var conn new SQLiteConnection($Data Source{GetDbPath()})) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue(name, save.SaveName); cmd.Parameters.AddWithValue(time, DateTime.Now.Ticks); cmd.Parameters.AddWithValue(data, save.Data); return cmd.ExecuteNonQuery() 0; } } } catch (Exception e) { GD.PrintErr($存档创建失败: {e.Message}); return false; } } // 其他方法省略... }3.2 实现物品管理系统物品数据通常需要更复杂的结构。我推荐使用JSON存储物品属性既保持灵活性又方便查询public class InventoryItem { public string ItemId { get; set; } public int Count { get; set; } public Dictionarystring, object Attributes { get; set; } } public static class InventoryDAO { public static void SaveInventory(string playerId, ListInventoryItem items) { var sql INSERT OR REPLACE INTO player_inventory (player_id, item_data) VALUES (id, data); using (var conn new SQLiteConnection(DBConnection.GetConnectionString())) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue(id, playerId); cmd.Parameters.AddWithValue(data, JsonConvert.SerializeObject(items)); cmd.ExecuteNonQuery(); } } } public static ListInventoryItem LoadInventory(string playerId) { var sql SELECT item_data FROM player_inventory WHERE player_id id; using (var conn new SQLiteConnection(DBConnection.GetConnectionString())) { conn.Open(); using (var cmd new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue(id, playerId); using (var reader cmd.ExecuteReader()) { if (reader.Read()) { return JsonConvert.DeserializeObjectListInventoryItem( reader[item_data].ToString()); } } } } return new ListInventoryItem(); } }4. 性能优化与实战技巧4.1 批量操作提升效率当需要处理大量数据时比如保存100个NPC的状态单个SQL语句执行会非常低效。这时应该使用事务批量处理public static void BatchUpdateNpcs(ListNpcState npcs) { using (var conn new SQLiteConnection(DBConnection.GetConnectionString())) { conn.Open(); using (var trans conn.BeginTransaction()) { try { var sql UPDATE npc_states SET position_xx, position_yy, healthhp WHERE npc_idid; foreach (var npc in npcs) { using (var cmd new SQLiteCommand(sql, conn)) { cmd.Parameters.AddWithValue(x, npc.Position.X); cmd.Parameters.AddWithValue(y, npc.Position.Y); cmd.Parameters.AddWithValue(hp, npc.Health); cmd.Parameters.AddWithValue(id, npc.Id); cmd.ExecuteNonQuery(); } } trans.Commit(); } catch { trans.Rollback(); throw; } } } }在我的一个策略游戏项目中使用批量操作后存档时间从3秒缩短到了0.5秒左右。4.2 数据加密与安全直接存储明文数据存在安全隐患特别是对于在线游戏。建议至少对敏感数据进行简单加密public static class DataSecurity { private static byte[] key Encoding.UTF8.GetBytes(16字节长度的密钥); public static byte[] Encrypt(byte[] data) { using (var aes Aes.Create()) { aes.Key key; using (var encryptor aes.CreateEncryptor()) using (var ms new MemoryStream()) { using (var cs new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(data, 0, data.Length); } return ms.ToArray(); } } } // 解密方法类似... } // 在DAO中使用 var encryptedData DataSecurity.Encrypt(saveData);5. 调试与常见问题解决5.1 数据库文件位置问题新手最容易犯的错误是搞不清数据库文件的存放位置。Godot的user://路径在不同平台的实际位置Windows:%APPDATA%\Godot\app_userdata\[项目名]\macOS:~/Library/Application Support/Godot/app_userdata/[项目名]/Linux:~/.local/share/godot/app_userdata/[项目名]/调试时可以打印完整路径确认GD.Print(数据库路径: , Path.Combine(OS.GetUserDataDir(), game.db));5.2 处理并发访问当多个场景同时访问数据库时可能引发锁冲突。解决方案是使用单例模式管理连接public class DatabaseManager { private static SQLiteConnection _connection; private static readonly object _lock new object(); public static SQLiteConnection GetConnection() { lock (_lock) { if (_connection null) { _connection new SQLiteConnection(DBConnection.GetConnectionString()); _connection.Open(); } return _connection; } } }在回合制游戏项目中这种设计帮我解决了战斗结算时的数据竞争问题。6. 扩展应用场景6.1 玩家偏好设置存储除了存档SQLite还适合存储游戏设置public static class SettingsManager { public static void SaveSetting(string key, object value) { var sql INSERT OR REPLACE INTO game_settings (setting_key, setting_value) VALUES (key, value); using (var cmd new SQLiteCommand(sql, DatabaseManager.GetConnection())) { cmd.Parameters.AddWithValue(key, key); cmd.Parameters.AddWithValue(value, JsonConvert.SerializeObject(value)); cmd.ExecuteNonQuery(); } } public static T GetSettingT(string key, T defaultValue default) { var sql SELECT setting_value FROM game_settings WHERE setting_key key; using (var cmd new SQLiteCommand(sql, DatabaseManager.GetConnection())) { cmd.Parameters.AddWithValue(key, key); var result cmd.ExecuteScalar(); return result ! null ? JsonConvert.DeserializeObjectT(result.ToString()) : defaultValue; } } }6.2 成就系统实现用SQLite管理成就进度非常方便public class AchievementSystem { public void UnlockAchievement(string playerId, string achievementId) { var sql INSERT INTO player_achievements (player_id, achievement_id, unlock_time) VALUES (pid, aid, time); using (var cmd new SQLiteCommand(sql, DatabaseManager.GetConnection())) { cmd.Parameters.AddWithValue(pid, playerId); cmd.Parameters.AddWithValue(aid, achievementId); cmd.Parameters.AddWithValue(time, DateTime.Now.Ticks); cmd.ExecuteNonQuery(); } // 触发游戏内通知 EventBus.Publish(AchievementUnlocked, achievementId); } public bool IsAchievementUnlocked(string playerId, string achievementId) { var sql SELECT 1 FROM player_achievements WHERE player_id pid AND achievement_id aid; using (var cmd new SQLiteCommand(sql, DatabaseManager.GetConnection())) { cmd.Parameters.AddWithValue(pid, playerId); cmd.Parameters.AddWithValue(aid, achievementId); return cmd.ExecuteScalar() ! null; } } }在实际项目中这套架构成功支持了包含200成就的复杂系统。SQLite的轻量级特性使得即使存储大量玩家数据游戏启动速度也不受影响。

更多文章