1. 项目概述与核心价值
最近在折腾一个Godot的跨平台小项目,需要用到本地数据存储。一开始图省事,直接用了Godot自带的
ConfigFile
或者序列化到文件,但随着数据关系稍微复杂一点,比如需要关联查询、排序分页,这些方法就有点捉襟见肘了。这时候,一个轻量级、零配置、单文件的SQLite数据库就成了最自然的选择。尤其是在移动端,SQLite几乎是内置的,兼容性极佳。
这个项目标题“【极简代码】Godot Mono版的C#脚本实现对SQLite数据库的读写及对安卓端的兼容”,可以说精准地戳中了Godot C#开发者在数据持久化上的一个核心痛点。很多从Unity转过来的朋友,习惯了用各种ORM或者直接
System.Data.SQLite
,到了Godot Mono环境下,会发现一些微妙的差异和坑。比如,如何在Godot的C#环境中正确引用和部署SQLite的库?如何确保写出的代码在Windows编辑器里运行正常,打包成安卓APK后也能完美工作?这些都不是简单地把Unity的代码搬过来就能解决的。
所以,这篇内容我就结合自己趟过的路,分享一下如何在Godot Mono(即支持C#的版本)中,用最简洁、清晰的代码,实现一个稳定可靠的SQLite数据访问层,并且重点解决安卓端的兼容性问题。目标是让你看完就能直接用到自己的项目里,避免那些常见的“编辑器里好好的,一打包就崩”的尴尬情况。无论你是想做单机游戏的存档系统、管理复杂的配置数据,还是开发一个轻量级的工具应用,这套方案都值得一试。
2. 环境准备与核心库选型
2.1 Godot Mono项目创建与基础配置
首先,确保你安装的是支持Mono(C#)的Godot版本。在创建新项目时,渲染器选择兼容性强的“Forward+”或“Mobile”通常问题较少,但更重要的是在“项目设置”里确认“.NET”设置已启用。创建后,你应该能看到一个
YourProject.csproj
文件,这是C#项目的核心。
为了让C#脚本能方便地使用SQLite,我们需要通过NuGet引入库。但这里有个关键点:
不能直接用最常见的
System.Data.SQLite
。因为这个库包含本地原生(native)组件,在Windows x64的编辑器环境下运行没问题,但直接用它打包安卓(ARM架构)会因架构不匹配而崩溃。
正确的选择是
Microsoft.Data.Sqlite
。它是一个纯 .NET Standard 2.0/2.1 实现的库,大部分情况下无需依赖原生库,跨平台兼容性非常好。通过Godot编辑器内置的“工具 -> NuGet管理器”来安装是最稳妥的方式。搜索并安装
Microsoft.Data.Sqlite
(建议选择较新的稳定版,如7.x版本)。安装后,Godot会自动将依赖写入
.csproj
文件并下载。
注意 :有些教程会提到使用
Mono.Data.Sqlite,这是Mono项目时代的遗产,在.NET Core/5+及Godot Mono的新环境下,Microsoft.Data.Sqlite是官方推荐且维护更积极的方案,对异步操作(async/await)的支持也更好。
2.2 SQLite数据库文件的位置规划
在Godot中,文件路径因平台和运行环境(编辑器 vs 导出包)而异,这是导致很多文件读写问题的根源。我们必须使用Godot提供的API来获取正确的路径。
对于SQLite数据库文件(通常以
.db
或
.sqlite
为扩展名),我们一般考虑两个位置:
-
用户数据目录 (
user://) :这是每个应用独立的、可读写的位置,适合存放运行时生成或修改的数据,如用户存档、日志。不同平台的实际路径不同(如Windows的AppData, Android的/data/data/包名/files),但user://这个抽象路径始终指向正确的位置。 -
资源目录 (
res://) :只读,存放随项目打包的初始数据。如果你想内置一个初始数据库,可以放在这里,但程序无法直接写入。
最佳实践是:程序启动时,检查
user://
目录下是否存在数据库文件,如果不存在,则从
res://
目录复制一份初始数据库到
user://
,后续所有读写操作都针对
user://
下的副本。
这样既保证了打包后初始数据的存在,又确保了应用有写入权限。
using System.IO;
using Godot;
public class DatabaseManager
{
private static string _dbPath;
public static string DbPath
{
get
{
if (string.IsNullOrEmpty(_dbPath))
{
// 定义数据库文件名
string dbFileName = "mygame.db";
// 目标路径:用户数据目录
string userDir = OS.GetUserDataDir();
_dbPath = Path.Combine(userDir, dbFileName);
GD.Print($"数据库路径: {_dbPath}");
}
return _dbPath;
}
}
public static void InitializeDatabase()
{
// 检查用户目录下是否已有数据库
if (!File.Exists(DbPath))
{
GD.Print("用户数据目录下未找到数据库,准备初始化...");
// 这里可以添加从 res:// 复制初始数据库文件的逻辑
// 例如:CopyInitialDatabase();
}
else
{
GD.Print("数据库文件已存在。");
}
}
}
2.3 安卓平台的特殊考量
对于安卓平台,除了路径,还需要注意:
-
权限
:访问
user://(即应用内部存储)通常不需要额外的运行时权限,这是最安全省事的选择。如果你确需访问外部共享存储(如SD卡),则需要在安卓模版(Export Template)中配置相应的权限,并在Android 6.0+上动态申请,这会让事情变复杂,非必要不推荐。 -
库依赖
:如前所述,使用
Microsoft.Data.Sqlite可以最大程度避免原生库依赖问题。但极少数情况下,某些SQLite特性(如全文搜索FTS)可能需要本地库。如果遇到相关错误,可能需要为安卓单独打包包含SQLite本地库的e_sqlite3,但这已超出“极简”范畴,绝大多数基础CRUD操作无需此步骤。
3. 核心数据库操作类封装
为了达到“极简代码”的目的,我们将封装一个
SqliteHelper
类,提供连接管理、执行非查询命令(增删改)、执行查询命令(查)等基础功能。这里采用简单的单例模式来管理全局唯一的数据库连接。
3.1 连接字符串与连接管理
Microsoft.Data.Sqlite
的连接字符串非常简洁。我们需要特别注意
Data Source
(数据源)的路径,以及一个对移动端非常重要的选项:
Mode=ReadWriteCreate
。这个模式表示如果数据库文件不存在,则创建它;如果存在,则以读写方式打开。
using Microsoft.Data.Sqlite;
using System;
using System.Data.Common;
public class SqliteHelper : IDisposable
{
private static SqliteHelper _instance;
private SqliteConnection _connection;
// 私有构造函数,防止外部实例化
private SqliteHelper()
{
string connectionString = $"Data Source={DatabaseManager.DbPath};Mode=ReadWriteCreate;Cache=Shared";
_connection = new SqliteConnection(connectionString);
OpenConnection();
}
public static SqliteHelper Instance
{
get
{
if (_instance == null)
{
_instance = new SqliteHelper();
}
return _instance;
}
}
private void OpenConnection()
{
if (_connection.State != System.Data.ConnectionState.Open)
{
_connection.Open();
// 对于SQLite,启用外键约束是一个好习惯,但需要显式开启
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "PRAGMA foreign_keys = ON;";
cmd.ExecuteNonQuery();
}
}
}
public SqliteConnection GetConnection()
{
OpenConnection(); // 确保返回时连接是打开的
return _connection;
}
public void Dispose()
{
_connection?.Close();
_connection?.Dispose();
_instance = null;
GC.SuppressFinalize(this);
}
}
实操心得 :
Cache=Shared这个参数在多线程访问同一个数据库连接时很有用,但在Godot的C#脚本中,我们通常在一个线程(主线程)内操作数据库。如果你计划在后台线程进行大量数据库操作,需要考虑连接池或为每个线程创建独立连接,并注意SQLite的写锁机制。对于大多数Godot游戏场景,单例连接在主线程使用是简单有效的。
3.2 增删改(ExecuteNonQuery)的封装
对于INSERT、UPDATE、DELETE这类不返回结果集的操作,我们封装一个通用方法。这里关键是要使用 参数化查询 ,绝对不要拼接SQL字符串,以防止SQL注入攻击并提升性能(SQLite可以复用查询计划)。
public int ExecuteNonQuery(string sql, params SqliteParameter[] parameters)
{
try
{
using (var cmd = GetConnection().CreateCommand())
{
cmd.CommandText = sql;
if (parameters != null)
{
cmd.Parameters.AddRange(parameters);
}
int rowsAffected = cmd.ExecuteNonQuery();
return rowsAffected;
}
}
catch (SqliteException ex)
{
GD.PrintErr($"执行非查询SQL时出错: {ex.Message}");
GD.PrintErr($"SQL: {sql}");
// 在实际项目中,你可能需要更复杂的错误处理或向上抛出异常
return -1;
}
}
使用示例:插入一条玩家记录
public void AddPlayer(string name, int level)
{
string sql = "INSERT INTO players (name, level, create_time) VALUES (@name, @level, datetime('now'))";
var parameters = new[]
{
new SqliteParameter("@name", name),
new SqliteParameter("@level", level)
};
int result = SqliteHelper.Instance.ExecuteNonQuery(sql, parameters);
if (result > 0)
{
GD.Print("玩家添加成功。");
}
}
3.3 查询(ExecuteReader)与数据模型的映射
查询操作相对复杂,因为它需要处理返回的数据行。我们封装一个查询方法,返回一个
List<Dictionary<string, object>>
,这样比较灵活,每一行数据都是一个字典,键是列名,值是列值。
public List<Dictionary<string, object>> ExecuteReader(string sql, params SqliteParameter[] parameters)
{
var results = new List<Dictionary<string, object>>();
try
{
using (var cmd = GetConnection().CreateCommand())
{
cmd.CommandText = sql;
if (parameters != null)
{
cmd.Parameters.AddRange(parameters);
}
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var row = new Dictionary<string, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
string columnName = reader.GetName(i);
object value = reader.IsDBNull(i) ? null : reader.GetValue(i);
row[columnName] = value;
}
results.Add(row);
}
}
}
}
catch (SqliteException ex)
{
GD.PrintErr($"执行查询SQL时出错: {ex.Message}");
GD.PrintErr($"SQL: {sql}");
}
return results;
}
虽然返回字典很灵活,但在实际项目中,我们更希望将查询结果映射到强类型的C#对象(模型)上,这样使用起来更方便、安全。我们可以写一个简单的映射方法。
首先,定义一个玩家模型:
public class Player
{
public int Id { get; set; }
public string Name { get; set; }
public int Level { get; set; }
public DateTime CreateTime { get; set; }
}
然后,扩展一个查询并映射到对象列表的方法:
public List<T> Query<T>(string sql, Func<Dictionary<string, object>, T> mapper, params SqliteParameter[] parameters)
{
var rawResults = ExecuteReader(sql, parameters);
var list = new List<T>();
foreach (var row in rawResults)
{
list.Add(mapper(row));
}
return list;
}
使用示例:查询所有玩家并映射到Player对象
public List<Player> GetAllPlayers()
{
string sql = "SELECT id, name, level, create_time FROM players ORDER BY level DESC";
return SqliteHelper.Instance.Query(sql, row => new Player
{
Id = Convert.ToInt32(row["id"]),
Name = row["name"]?.ToString(),
Level = Convert.ToInt32(row["level"]),
CreateTime = DateTime.Parse(row["create_time"]?.ToString())
});
}
注意事项 :上面的映射示例为了清晰,直接进行转换和解析。在实际代码中,你需要加入更健壮的异常处理,比如处理
DBNull值。也可以使用更成熟的微型ORM库,如Dapper(通过NuGet安装),它能极大地简化对象映射,但会增加项目依赖。对于“极简”目标,上述手动映射在小项目中是可控的。
4. 在Godot节点中的集成与使用
封装好数据库助手类后,在Godot的C#节点脚本中使用就非常直观了。通常,我们会在一个全局的自动加载(Autoload)单例节点中初始化数据库,或者在主场景的
_Ready
方法中初始化。
4.1 初始化与生命周期管理
创建一个名为
GameDatabase
的C#脚本,并将其设置为Autoload单例(在项目设置 -> Autoload中添加)。
// GameDatabase.cs
using Godot;
public partial class GameDatabase : Node
{
public override void _Ready()
{
base._Ready();
// 初始化数据库路径检查
DatabaseManager.InitializeDatabase();
// 可在此处执行数据库表创建或迁移脚本
CreateTablesIfNotExist();
GD.Print("游戏数据库初始化完成。");
}
private void CreateTablesIfNotExist()
{
string createPlayerTableSql = @"
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
level INTEGER DEFAULT 1,
create_time TEXT DEFAULT (datetime('now'))
);";
SqliteHelper.Instance.ExecuteNonQuery(createPlayerTableSql);
// ... 可以继续创建其他表
}
public override void _Notification(int what)
{
// 当节点退出树时(如游戏退出),释放数据库连接资源
if (what == NotificationPredelete)
{
SqliteHelper.Instance?.Dispose();
}
base._Notification(what);
}
}
4.2 在游戏逻辑中调用
现在,在任何其他C#节点脚本中,你都可以直接调用我们封装的方法来操作数据库了。
// 例如,在某个UI按钮的按下事件中
private void OnSaveButtonPressed()
{
string playerName = _nameLineEdit.Text;
int playerLevel = (int)_levelSpinBox.Value;
// 调用之前封装的方法
var dbHelper = SqliteHelper.Instance; // 获取单例
// 或者通过一个专门的服务类
PlayerService.SavePlayer(playerName, playerLevel);
GD.Print("玩家数据已保存。");
}
// 在需要加载数据的地方
private void LoadAndDisplayPlayers()
{
var players = GetAllPlayers(); // 调用之前的查询方法
foreach (var player in players)
{
GD.Print($"玩家: {player.Name}, 等级: {player.Level}");
// 更新UI...
}
}
4.3 异步操作考量
Godot 4.0+的C#环境对
async/await
有很好的支持。虽然SQLite的同步操作在数据量不大时通常不会阻塞主线程太久,但为了最佳体验,特别是执行复杂查询或批量插入时,可以考虑将数据库操作放在后台线程。
Microsoft.Data.Sqlite
支持异步方法,如
ExecuteNonQueryAsync
、
ExecuteReaderAsync
。你可以将我们的
SqliteHelper
方法改写成异步版本。但请注意,SQLite连接本身不是线程安全的,你不能在多个线程中同时使用同一个连接对象。一个简单的策略是:在异步方法内部,创建并打开一个
新的临时连接
,执行操作后立即关闭释放。或者使用连接池,但在Godot的简单场景中,临时连接更易于管理。
public async Task<int> ExecuteNonQueryAsync(string sql, params SqliteParameter[] parameters)
{
// 注意:这里创建了一个独立于单例的新连接
string connectionString = $"Data Source={DatabaseManager.DbPath};Mode=ReadWrite;Cache=Shared";
await using (var tempConnection = new SqliteConnection(connectionString))
{
await tempConnection.OpenAsync();
using (var cmd = tempConnection.CreateCommand())
{
cmd.CommandText = sql;
if (parameters != null) cmd.Parameters.AddRange(parameters);
return await cmd.ExecuteNonQueryAsync();
}
} // 自动关闭并释放连接
}
在Godot节点中调用异步方法:
private async void OnSaveButtonPressedAsync()
{
_saveButton.Disabled = true;
try
{
int rows = await SqliteHelper.Instance.ExecuteNonQueryAsync("...");
GD.Print($"保存成功,影响行数: {rows}");
}
catch (Exception ex)
{
GD.PrintErr($"保存失败: {ex.Message}");
}
finally
{
_saveButton.Disabled = false;
}
}
5. 安卓端导出配置与真机调试
这是将一切理论付诸实践并验证兼容性的关键一步。Godot导出安卓应用需要安装Android SDK和JDK,并配置好导出模板。
5.1 导出前的项目配置检查
-
导出预设 :在“项目 -> 导出”中,添加“Android”预设。你需要配置:
-
包名
:唯一的应用标识,如
com.yourcompany.yourgame。 - 版本信息 :版本号和版本名称。
- Keystore :发布用的签名文件。调试时可以勾选“调试”使用调试密钥。
-
权限
:除非你需要访问网络、外部存储等,否则保持默认即可。我们的数据库在
user://,无需额外权限。
-
包名
:唯一的应用标识,如
-
构建设置 :
-
在“架构”中,选择
arm64-v8a和armeabi-v7a以覆盖大多数安卓设备。如果使用Microsoft.Data.Sqlite且无原生依赖,理论上选一个就够了。 -
“压缩资源”选项
:确保设置为“无”或“Zstd”。如果设置为“压缩”,可能会影响对
res://目录下初始数据库文件的读取(如果需要复制的话),因为文件会被打包进.apk的压缩资源中,无法直接用File类读取。一个更可靠的做法是将初始数据库文件放在“导出”设置中的“资源”选项卡里,标记为“不导出”,然后通过Godot的ResourceLoader或FileAccessAPI来访问,但这涉及Godot的Resource系统,稍复杂。对于极简方案,如果初始数据库不大,也可以将创建表的SQL语句硬编码在代码中,在首次运行时创建空表。
-
在“架构”中,选择
5.2 处理初始数据库文件(如果需要)
如前所述,如果项目需要一个预填充数据的初始数据库,推荐方法是在首次运行时从
res://
复制到
user://
。但安卓的
res://
对应的是APK包内的资源,不能直接用
System.IO.File
读取。需要使用Godot的
FileAccess
API。
修改我们之前的
DatabaseManager.InitializeDatabase
方法:
public static void InitializeDatabase()
{
string dbFileName = "mygame.db";
string userDir = OS.GetUserDataDir();
_dbPath = Path.Combine(userDir, dbFileName);
// 检查用户目录下是否已有数据库
if (!File.Exists(_dbPath))
{
GD.Print("用户数据目录下未找到数据库,准备从资源中复制...");
CopyDatabaseFromResources(dbFileName);
}
else
{
GD.Print("数据库文件已存在。");
}
}
private static void CopyDatabaseFromResources(string dbFileName)
{
// 使用Godot的FileAccess读取资源包内的文件
string sourcePath = $"res://{dbFileName}";
using var sourceFile = FileAccess.Open(sourcePath, FileAccess.ModeFlags.Read);
if (sourceFile == null)
{
GD.PrintErr($"无法在资源路径找到初始数据库文件: {sourcePath}");
// 可能文件不存在,改为执行SQL创建空表
CreateEmptyDatabaseSchema();
return;
}
byte[] fileData = sourceFile.GetBuffer((long)sourceFile.GetLength());
sourceFile.Close();
// 使用System.IO将数据写入用户目录
try
{
File.WriteAllBytes(_dbPath, fileData);
GD.Print($"初始数据库已复制到: {_dbPath}");
}
catch (Exception ex)
{
GD.PrintErr($"复制数据库文件失败: {ex.Message}");
CreateEmptyDatabaseSchema();
}
}
private static void CreateEmptyDatabaseSchema()
{
GD.Print("创建空的数据库结构...");
// 这里执行创建所有表的SQL语句
string createTableSql = @"..."; // 你的建表SQL
using (var helper = new SqliteHelper()) // 注意:这里会触发连接创建和文件生成
{
helper.ExecuteNonQuery(createTableSql);
}
}
重要提示 :确保你的初始数据库文件
mygame.db位于Godot项目的res://根目录下,并且在“导出”设置中,这个文件 必须 被包含在资源中(通常默认会包含)。不要将其标记为“不导出”。
5.3 真机调试与日志查看
-
连接设备
:开启安卓设备的USB调试模式,用数据线连接电脑。在Godot编辑器的“编辑器 -> 编辑器设置 -> 导出 -> Android”中,配置好Android SDK路径,并确保设备被识别(可以通过
adb devices命令检查)。 - 一键调试 :在Godot编辑器中,直接点击“运行”按钮旁边的下拉箭头,选择你的安卓设备,Godot会自动编译、安装并启动应用。
-
查看日志
:Godot编辑器底部的“输出”面板会显示来自游戏的
GD.Print输出。这是排查运行时问题最直接的工具。如果应用崩溃,这里通常会有错误堆栈信息。对于更底层的安卓日志,可以使用adb logcat命令。
6. 常见问题、性能优化与进阶技巧
6.1 常见问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 编辑器运行正常,安卓打包后报错“找不到SQLite提供程序”或“DLL找不到”。 |
1. 错误引用了依赖原生库的
System.Data.SQLite
。
2.
Microsoft.Data.Sqlite
的特定版本与目标框架不兼容。
|
1. 确认NuGet包是
Microsoft.Data.Sqlite
。
2. 尝试在
.csproj
中指定较新的稳定版,如
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.20" />
。
|
| 安卓上读取/写入数据库失败,提示“权限被拒绝”。 |
尝试写入
res://
目录或外部存储未授权目录。
|
确保所有文件操作路径基于
user://
(通过
OS.GetUserDataDir()
获取)。
|
| 执行查询时,中文字段显示为乱码。 | 数据库连接或表的字符集不是UTF-8。 |
在创建连接字符串时添加
;Default Encoding=UTF8;
(
Microsoft.Data.Sqlite
可能不支持此参数)。更可靠的是,在创建表时,为
TEXT
类型的字段显式指定排序规则,如
CREATE TABLE ... (name TEXT COLLATE NOCASE)
。确保源代码文件本身也是UTF-8编码。
|
| 批量插入数据速度很慢。 | 每条INSERT都是一个独立的事务。 | 使用事务(Transaction)包裹批量操作。 |
| 应用退出再打开,数据丢失。 | 数据可能被错误地写入了临时目录或每次运行都被重置。 |
确认数据库路径是
user://
,并且应用有正常的退出流程(不强制杀死进程),确保数据已持久化。可以在写入后立即调用
SqliteConnection.Close()
或确保单例在应用退出时被正确Dispose。
|
真机调试时,
GD.Print
输出看不到。
| Godot输出面板可能未正确捕获。 |
使用
adb logcat -s godot
命令在终端查看Godot的专属日志。在代码中也可以用
System.Console.WriteLine
作为补充,其输出也可能被
adb logcat
捕获。
|
6.2 性能优化建议
-
使用事务
:对于任何批量写入操作(如初始化数据、批量更新),务必使用事务。这可以将性能提升几个数量级。
public void BulkInsertPlayers(List<Player> players) { using (var transaction = GetConnection().BeginTransaction()) { try { string sql = "INSERT INTO players (name, level) VALUES (@name, @level)"; foreach (var player in players) { using (var cmd = GetConnection().CreateCommand()) { cmd.CommandText = sql; cmd.Parameters.AddWithValue("@name", player.Name); cmd.Parameters.AddWithValue("@level", player.Level); cmd.ExecuteNonQuery(); } } transaction.Commit(); } catch { transaction.Rollback(); throw; } } } -
合理使用索引
:如果经常需要根据
name或level进行查询或排序,在对应的列上创建索引能极大加快查询速度。CREATE INDEX idx_players_level ON players(level); CREATE INDEX idx_players_name ON players(name); -
避免
SELECT *:只查询需要的列,减少数据读取和传输的开销。 - 连接管理 :对于轻量级应用,单例连接足够。如果遇到并发问题,考虑使用连接池或为耗时操作创建短期独立连接。
6.3 进阶技巧:简单的数据迁移
当游戏更新,数据库表结构需要变更时(比如增加一个新列),就需要数据迁移。一个极简的方案是在数据库初始化时,记录一个版本号(可以是一个单独的
user_version
PRAGMA,或者自己建一个
version
表),然后根据当前版本号执行相应的升级SQL脚本。
private void MigrateDatabase()
{
int currentVersion = GetDatabaseVersion(); // 从数据库读取当前版本
int targetVersion = 2; // 你的应用目标版本
while (currentVersion < targetVersion)
{
currentVersion++;
switch (currentVersion)
{
case 1:
// 初始版本,创建表
ExecuteNonQuery("CREATE TABLE ...");
break;
case 2:
// 版本2,增加邮箱列
ExecuteNonQuery("ALTER TABLE players ADD COLUMN email TEXT;");
break;
// ... 更多版本
}
UpdateDatabaseVersion(currentVersion); // 更新版本号
}
}
private int GetDatabaseVersion()
{
// 使用SQLite的user_version,或者查询自己的version表
using (var cmd = GetConnection().CreateCommand())
{
cmd.CommandText = "PRAGMA user_version;";
object result = cmd.ExecuteScalar();
return Convert.ToInt32(result);
}
}
private void UpdateDatabaseVersion(int version)
{
using (var cmd = GetConnection().CreateCommand())
{
cmd.CommandText = $"PRAGMA user_version = {version};";
cmd.ExecuteNonQuery();
}
}
这套从环境搭建、核心封装、Godot集成到安卓部署的流程,是我在多个小项目中验证过的稳定方案。它避免了过度设计,用最少的代码实现了足够的功能,并且重点解决了跨平台的兼容性痛点。在实际开发中,根据项目复杂度的增长,你可以在此基础上引入更强大的ORM(如Dapper),或者将数据库操作进一步抽象为仓储模式。但无论如何,理解这个基础流程和其中的坑,都能让你在Godot C#的数据处理上更加得心应手。

424

被折叠的 条评论
为什么被折叠?



