1. 项目概述:为什么游戏数据持久化是独立开发者的必修课
如果你正在用Godot和C#做游戏,尤其是那些需要存档、读档、排行榜或者复杂道具系统的项目,那么数据持久化绝对是你绕不开的核心技术。简单来说,数据持久化就是让游戏产生的数据(比如玩家的等级、金币、背包里的装备、地图探索进度)在游戏关闭后依然存在,下次打开游戏还能接着玩。听起来基础,但实现方式的选择,直接关系到你项目的稳定性、开发效率和未来的可扩展性。
早期很多教程会教你用Godot自带的ConfigFile或者直接读写文本文件(.json,.ini)来存数据。对于小型、结构简单的数据,这确实够用。但一旦数据量变大、关系变复杂(比如玩家拥有多个角色,每个角色有独立的装备栏和技能树),文件读写就会变得笨拙且容易出错。这时,一个轻量级但功能完整的数据库就成了更优解。SQLite,作为一个进程内的、无需服务器的、零配置的SQL数据库引擎,几乎是桌面端和移动端游戏数据存储的“标准答案”。它把整个数据库存成一个单一的磁盘文件,用标准的SQL语句操作,既拥有了关系型数据库的强大查询能力,又保持了本地文件的简单性。
这次,我们就来实战如何在Godot 4.x环境中,通过C#脚本来集成和使用SQLite,构建一个健壮的游戏数据存储层。我会带你走通从环境搭建、数据库设计、C#交互到错误处理的完整流程,并分享那些官方文档里不会写的“踩坑”经验。
2. 环境准备与核心工具链搭建
在开始写代码之前,我们需要把“战场”布置好。Godot本身并不原生支持SQLite,我们需要借助一个社区维护的插件来搭建桥梁。同时,为了可视化管理数据库文件,一个好用的GUI工具也必不可少。
2.1 安装Godot-SQLite插件
插件的安装是第一步,也是第一个容易出错的地方。
- 打开AssetLib:在Godot编辑器顶部菜单栏,点击“项目(Project)” -> “资产管理(AssetLib)”。
- 搜索插件:在搜索框输入“SQLite”。通常,排名靠前的会是
godot-sqlite或Godot-SQLite。认准作者和星标,选择一个维护活跃的版本(例如由paul-ti-robot或2shady4u维护的)。 - 下载与安装:点击插件,选择“下载(Download)”,下载完成后点击“安装(Install)”。Godot会自动将插件文件解压到你的项目目录中。
关键注意事项:安装完成后,你必须检查插件文件是否被正确放置在了
res://addons/目录下。这是Godot加载插件的固定位置。有时网络或编辑器问题会导致文件被下载到临时目录,你需要手动将包含.gd和.cs文件的插件文件夹移动到项目的addons目录中。如果插件目录位置不对,Godot在启用插件时会直接“看不见”它。
- 启用插件:再次点击“项目(Project)” -> “项目设置(Project Settings)”。切换到“插件(Plugins)”标签页,找到刚刚安装的SQLite插件,点击其右侧的“启用(Enable)”复选框。如果启用成功,你可能会在编辑器底部看到相关输出,或者插件图标变亮。
2.2 配置C#项目与SQLite依赖
由于我们使用C#进行开发,还需要确保C#项目能引用到SQLite的程序集。
- 创建C#脚本:在Godot中新建一个C#脚本(例如
DatabaseManager.cs)。当你第一次创建C#脚本时,Godot会自动为你生成一个.csproj(C#项目文件)。 - 编辑.csproj文件:用文本编辑器(如VSCode)打开项目根目录下的
.csproj文件。我们需要手动添加对System.Data.SQLite的NuGet包引用。在<ItemGroup>节点内添加如下内容:
这里版本号<ItemGroup> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" /> </ItemGroup>1.0.118是一个较稳定且与.NET框架兼容性较好的版本,你可以根据情况使用更新的版本,但需注意与Godot使用的.NET运行时版本的兼容性。 - 恢复NuGet包:保存
.csproj文件后,在命令行中进入你的项目根目录,运行dotnet restore命令。这会自动下载System.Data.SQLite.Core包及其依赖到本地。如果你使用的IDE(如Rider或安装了C#插件的VSCode)通常会自动执行这一步。
2.3 数据库可视化工具:DB Browser for SQLite
在开发过程中,我们经常需要直接查看、修改或调试数据库内容。DB Browser for SQLite (DB4S)是一个免费、开源、跨平台的绝佳选择。
- 作用:它可以让你像使用Excel一样打开
.db或.sqlite文件,执行SQL命令,浏览表结构,编辑数据,甚至进行导入/导出操作。 - 使用场景:
- 调试:当游戏内数据出现异常时,直接打开数据库文件查看,比打日志更直观。
- 初始化:在游戏开发初期,手动插入一些测试数据,方便快速搭建测试环境。
- 修复:如果游戏逻辑导致数据错误,可以直接在DB4S里进行修正。
安装好这个工具,把它当作你开发工具箱里的常备利器。在后续操作数据库时,你可以随时用DB4S打开生成的文件,验证你的C#代码是否按预期工作。
3. 数据库设计与C#数据模型构建
在动手连接数据库之前,我们必须想清楚数据要怎么存。一个好的设计能让你后续的开发事半功倍。
3.1 设计游戏数据表结构
假设我们正在开发一个简单的RPG游戏,我们需要存储玩家基础信息、物品库存和游戏进度。我们可以先设计三张表:
players (玩家表):
id (INTEGER PRIMARY KEY AUTOINCREMENT): 主键,唯一标识一个玩家。name (TEXT NOT NULL): 玩家角色名。level (INTEGER DEFAULT 1): 玩家等级。experience (INTEGER DEFAULT 0): 当前经验值。created_at (TEXT DEFAULT CURRENT_TIMESTAMP): 创建时间,SQLite可以用TEXT类型存储ISO8601格式的时间字符串。
inventory (背包表):
id (INTEGER PRIMARY KEY AUTOINCREMENT): 主键。player_id (INTEGER NOT NULL): 关联的玩家ID,外键。item_id (INTEGER NOT NULL): 物品ID(对应一个物品配置表,这里简化为直接存)。quantity (INTEGER DEFAULT 1): 物品数量。FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE: 外键约束,当玩家被删除时,其所有背包物品也自动删除。
game_state (游戏状态表):
id (INTEGER PRIMARY KEY): 这里我们可以设计为只存一条记录,所以主键固定为1。current_scene (TEXT): 最后保存的场景路径。play_time_seconds (INTEGER DEFAULT 0): 累计游戏时间(秒)。last_saved (TEXT DEFAULT CURRENT_TIMESTAMP): 最后保存时间。
3.2 创建对应的C#数据模型类
在C#中,我们创建对应的类来映射这些表结构。这被称为“贫血模型”,虽然简单,但对于游戏开发来说清晰高效。我们在项目中创建Models文件夹,并在其中创建类文件。
Player.cs:
public class Player { public int Id { get; set; } public string Name { get; set; } = string.Empty; public int Level { get; set; } = 1; public int Experience { get; set; } = 0; public string CreatedAt { get; set; } = string.Empty; }InventoryItem.cs:
public class InventoryItem { public int Id { get; set; } public int PlayerId { get; set; } public int ItemId { get; set; } public int Quantity { get; set; } = 1; }GameState.cs:
public class GameState { public int Id { get; set; } = 1; // 固定为1 public string CurrentScene { get; set; } = string.Empty; public int PlayTimeSeconds { get; set; } = 0; public string LastSaved { get; set; } = string.Empty; }实操心得:将数据库表映射为C#类,不仅让代码更清晰,还能利用C#的强类型特性,减少运行时错误。所有属性都设置默认值是个好习惯,可以避免在构造新对象时出现空引用异常。时间字段用
string类型是因为SQLite的日期函数处理TEXT格式最为方便和可靠。
4. 核心数据库管理类(DatabaseManager)实现
这是整个数据持久化系统的中枢。我们将创建一个单例类DatabaseManager来封装所有数据库操作,确保全局只有一个数据库连接,并管理其生命周期。
4.1 初始化与连接管理
首先,我们定义数据库文件路径并建立连接。
using Godot; using System.Data.SQLite; using System.IO; public partial class DatabaseManager : Node { // 单例实例 private static DatabaseManager _instance; public static DatabaseManager Instance => _instance; private SQLiteConnection _connection; private string _databasePath; public override void _Ready() { // 简单的单例模式,确保场景中只有一个DatabaseManager if (_instance != null && _instance != this) { QueueFree(); // 销毁多余的实例 return; } _instance = this; InitializeDatabase(); } private void InitializeDatabase() { // 确定数据库文件路径。使用`user://`目录保证跨平台兼容性(可写)。 _databasePath = ProjectSettings.GlobalizePath("user://game_data.db"); GD.Print($"数据库路径: {_databasePath}"); // 创建连接字符串 string connectionString = $"Data Source={_databasePath};Version=3;"; try { _connection = new SQLiteConnection(connectionString); _connection.Open(); GD.Print("数据库连接成功。"); // 创建表 CreateTables(); } catch (SQLiteException ex) { GD.PrintErr($"数据库连接或创建失败: {ex.Message}"); // 在实际游戏中,这里应该有更友好的错误处理,比如提示玩家。 } } private void CreateTables() { string createPlayersTable = @" CREATE TABLE IF NOT EXISTS players ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, level INTEGER DEFAULT 1, experience INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP );"; string createInventoryTable = @" CREATE TABLE IF NOT EXISTS inventory ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL, item_id INTEGER NOT NULL, quantity INTEGER DEFAULT 1, FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE );"; string createGameStateTable = @" CREATE TABLE IF NOT EXISTS game_state ( id INTEGER PRIMARY KEY CHECK (id = 1), current_scene TEXT, play_time_seconds INTEGER DEFAULT 0, last_saved TEXT DEFAULT CURRENT_TIMESTAMP );"; ExecuteNonQuery(createPlayersTable); ExecuteNonQuery(createInventoryTable); ExecuteNonQuery(createGameStateTable); // 初始化game_state表,如果它是空的 string initGameState = "INSERT OR IGNORE INTO game_state (id) VALUES (1);"; ExecuteNonQuery(initGameState); GD.Print("数据表检查/创建完成。"); } }关键点解析:
- 路径选择:使用
user://协议是Godot的跨平台最佳实践。它指向每个平台特定的、有写入权限的用户数据目录(如Windows的AppData, macOS的Application Support)。绝对不要将数据库文件放在res://目录下,因为该目录在游戏发布后是只读的。CREATE TABLE IF NOT EXISTS:这个语句是幂等的,无论执行多少次都不会出错,非常适合放在初始化逻辑里。- 外键与约束:在
inventory表中定义FOREIGN KEY并设置ON DELETE CASCADE,可以保证数据的一致性。当删除一个玩家时,SQLite会自动删除其所有背包物品,无需我们手动处理。- 单例模式:通过静态
Instance属性提供全局访问点,方便游戏内任何脚本调用数据库功能。在_Ready中处理重复实例的销毁。
4.2 封装基础SQL执行方法
接下来,在DatabaseManager类中添加几个核心的、通用的数据库操作方法。
// 执行不返回结果的SQL命令(如INSERT, UPDATE, DELETE, CREATE) public int ExecuteNonQuery(string sql, Dictionary<string, object> parameters = null) { using (var command = new SQLiteCommand(sql, _connection)) { if (parameters != null) { foreach (var param in parameters) { command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); } } return command.ExecuteNonQuery(); } } // 执行返回单个值的SQL查询(如SELECT COUNT(*), SELECT name FROM ... WHERE id=1) public object ExecuteScalar(string sql, Dictionary<string, object> parameters = null) { using (var command = new SQLiteCommand(sql, _connection)) { if (parameters != null) { foreach (var param in parameters) { command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); } } return command.ExecuteScalar(); } } // 执行返回多行结果的SQL查询,并使用委托处理每一行 public void ExecuteReader(string sql, Action<SQLiteDataReader> rowProcessor, Dictionary<string, object> parameters = null) { using (var command = new SQLiteCommand(sql, _connection)) { if (parameters != null) { foreach (var param in parameters) { command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); } } using (var reader = command.ExecuteReader()) { while (reader.Read()) { rowProcessor(reader); } } } }为什么这样封装?
ExecuteNonQuery:用于修改数据或结构,返回受影响的行数。例如,创建角色、更新等级。ExecuteScalar:用于获取聚合结果或单个字段,非常高效。例如,检查用户名是否存在、获取玩家总数。ExecuteReader:这是最灵活的方法。它通过一个Action<SQLiteDataReader>委托,允许调用者自定义如何处理查询返回的每一行数据。我们将用它来实现所有的数据查询功能。- 参数化查询:所有方法都支持
Dictionary<string, object>参数。这是防止SQL注入攻击的关键!永远不要用字符串拼接的方式构造SQL语句。通过AddWithValue方法,SQLite库会安全地处理参数值,确保用户输入不会被误执行为SQL代码。
5. 游戏业务逻辑与数据库的交互实战
有了强大的DatabaseManager,我们就可以为游戏中的各种功能编写清晰的数据访问代码了。我们在DatabaseManager类中继续添加以下方法。
5.1 玩家角色管理
// 创建新玩家 public int CreatePlayer(string playerName) { string sql = "INSERT INTO players (name) VALUES (@name); SELECT last_insert_rowid();"; var parameters = new Dictionary<string, object> { { "@name", playerName } }; // last_insert_rowid() 返回刚插入行的自增ID var newId = ExecuteScalar(sql, parameters); return Convert.ToInt32(newId); } // 根据ID获取玩家信息 public Player GetPlayerById(int playerId) { Player player = null; string sql = "SELECT * FROM players WHERE id = @id LIMIT 1;"; var parameters = new Dictionary<string, object> { { "@id", playerId } }; ExecuteReader(sql, (reader) => { player = new Player { Id = reader.GetInt32(0), Name = reader.GetString(1), Level = reader.GetInt32(2), Experience = reader.GetInt32(3), CreatedAt = reader.GetString(4) }; }, parameters); return player; // 如果没找到,返回null } // 更新玩家经验与等级 public void UpdatePlayerExperience(int playerId, int expGained) { // 1. 先获取当前玩家数据 var player = GetPlayerById(playerId); if (player == null) return; // 2. 计算新经验值(这里可以加入升级逻辑) int newExp = player.Experience + expGained; int newLevel = player.Level; // 假设每1000经验升一级 while (newExp >= newLevel * 1000) { newExp -= newLevel * 1000; newLevel++; } // 3. 更新数据库 string sql = "UPDATE players SET experience = @exp, level = @lvl WHERE id = @id;"; var parameters = new Dictionary<string, object> { { "@exp", newExp }, { "@lvl", newLevel }, { "@id", playerId } }; ExecuteNonQuery(sql, parameters); GD.Print($"玩家 {playerId} 经验更新: 等级{player.Level}->{newLevel}, 经验{player.Experience}->{newExp}"); }5.2 背包系统数据操作
// 向玩家背包添加物品(如果已存在则增加数量) public void AddItemToInventory(int playerId, int itemId, int quantity = 1) { // 首先检查是否已有该物品 string checkSql = "SELECT id, quantity FROM inventory WHERE player_id = @pid AND item_id = @iid;"; var checkParams = new Dictionary<string, object> { { "@pid", playerId }, { "@iid", itemId } }; int existingId = -1; int existingQty = 0; ExecuteReader(checkSql, (reader) => { existingId = reader.GetInt32(0); existingQty = reader.GetInt32(1); }, checkParams); if (existingId > 0) { // 更新数量 string updateSql = "UPDATE inventory SET quantity = @qty WHERE id = @id;"; var updateParams = new Dictionary<string, object> { { "@qty", existingQty + quantity }, { "@id", existingId } }; ExecuteNonQuery(updateSql, updateParams); } else { // 插入新记录 string insertSql = "INSERT INTO inventory (player_id, item_id, quantity) VALUES (@pid, @iid, @qty);"; var insertParams = new Dictionary<string, object> { { "@pid", playerId }, { "@iid", itemId }, { "@qty", quantity } }; ExecuteNonQuery(insertSql, insertParams); } } // 获取玩家所有背包物品 public List<InventoryItem> GetPlayerInventory(int playerId) { var inventory = new List<InventoryItem>(); string sql = "SELECT * FROM inventory WHERE player_id = @pid;"; var parameters = new Dictionary<string, object> { { "@pid", playerId } }; ExecuteReader(sql, (reader) => { inventory.Add(new InventoryItem { Id = reader.GetInt32(0), PlayerId = reader.GetInt32(1), ItemId = reader.GetInt32(2), Quantity = reader.GetInt32(3) }); }, parameters); return inventory; }5.3 游戏状态保存与加载
// 保存游戏状态(如场景、游戏时间) public void SaveGameState(string currentScenePath, int playTimeSeconds) { string sql = @" UPDATE game_state SET current_scene = @scene, play_time_seconds = @time, last_saved = CURRENT_TIMESTAMP WHERE id = 1;"; var parameters = new Dictionary<string, object> { { "@scene", currentScenePath }, { "@time", playTimeSeconds } }; ExecuteNonQuery(sql, parameters); GD.Print($"游戏状态已保存: 场景[{currentScenePath}], 时间[{playTimeSeconds}s]"); } // 加载游戏状态 public GameState LoadGameState() { GameState state = new GameState(); string sql = "SELECT * FROM game_state WHERE id = 1;"; ExecuteReader(sql, (reader) => { state.Id = reader.GetInt32(0); // 注意GetString可能返回DBNull,需要处理 state.CurrentScene = reader.IsDBNull(1) ? string.Empty : reader.GetString(1); state.PlayTimeSeconds = reader.GetInt32(2); state.LastSaved = reader.GetString(3); }); return state; }6. 在Godot场景中集成与调用
现在,我们有了完整的数据库后端。接下来就是在游戏场景中实际使用它。
将DatabaseManager添加到自动加载(AutoLoad):
- 这是最推荐的方式。将
DatabaseManager.cs脚本附加到一个Node上(比如一个Node节点),然后将这个场景保存为DatabaseManager.tscn。 - 打开“项目(Project)” -> “项目设置(Project Settings)” -> “自动加载(AutoLoad)”。
- 将
DatabaseManager.tscn添加进来,并设置一个名称(如DB)。这样,游戏一启动,这个管理器就全局可用,在任何脚本中都可以通过DB这个名称访问DatabaseManager.Instance。
- 这是最推荐的方式。将
在UI场景中调用: 假设我们有一个创建角色的界面,上面有一个
LineEdit用于输入名字和一个Button。CreatePlayerUI.cs:using Godot; public partial class CreatePlayerUI : Control { [Export] public LineEdit NameInput; [Export] public Button ConfirmButton; public override void _Ready() { ConfirmButton.Pressed += OnConfirmButtonPressed; } private void OnConfirmButtonPressed() { string playerName = NameInput.Text.Trim(); if (string.IsNullOrEmpty(playerName)) { GD.Print("玩家名不能为空!"); return; } // 通过自动加载的单例访问数据库 int newPlayerId = DatabaseManager.Instance.CreatePlayer(playerName); if (newPlayerId > 0) { GD.Print($"玩家创建成功!ID: {newPlayerId}"); // 这里可以触发场景切换,加载游戏主界面等 // 例如:GetTree().ChangeSceneToFile("res://MainWorld.tscn"); } else { GD.PrintErr("玩家创建失败!"); } } }在游戏主循环中保存状态: 可以在游戏主场景的
_Process或_PhysicsProcess中定期保存,或者在切换场景、退出游戏时保存。MainGame.cs:using Godot; public partial class MainGame : Node2D { private float _playTimer = 0.0f; private const float SAVE_INTERVAL = 60.0f; // 每60秒自动保存一次 public override void _Process(double delta) { _playTimer += (float)delta; if (_playTimer >= SAVE_INTERVAL) { _playTimer = 0; AutoSave(); } } private void AutoSave() { string currentScene = GetTree().CurrentScene.SceneFilePath; int totalPlayTime = // ... 从某个计时器获取总游戏时间 DatabaseManager.Instance.SaveGameState(currentScene, totalPlayTime); } // 当玩家退出游戏时 private void OnExitButtonPressed() { AutoSave(); // 退出前保存一次 GetTree().Quit(); } }
7. 性能优化、错误处理与实战避坑指南
将基础功能跑通只是第一步,要让它在实际项目中稳定运行,还需要注意以下几点。
7.1 性能优化要点
连接池与单例:我们已经使用了单例模式,整个游戏生命周期只维持一个数据库连接(
SQLiteConnection),这本身就是一种高效的连接管理。SQLite是文件数据库,频繁打开关闭连接开销很大。事务(Transaction):当你需要执行一系列关联的写操作(比如,完成任务需要同时更新玩家经验、移除任务物品、发放奖励物品)时,务必使用事务。这能保证数据的一致性,并且由于SQLite在事务内会将多次写入合并,能极大提升性能(有时可达数十倍)。
public void CompleteQuest(int playerId, int questId) { // 假设这是一个复杂操作 using (var transaction = _connection.BeginTransaction()) { try { // 1. 更新任务状态 ExecuteNonQuery("UPDATE quests SET completed=1 WHERE id=@qid AND player_id=@pid", ...); // 2. 增加经验 UpdatePlayerExperience(playerId, 500); // 3. 发放奖励物品 AddItemToInventory(playerId, 123, 1); // 4. 扣除任务物品 RemoveItemFromInventory(playerId, 456, 1); transaction.Commit(); // 所有操作成功,提交事务 GD.Print("任务完成!"); } catch (Exception ex) { transaction.Rollback(); // 任何一步失败,回滚所有操作 GD.PrintErr($"任务完成失败,已回滚: {ex.Message}"); throw; // 可以选择向上抛出异常 } } }索引优化:对于经常用于查询条件的字段(如
players.name,inventory.player_id),创建索引可以大幅加快查询速度。可以在CreateTables方法中添加:ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_inventory_player ON inventory(player_id);");
7.2 全面的错误处理与日志
我们的基础方法已经使用了try-catch,但还需要更细致。
- 连接失败:可能是磁盘已满、文件权限问题。应给玩家明确的提示,而不是让游戏崩溃。
- SQL语法错误:通常是开发时的笔误,通过详细的日志(输出执行的SQL和参数)来定位。
- 约束违反:比如插入了重复的主键,或外键关联不存在。这属于业务逻辑错误,应在执行前做好检查。
- 建议:将
DatabaseManager中的核心方法(如ExecuteNonQuery)的catch块完善,不仅打印错误到控制台,还可以触发一个自定义的“数据库错误”信号,让UI层弹出错误提示框。
7.3 常见问题与排查技巧实录
以下是我在实际开发中遇到过的典型问题及解决方案:
“无法加载DLL ‘SQLite.Interop.dll’” 错误
- 现象:游戏在开发环境运行正常,但导出后(尤其是Windows平台)崩溃,报错找不到SQLite的本地依赖库。
- 原因:
System.Data.SQLite.Core包包含针对x86/x64的原生库,导出时可能没有正确包含。 - 解决:在导出项目时,确保在“导出设置(Export Settings)”的“资源(Resources)”选项卡中,将你的数据库文件(
.db)和SQLite.Interop.dll(通常位于项目bin目录下的x86和x64子文件夹中)添加为“导出文件(Export Files)”,并确保它们被复制到导出目录的正确位置。一个更稳妥的方法是使用纯托管的SQLite实现,如Microsoft.Data.Sqlite,但需要确认其与Godot插件的兼容性。
数据库文件被锁,无法写入
- 现象:在编辑器里快速反复运行游戏测试时,偶尔会出现数据库访问错误,提示文件被锁定。
- 原因:上一次游戏进程可能没有正常关闭,连接没有释放,导致文件句柄被占用。
- 解决:
- 在
DatabaseManager的_ExitTree或_Notification(NOTIFICATION_WM_CLOSE_REQUEST)方法中,显式地关闭和释放数据库连接(_connection.Close(); _connection.Dispose();)。 - 在开发时,如果遇到此问题,可以手动关闭Godot编辑器再重新打开,或者使用DB Browser for SQLite工具检查并关闭可能存在的连接。
- 在
查询结果为空时的处理
- 现象:
GetPlayerById返回null,但在调用其属性时导致空引用异常。 - 解决:这是良好的编程习惯。在所有返回可能为
null的对象的方法中,调用方必须进行空值检查。例如:var player = DB.GetPlayerById(selectedId); if (player != null) { // 安全地使用player.Name等属性 } else { GD.Print("未找到指定玩家!"); }
- 现象:
数据迁移(版本升级)
- 场景:游戏发布后,需要为数据库增加新字段或新表。
- 方案:绝对不能直接
DROP TABLE再CREATE,那会丢失所有用户数据!标准做法是引入一个“版本号”概念。- 在数据库中创建一个
version_info表,记录当前数据库版本(如1)。 - 在
InitializeDatabase中,读取当前版本号。 - 编写一系列“升级脚本”(如
UpgradeFromV1ToV2.sql),用ALTER TABLE ADD COLUMN等语句增量修改。 - 根据当前版本与目标版本的差异,按顺序执行这些升级脚本,并更新
version_info表中的版本号。
- 在数据库中创建一个
将SQLite集成到Godot C#项目中,为你的游戏提供了一个强大、可靠且高效的数据存储后端。它超越了简单的文件存储,让你能够以结构化的方式管理复杂数据,并通过SQL获得强大的查询能力。从环境搭建、设计建模、封装通用层,到业务集成和错误处理,这套流程经过实践检验,能够支撑起中小型游戏的数据持久化需求。关键在于理解每一步背后的“为什么”,并养成良好的习惯:使用参数化查询防注入、利用事务保证一致性、做好错误处理提升健壮性。最后,别忘了DB Browser for SQLite这个可视化工具,它在你调试和验证数据时,将是不可或缺的帮手。