Godot 4 C#开发中SQLite数据库集成实战:从依赖管理到多线程优化的完整避坑指南
1. 项目概述为什么GodotC#SQLite的组合会“踩坑”如果你正在用Godot 4的C#开发游戏并且想把玩家存档、配置或者游戏内的一些结构化数据比如背包物品、任务进度存到本地SQLite几乎是个绕不开的选择。它轻量、单文件、无需额外服务听起来是绝配。但当你兴冲冲地打开NuGet准备安装System.Data.SQLite或者直接在Godot里写连接字符串时一系列“诡异”的问题可能就找上门了。这些问题之所以“诡异”是因为它们往往不是纯粹的C#或SQLite问题而是Godot独特的运行时环境、资源管理机制与传统的.NET库碰撞后产生的“化学反应”。我自己在最近一个2D Roguelike项目里就深有体会。我需要一个本地数据库来存储玩家解锁的角色、收集的符文和全局游戏统计。一开始我以为这会是整个项目里最简单的一环结果光是让SQLite在Godot里跑起来就花了我整整两天时间排查各种报错和异常行为。从“DLL找不到”到“数据库文件被锁定”再到多线程下的崩溃每一个坑都踩得结结实实。所以这篇指南不是教科书式的API讲解而是我作为一个踩坑者把那些官方文档不会写、搜索引擎需要翻好几页才能找到答案的“实战陷阱”和解决方案系统地梳理给你。无论你是想存个简单的配置还是构建复杂的数据层提前了解这些能省下你大量的调试时间。2. 问题一找不到SQLite的本地依赖库DLL报错“Unable to load DLL ‘SQLite.Interop.dll’”这是几乎所有Godot C#开发者连接SQLite时遇到的第一个也是最经典的拦路虎。你在Visual Studio或Rider里调试得好好的一打包成Godot项目运行立刻就抛出DllNotFoundException。2.1 问题根源Godot的独特打包与运行时路径在标准的.NET Core/控制台应用里NuGet包管理器比如安装Microsoft.Data.Sqlite或System.Data.SQLite.Core会自动处理本地依赖库Native Library的复制和加载。这些SQLite.Interop.dll文件会根据你的运行平台x86, x64, arm64被放到runtimes目录下运行时CLR能正确找到它们。然而Godot的C#项目结构和工作流打破了这一惯例输出目录不同Godot期望所有的程序集.dll都位于项目根目录下的.godot/mono/temp/bin/Debug或Release或最终的导出路径中。NuGet包默认的复制行为可能无法将本机库正确部署到Godot期望的位置。运行时探测路径.NET运行时在Godot环境下的“应用程序基目录”和“私有二进制路径”可能与你的项目目录不一致导致它找不到放在runtimes子目录下的SQLite.Interop.dll。2.2 解决方案手动确保本机库就位这里提供两种经过验证的可靠方案我推荐方案二。方案一使用纯托管的SQLite实现——Microsoft.Data.Sqlite SQLitePCLRaw.bundle_e_sqlite3这是目前最省心、兼容性最好的方案也是我最终采用的。安装NuGet包在你的C#类库项目或Godot项目的.csproj文件中通过NuGet安装以下两个包PackageReference IncludeMicrosoft.Data.Sqlite Version8.0.0 / PackageReference IncludeSQLitePCLRaw.bundle_e_sqlite3 Version2.1.8 /SQLitePCLRaw.bundle_e_sqlite3这个包是关键。它包含了预编译的、跨平台Windows, macOS, Linux, 甚至移动端的SQLite本地库并将它们作为托管资源嵌入到程序集内部。运行时无需外部DLL文件直接从资源中加载。代码示例using Microsoft.Data.Sqlite; using Godot; public class DatabaseManager { private string _dbPath; private SqliteConnection _connection; public void Initialize(string dbName) { // Godot中用户数据路径是跨平台安全的 _dbPath ProjectSettings.GlobalizePath($user://{dbName}.db); var connectionString new SqliteConnectionStringBuilder { DataSource _dbPath, Mode SqliteOpenMode.ReadWriteCreate // 允许读写和创建 }.ToString(); _connection new SqliteConnection(connectionString); _connection.Open(); // 初始化表结构... } }注意务必使用ProjectSettings.GlobalizePath来处理路径。user://是Godot的虚拟文件系统路径指向跨平台且可写的用户数据目录。直接使用user://字符串在SqliteConnection中可能无法被正确识别。方案二手动复制System.Data.SQLite.Core的Interop DLL适用于遗留项目如果你的项目已经深度依赖System.Data.SQLite可以尝试此方案。安装NuGet包安装System.Data.SQLite.Core。它不包含本机库需要你手动处理。定位并复制DLL在NuGet包缓存目录通常位于%USERPROFILE%\.nuget\packages\system.data.sqlite.core\version\runtimes找到对应平台的SQLite.Interop.dll。你需要根据Godot编辑器的运行平台不是目标游戏平台和你的项目输出平台通常是x64来选择。例如在Windows x64上开发就找runtimes\win-x64\native\SQLite.Interop.dll。关键步骤将这个SQLite.Interop.dll复制到你的Godot项目根目录并重命名为libsqlite3Linux/macOS或保持SQLite.Interop.dll但确保它在输出目录同级。对于Godot更可靠的做法是在项目.csproj中添加构建后事件将其复制到输出目录的根层。ItemGroup Content Include..\path\to\SQLite.Interop.dll LinkSQLite.Interop.dll CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /Content /ItemGroup设置连接代码确保连接字符串正确。using System.Data.SQLite; // ... 初始化路径 var connectionString $Data Source{_dbPath};Version3;; var connection new SQLiteConnection(connectionString); connection.Open();实操心得首选Microsoft.Data.Sqlitebundle_e_sqlite3组合。这是微软官方维护的、面向现代.NET的库与.NET生态集成更好且无外部依赖问题一次配置全平台无忧。如果你在Godot编辑器里运行正常但导出项目后依然报错请检查导出模板是否包含了所有必要的本地库。对于方案二你需要确保SQLite.Interop.dll被包含在导出文件的同级目录中这可能需要修改导出预设手动添加该文件。可以使用Environment.CurrentDirectory或AppDomain.CurrentDomain.BaseDirectory在运行时打印一下当前目录帮助你判断DLL应该放在哪里。3. 问题二数据库文件被锁定无法写入或“database is locked”你可能会在尝试执行INSERT、UPDATE或创建事务时遇到这个错误。尤其是在游戏运行时频繁进行自动存档或者从多个地方比如不同的场景脚本访问同一个数据库连接时。3.1 问题根源连接未妥善管理与Godot的场景生命周期SQLite是一个文件数据库当有一个连接SqliteConnection以写入模式打开数据库文件时它会持有该文件的锁。在Godot中常见的错误模式有连接泄露在某个节点的_Ready()方法中打开连接但节点被移除或场景切换时没有在_ExitTree()或Dispose()中关闭连接。这个连接对象可能依然存活持有文件锁。单例管理不当你创建了一个全局的DatabaseManager单例来管理连接但在游戏整个生命周期中只打开一次连接。如果这个连接被多个线程哪怕是主线程的不同协程同时用于执行命令而SQLite的默认连接不是线程安全的就可能引发锁冲突。未使用事务的批量操作频繁地进行单条INSERT操作每条都涉及文件的打开、写入、关闭或锁的获取/释放在高频操作下极易发生锁竞争。3.2 解决方案采用连接池与妥善的生命周期管理核心原则及时关闭连接或使用单个线程安全的连接。方案一按需打开及时关闭最简单对于不频繁的数据库操作如加载存档、保存设置这是最安全的方式。public void SavePlayerData(PlayerData data) { // 每次都创建新的连接字符串和连接 var connectionString new SqliteConnectionStringBuilder { DataSource _dbPath, Mode SqliteOpenMode.ReadWrite }.ToString(); using (var connection new SqliteConnection(connectionString)) { connection.Open(); using (var transaction connection.BeginTransaction()) { try { // 执行你的INSERT/UPDATE命令 var command connection.CreateCommand(); command.CommandText UPDATE player SET goldgold WHERE id1; command.Parameters.AddWithValue(gold, data.Gold); command.ExecuteNonQuery(); transaction.Commit(); // 提交事务 } catch (Exception ex) { transaction.Rollback(); // 出错回滚 GD.PrintErr($Save failed: {ex.Message}); throw; } } } // using语句结束时connection会自动Dispose并关闭 }注意using语句确保了即使发生异常连接也会被正确关闭和释放。将多个相关操作包裹在一个事务中可以大幅提升批量写入的性能和原子性。方案二单例线程安全连接适用于高频操作如果你需要在游戏运行时高频访问数据库例如实时记录数据可以维护一个全局的、线程安全的连接。// 一个简单的线程安全数据库管理器 public class DatabaseManager : IDisposable { private static DatabaseManager _instance; private static readonly object _lock new object(); private SqliteConnection _connection; private bool _disposed false; private DatabaseManager() { } public static DatabaseManager Instance { get { lock (_lock) { return _instance ?? new DatabaseManager(); } } } public void Initialize(string dbPath) { lock (_lock) { if (_connection ! null) return; var csb new SqliteConnectionStringBuilder { DataSource dbPath, Mode SqliteOpenMode.ReadWriteCreate, // 启用连接池对于Sqlite更多是连接复用和设置繁忙超时 Pooling true, // 设置繁忙时等待时间毫秒 // 在Microsoft.Data.Sqlite中需在打开后执行命令 }; _connection new SqliteConnection(csb.ToString()); _connection.Open(); // 设置繁忙超时 var setBusyTimeoutCmd _connection.CreateCommand(); setBusyTimeoutCmd.CommandText PRAGMA busy_timeout 5000;; // 等待5秒 setBusyTimeoutCmd.ExecuteNonQuery(); } } public SqliteConnection GetConnection() { // 注意返回的连接本身不是线程安全的。 // 更好的做法是提供一个执行方法内部加锁。 lock (_lock) { if (_connection.State ! System.Data.ConnectionState.Open) _connection.Open(); // 重连逻辑 return _connection; // 谨慎直接返回连接需调用者保证序列化访问 } } // 推荐提供线程安全的执行方法 public int ExecuteNonQuery(string sql, params SqliteParameter[] parameters) { lock (_lock) { using (var cmd _connection.CreateCommand()) { cmd.CommandText sql; cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } public void Dispose() { lock (_lock) { if (!_disposed) { _connection?.Close(); _connection?.Dispose(); _disposed true; } } } } // 在Godot的Autoload单例中初始化并管理生命周期 public partial class GameGlobal : Node { public override void _Ready() { var dbMgr DatabaseManager.Instance; string path ProjectSettings.GlobalizePath(user://game.db); dbMgr.Initialize(path); } public override void _Notification(int what) { // 当游戏退出时释放数据库连接 if (what NotificationWMCloseRequest) { DatabaseManager.Instance.Dispose(); } } }实操心得事务是你的朋友任何超过一条的写入操作务必放在事务中。这不仅能保证数据一致性还能将多次磁盘I/O合并极大提升性能减少锁持有时间。谨慎使用全局连接方案二中的全局连接虽然方便但你必须确保所有数据库访问都是序列化的通过lock。在Godot中如果你使用了异步async/await或多线程Thread竞争条件会变得更加复杂。对于大多数游戏方案一的“短连接”模式更安全。设置busy_timeout通过执行PRAGMA busy_timeout 5000;命令可以告诉SQLite在遇到锁时等待最多5秒而不是立即报错“database is locked”。这能缓解瞬时并发。检查WAL模式考虑启用WALWrite-Ahead Logging模式PRAGMA journal_modeWAL;。WAL模式允许读和写并发进行能显著改善多线程读写的锁竞争情况。但请注意WAL模式在Godot的某些移动端或Web导出目标上可能受限。4. 问题三跨平台路径问题与“找不到数据库文件”在Windows上运行得好好的导出到Android或Linux后游戏提示无法创建或打开数据库文件。错误信息可能是“Unable to open database file”。4.1 问题根源Godot的沙盒文件系统与平台路径差异不同操作系统有不同的文件系统路径规则如Windows用\Unix用/而移动平台iOS/Android有严格的沙盒限制你不能随意在任意路径读写文件。绝对路径硬编码在代码中写了类似C:\Users\Name\game.db的路径在其他平台当然失效。使用相对路径./game.db这个“当前目录”在打包后的游戏中是不确定的很可能没有写入权限。未使用Godot提供的安全路径Godot通过user://和res://等前缀抽象了跨平台的文件访问。4.2 解决方案统一使用Godot的ProjectSettings进行路径解析黄金法则永远使用ProjectSettings.GlobalizePath()来处理涉及文件系统的路径。using Godot; using Microsoft.Data.Sqlite; public class CrossPlatformDbHelper { // 存储用户数据的数据库可读写 public static string GetUserDatabasePath(string dbFileName) { // 将 user://saves/game.db 转换为当前系统的绝对路径 // 例如在Windows上可能是 C:\Users\[用户名]\AppData\Roaming\Godot\app_userdata\[项目名]\saves\game.db // 在Android上则是应用的内部存储空间 string userDirPath user://saves/; // 确保目录存在 using var dir DirAccess.Open(user://); if (dir ! null) { dir.MakeDirRecursive(saves); } string fullUserPath ProjectSettings.GlobalizePath(userDirPath dbFileName); return fullUserPath; } // 读取只读资源数据库打包在exe旁或内部 public static string GetResourceDatabasePath(string dbFileName) { // res:// 指向项目资源目录导出后只读 string resPath $res://data/{dbFileName}; string globalizedPath ProjectSettings.GlobalizePath(resPath); // 注意导出后res:// 可能无法直接用于需要写入的SQLite连接。 // 通常做法是如果user://下没有则从res://复制一份到user:// return globalizedPath; } public void InitializeDatabase() { string targetDbPath GetUserDatabasePath(mygame.db); var sourceDbPath GetResourceDatabasePath(default.db); // 检查用户数据目录下是否已有数据库没有则从资源中复制初始数据库 using var dir DirAccess.Open(user://saves/); if (dir ! null !dir.FileExists(mygame.db)) { using var resDir DirAccess.Open(res://data/); if (resDir ! null resDir.FileExists(default.db)) { DirAccess.CopyAbsolute(sourceDbPath, targetDbPath); } } var connectionString new SqliteConnectionStringBuilder { DataSource targetDbPath, // 使用转换后的绝对路径 Mode SqliteOpenMode.ReadWriteCreate }.ToString(); using var connection new SqliteConnection(connectionString); connection.Open(); // ... 后续操作 } }实操心得user://是可写的用于存档、配置、下载内容等。这是你存放SQLite数据库文件的首选位置。res://是只读的用于打包在游戏内的初始资源。你不能直接打开res://路径的数据库进行写入。常见的模式是将一个初始的、只读的数据库放在res://游戏第一次运行时将其复制到user://然后所有读写操作都在user://的副本上进行。路径分隔符使用Path.Combine()或直接使用/Godot和现代.NET都能正确处理跨平台路径分隔符。避免使用\。导出后测试务必在目标平台如Android的导出模板上测试数据库的创建和读写功能。在编辑器里运行使用的是开发环境路径与最终发布环境不同。5. 问题四异步操作与多线程下的连接崩溃你为了不阻塞主线程游戏画面尝试在async方法或Thread中执行数据库查询。结果游戏时不时卡死、崩溃或者出现一些难以复现的数据错乱。5.1 问题根源SQLite连接的非线程安全性与Godot节点的单线程特性SQLite连接的非线程安全一个SqliteConnection对象在其生命周期内默认不能被多个线程同时使用。即使你用了lock如果在A线程打开了连接然后在B线程执行命令也可能出现问题。许多SQLite驱动要求连接在哪个线程打开就在哪个线程关闭和操作。Godot节点的线程限制Godot的视觉节点Node及其相关API如GD.Print、修改Label.Text不是线程安全的。你无法在子线程中直接更新UI或访问某些引擎属性。.NET异步与线程池async/await默认会在线程池线程上恢复执行除非使用了特定的同步上下文如UI线程。这意味着一个看似连续的async方法其await前后可能运行在不同的线程上。5.2 解决方案严格的线程隔离与任务队列核心策略将数据库操作限制在单一后台线程或使用线程安全的连接池并通过回调或信号将结果传回主线程。方案一专用后台线程 阻塞队列生产者-消费者模式这是处理高频、复杂数据库操作的稳健模式。using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Godot; public class ThreadSafeDatabaseService : Node { private Thread _dbThread; private CancellationTokenSource _cts; private BlockingCollectionDbCommand _commandQueue new BlockingCollectionDbCommand(); private class DbCommand { public string Sql { get; set; } public SqliteParameter[] Parameters { get; set; } public Actionobject Callback { get; set; } // 结果回调在主线程执行 } public override void _Ready() { _cts new CancellationTokenSource(); _dbThread new Thread(DatabaseWorkerThread); _dbThread.IsBackground true; // 设为后台线程防止阻止进程退出 _dbThread.Start(); } public override void _ExitTree() { _cts?.Cancel(); _commandQueue?.CompleteAdding(); // 停止添加新命令 _dbThread?.Join(1000); // 等待线程结束最多1秒 _cts?.Dispose(); } // 供外部调用的方法在主线程 public void ExecuteQuery(string sql, Actionobject callback, params SqliteParameter[] parameters) { var cmd new DbCommand { Sql sql, Parameters parameters, Callback callback }; _commandQueue.Add(cmd); } // 工作线程方法 private void DatabaseWorkerThread() { // 在线程内创建和使用连接 string dbPath ProjectSettings.GlobalizePath(user://game.db); var connectionString new SqliteConnectionStringBuilder { DataSource dbPath }.ToString(); using (var connection new SqliteConnection(connectionString)) { connection.Open(); while (!_cts.Token.IsCancellationRequested) { try { // 阻塞直到有命令或取消 DbCommand cmd _commandQueue.Take(_cts.Token); object result null; try { using (var sqlCmd connection.CreateCommand()) { sqlCmd.CommandText cmd.Sql; if (cmd.Parameters ! null) sqlCmd.Parameters.AddRange(cmd.Parameters); if (cmd.Sql.Trim().ToUpper().StartsWith(SELECT)) { using (var reader sqlCmd.ExecuteReader()) { // 简单示例将结果读入列表 var dataTable new ListDictionarystring, object(); while (reader.Read()) { var row new Dictionarystring, object(); for (int i 0; i reader.FieldCount; i) { row[reader.GetName(i)] reader.GetValue(i); } dataTable.Add(row); } result dataTable; } } else { result sqlCmd.ExecuteNonQuery(); } } } catch (Exception ex) { GD.PrintErr($[DB Thread] Error executing command: {ex.Message}); result ex; } // 通过CallDeferred将回调调度到主线程执行 object finalResult result; CallDeferred(nameof(InvokeCallbackOnMainThread), cmd.Callback, finalResult); } catch (OperationCanceledException) { break; // 正常退出 } catch (Exception ex) { GD.PrintErr($[DB Thread] Unexpected error: {ex.Message}); } } } } // 在主线程执行的回调 private void InvokeCallbackOnMainThread(Actionobject callback, object result) { callback?.Invoke(result); } } // 使用示例 public partial class PlayerManager : Node { [Signal] public delegate void GoldUpdatedEventHandler(int newGold); public void SaveGoldAsync(int goldAmount) { var dbService GetNodeThreadSafeDatabaseService(/root/DatabaseService); var param new SqliteParameter(gold, goldAmount); dbService.ExecuteQuery( UPDATE player SET goldgold WHERE id1, (result) { // 这个回调在主线程执行可以安全更新UI if (result is Exception ex) { GD.PrintErr($Save failed: {ex.Message}); } else { GD.Print(Save successful!); EmitSignal(SignalName.GoldUpdated, goldAmount); } }, param ); } }方案二使用async/await但确保连接在线程安全上下文简化场景对于低频、简单的操作可以结合Task.Run和Godot的CallDeferred。public async TaskListItem LoadPlayerInventoryAsync(int playerId) { // 在后台线程执行数据库查询 var dataTable await Task.Run(() { // 注意在Task.Run内部创建新连接确保连接生命周期在线程内 string path ProjectSettings.GlobalizePath(user://game.db); using var connection new SqliteConnection($Data Source{path}); connection.Open(); using var cmd connection.CreateCommand(); cmd.CommandText SELECT * FROM inventory WHERE player_idpid; cmd.Parameters.AddWithValue(pid, playerId); using var reader cmd.ExecuteReader(); // ... 读取数据到列表 return inventoryList; }); // 此时已回到主线程Godot 4.2 的默认同步上下文会确保这一点但显式处理更安全 CallDeferred(nameof(ProcessInventoryDataOnMainThread), dataTable); return dataTable; } private void ProcessInventoryDataOnMainThread(ListItem items) { // 安全地更新UI或节点状态 foreach (var item in items) { // ... 更新背包UI等 } }实操心得连接生命周期与线程绑定最简单的规则是在哪个线程创建连接就在哪个线程关闭和使用它。避免跨线程传递连接对象。Godot的CallDeferred和SetDeferred这是将代码调度到主线程执行的安全方式。任何需要修改场景树、更新UI或访问非线程安全引擎API的操作都必须通过它们或Signal回到主线程。谨慎使用async/await在Godot C#中默认的同步上下文能确保await后在主线程恢复但这并非绝对例如在Task.Run内部。最安全的做法是在await之后如果涉及Godot对象用CallDeferred包装一下。考虑使用更高级的库对于复杂的数据层可以考虑使用像SQLite-net或Dapper这样的微型ORM它们能简化一些操作但线程安全问题依然需要你遵循上述原则来管理连接。6. 问题五性能瓶颈与内存泄漏——查询慢、内存增长游戏运行一段时间后感觉越来越卡或者内存占用持续上升尤其是在频繁进行数据库自动存档或日志记录时。6.1 问题根源未参数化查询、未释放资源与不当的日志策略SQL注入与编译开销使用字符串拼接来构建SQL语句如$SELECT * FROM users WHERE name{userInput}不仅存在严重的安全风险SQL注入而且每次执行时SQLite都需要重新解析和编译SQL语句带来不必要的开销。资源未释放SqliteCommand、SqliteDataReader、SqliteConnection等对象实现了IDisposable接口。如果没有使用using语句或手动调用Dispose()可能会导致数据库连接未关闭锁未释放、内存泄漏。N1查询问题在循环中执行查询。例如加载100个物品每个物品又去查一次数据库获取详细信息这就产生了101次查询。过度日志记录将每帧的调试信息如玩家位置都写入数据库会产生海量的小型写入操作严重拖慢性能并迅速增大数据库文件。6.2 解决方案优化查询模式与资源管理1. 始终使用参数化查询// 错误示范字符串拼接易注入性能差 string badSql $UPDATE player SET score {newScore} WHERE id {playerId}; // 正确示范参数化查询安全可复用执行计划 string goodSql UPDATE player SET score score WHERE id id; using var cmd connection.CreateCommand(); cmd.CommandText goodSql; cmd.Parameters.AddWithValue(score, newScore); cmd.Parameters.AddWithValue(id, playerId); cmd.ExecuteNonQuery();2. 严格使用using语句或try-finally确保资源释放// 推荐using 语句自动 Dispose public PlayerData LoadPlayer(int id) { PlayerData player null; string path ProjectSettings.GlobalizePath(user://game.db); using (var connection new SqliteConnection($Data Source{path})) { connection.Open(); using (var command connection.CreateCommand()) { command.CommandText SELECT * FROM players WHERE idid; command.Parameters.AddWithValue(id, id); using (var reader command.ExecuteReader()) // Reader 也需要释放 { if (reader.Read()) { player new PlayerData { Id reader.GetInt32(0), Name reader.GetString(1), // ... }; } } } } // 此处 connection, command, reader 都会被自动关闭和释放 return player; }3. 使用JOIN和批量操作避免N1查询// 假设要加载玩家及其所有物品 // 低效做法先查玩家再循环查物品 // 高效做法一次查询使用 JOIN string efficientSql SELECT p.*, i.* FROM players p LEFT JOIN inventory i ON p.id i.player_id WHERE p.id playerId; using var cmd connection.CreateCommand(); cmd.CommandText efficientSql; cmd.Parameters.AddWithValue(playerId, playerId); using var reader cmd.ExecuteReader(); while (reader.Read()) { // 处理玩家和物品数据... // 注意这里可能需要处理一对多关系一个玩家对应多行物品记录 }4. 对高频写入操作进行批处理和节流批量插入使用事务包裹多条INSERT。using var transaction connection.BeginTransaction(); using var cmd connection.CreateCommand(); cmd.CommandText INSERT INTO game_log (event, timestamp) VALUES (event, time); cmd.Parameters.Add(event, SqliteType.Text); cmd.Parameters.Add(time, SqliteType.Integer); cmd.Transaction transaction; foreach (var log in logEntries) { cmd.Parameters[event].Value log.Event; cmd.Parameters[time].Value log.Timestamp; cmd.ExecuteNonQuery(); // 在事务内执行 } transaction.Commit(); // 一次性提交性能提升巨大节流不要每帧都存档。可以设置一个定时器例如每10秒或者当数据发生重要变更时如玩家升级、获得重要物品再触发保存。对于调试日志考虑先写入内存缓冲区定期如每秒批量刷入数据库或者直接输出到文件。5. 定期执行PRAGMA optimize和VACUUM谨慎PRAGMA optimize;让SQLite分析数据库并优化查询计划。可以在游戏启动或关闭时执行一次。VACUUM;重建整个数据库文件回收删除数据后留下的空间并可能优化文件结构。警告这是一个重量级操作会阻塞所有访问并创建一个新的数据库文件。切勿在游戏运行时频繁执行。仅建议在版本更新或确认数据库碎片化严重时在游戏启动界面手动触发。实操心得使用.ToList()或.ToArray()时要小心如果你用Dapper或类似工具执行查询返回IEnumerableT在内存中立即通过.ToList()物化结果然后尽快关闭数据库连接和Reader。不要在遍历IEnumerable时还保持着连接打开。监控连接状态在复杂应用中可以添加简单的日志来记录连接打开和关闭的次数确保没有泄露。考虑读写分离对于读多写少的场景如读取配置、物品模板可以在游戏启动时将整个表加载到内存中的字典或列表里避免运行时频繁查询数据库。只在数据变更时写回数据库。使用性能分析工具Godot编辑器自带的性能分析器可以监控CPU和内存。如果怀疑数据库操作是瓶颈可以测量关键数据库函数调用的耗时。

相关新闻

最新新闻

日新闻

周新闻

月新闻