Unity游戏开发中SQLite数据库的完整实战指南:从安装到性能优化
1. 项目概述:为什么Unity游戏开发绕不开SQLite?
如果你在Unity里做过稍微复杂点的项目,比如需要保存玩家进度、管理背包物品、记录排行榜,或者处理大量配置表,那你肯定对PlayerPrefs的局限性深有体会。它用起来是简单,但存点复杂结构的数据就得自己序列化,数据量一大性能就堪忧,更别提查询和关联了。这时候,一个轻量级、嵌入式的关系型数据库就成了刚需,而SQLite几乎是这个场景下的不二之选。
我这些年经手过不少Unity项目,从单机小游戏到需要本地缓存大量数据的中型应用,SQLite都扮演了关键角色。它不需要像MySQL或PostgreSQL那样单独安装和运行一个数据库服务,整个数据库就是一个.db或.sqlite文件,可以直接打包进你的游戏里,随应用分发,对玩家完全透明。这种“零配置”的特性,让它与Unity的跨平台部署(PC、移动端、甚至部分主机平台)完美契合。你写一套数据访问代码,在各个平台上基本都能跑,省去了大量适配工作。
网上很多教程只告诉你怎么装插件、执行一句INSERT,但真到了项目里,你会遇到一堆棘手问题:怎么在多线程环境下安全操作数据库?iOS平台上有哪些特殊的文件权限坑?如何给数据库加密防止玩家轻易修改存档?这些才是实战中的核心。这篇文章,我就以一个老鸟的身份,带你从零开始,把SQLite在Unity里的“安装、配置、核心使用、避坑指南”一条龙打通,并附上能直接抄作业的完整代码模块。
2. 核心工具选型与环境搭建
2.1 选择正确的SQLite集成方案
在Unity里用SQLite,你不是从零开始写一个驱动,而是选择一个成熟的、与Unity的.NET环境兼容的封装库。主流方案有几个:
Mono.Data.Sqlite:这是Unity旧版本(基于Mono时代)经常自带或通过
System.Data提供的一个方案。它的优点是“原生”,但缺点也很明显:版本老旧,对新版.NET兼容性不佳,在IL2CPP编译环境下(尤其是移动平台)容易出问题,而且功能有限。不推荐在新项目中使用。sqlite-net:这是一个非常流行的、面向对象的轻量级ORM(对象关系映射)库。它的API非常简洁友好,通过C#的类和属性就能定义表结构,用Linq进行查询,大大减少了手写SQL的繁琐。对于大多数游戏数据管理场景(如配置表加载、玩家数据缓存),它是首选。其核心文件通常只有一个
SQLite.cs,集成极其方便。Microsoft.Data.Sqlite:这是微软官方为.NET Core/.NET 5+提供的SQLite实现,现在也兼容Unity的新版.NET Standard 2.1/ .NET 6。它更底层,提供了对ADO.NET风格(
SqliteConnection,SqliteCommand)的直接操作,控制力更强,适合需要执行复杂SQL、存储过程(虽然SQLite不支持真正的存储过程,但可以用函数)或对性能有极致要求的场景。Unity 2021 LTS及以上版本,使用.NET Standard 2.1或更高时,可以很好地支持它。
我的实战选择与理由: 对于90%的Unity游戏项目,我推荐使用sqlite-net。理由很简单:游戏开发中,我们处理的数据对象(如PlayerData、Item)通常有明确的C#类对应。sqlite-net的ORM方式让代码更清晰,维护更方便,避免了SQL字符串拼接的错误和枯燥的DataReader字段映射。它能处理大部分需求,包括基本的索引、事务。只有当你有非常特殊的SQL需求,或者项目本身是基于ADO.NET架构迁移过来时,才需要考虑Microsoft.Data.Sqlite。
注意:无论选择哪个方案,在移动平台(iOS/Android)上,你都需要对应平台的SQLite原生库(
.bundle或.so文件)。幸运的是,像sqlite-net这样的成熟库,其相关的Unity插件包(如SQLite.Bytecode或sqlite-net的.unitypackage)通常会帮你处理好这些依赖,包含各个平台的预编译原生插件。自己从头编译配置这些原生库是个大坑,尽量避免。
2.2 一步步安装与配置sqlite-net
这里以sqlite-net为例,演示最可靠的集成方法。我推荐通过Unity的Package Manager从Git URL安装,这样可以方便地更新。
打开Package Manager:在Unity Editor中,点击
Window->Package Manager。添加来自Git的包:点击左上角的“+”号,选择“Add package from git URL...”。
- 输入sqlite-net的一个稳定版本仓库地址。例如,可以使用:
https://github.com/praeclarum/sqlite-net.git。你也可以在GitHub上搜索“sqlite-net unity”,找到一些已经打包好平台原生依赖的仓库,这样更省事。一个常用的、包含各平台原生插件的Unity适配版本地址可能是:https://github.com/codecoding/SQLite4Unity3d.git(安装前请确认仓库的可用性和许可)。
- 输入sqlite-net的一个稳定版本仓库地址。例如,可以使用:
等待安装:Unity会自动克隆仓库并导入。安装完成后,你会在Package Manager的“My Registries”或“In Project”列表中看到它。
检查关键文件:安装成功后,在你的项目
Assets文件夹下,应该能看到类似Plugins(里面包含x86,x86_64,Android,iOS等子文件夹,存放原生库)和SQLite(存放核心的SQLite.cs等C#脚本)的目录结构。这是集成成功的标志。处理iOS平台的特殊设置(关键!):如果你要发布到iOS,这是最大的一个坑。iOS对文件系统的访问有沙盒限制,并且要求所有动态库都必须签名。你需要确保:
- iOS平台的SQLite原生库(通常是一个
.bundle文件)已被正确导入。 - 在Unity中选中这个
.bundle文件,在Inspector面板中,确保**“Add to Framework”**选项被勾选。这步至关重要,它告诉Xcode将这个库链接到你的应用程序框架中,否则在真机上会因签名问题导致崩溃。
- iOS平台的SQLite原生库(通常是一个
2.3 创建第一个数据库连接
环境搭好了,我们来写第一行代码。目标是:在游戏的持久化数据路径下,创建或打开一个数据库文件。
using SQLite; // 引入sqlite-net命名空间 using System.IO; using UnityEngine; public class SQLiteDemo : MonoBehaviour { private string _dbPath; private SQLiteConnection _dbConnection; void Start() { InitializeDatabase(); } void InitializeDatabase() { // 1. 确定数据库文件路径 // Application.persistentDataPath 是跨平台的安全可写路径 // 例如:Windows上可能在 C:\Users\<用户名>\AppData\LocalLow\<公司名>\<产品名> // iOS/Android 在各自的沙盒目录下 string directoryPath = Application.persistentDataPath; _dbPath = Path.Combine(directoryPath, "game_data.db"); Debug.Log($"数据库路径: {_dbPath}"); // 2. 创建数据库连接 // SQLiteConnection 构造函数会处理:如果文件不存在则创建,存在则打开 // 第二个参数 storeDateTimeAsTicks 建议设为 true,以Ticks格式存储DateTime,避免时区混乱 _dbConnection = new SQLiteConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks: true); Debug.Log("数据库连接已建立!"); } void OnDestroy() { // 3. 重要!在程序退出或对象销毁时关闭连接,释放资源 if (_dbConnection != null) { _dbConnection.Close(); _dbConnection.Dispose(); _dbConnection = null; } } }这段代码的要点解析:
- 路径选择:
Application.persistentDataPath是Unity提供的、跨平台的、有读写权限的目录。玩家的存档、配置应该放在这里。切忌使用Application.dataPath(在发布后是只读的)或Application.streamingAssetsPath(通常用于存放初始只读资源)。 - 连接标志:
SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create表示以读写模式打开,如果文件不存在就创建。这是最常用的组合。 - 资源管理:
SQLiteConnection继承了IDisposable。务必在不用时(如场景切换、游戏退出)调用Close()和Dispose(),否则可能导致文件锁死,下次无法打开,或者在移动平台上引发内存泄漏。
3. 数据建模与基础操作实战
3.1 定义你的数据模型(表结构)
在sqlite-net中,定义表就是定义一个C#类。我们用[PrimaryKey],[AutoIncrement]等特性来修饰属性。
[Table("players")] // 指定表名,如果不指定,则使用类名 public class Player { [PrimaryKey, AutoIncrement] // 主键,且自增长 [Column("id")] // 指定列名 public int Id { get; set; } [Unique, NotNull] // 唯一约束,且不能为NULL [MaxLength(50)] // 最大长度限制,有助于优化存储 public string Name { get; set; } [Indexed] // 为该列创建索引,大幅提升根据Level查询的速度 public int Level { get; set; } public float Experience { get; set; } [Ignore] // 这个属性不会被映射到数据库表中 public string DisplayInfo => $"{Name} (Lv.{Level})"; // SQLite不支持直接的DateTime存储,但sqlite-net可以帮我们转换 public DateTime LastLogin { get; set; } } [Table("inventory_items")] public class InventoryItem { [PrimaryKey] public string ItemId { get; set; } // 使用GUID或自定义ID作为主键 [NotNull, Indexed] public int PlayerId { get; set; } // 外键,关联到players表的id [NotNull] public string ItemType { get; set; } public int Count { get; set; } // 可以存储一个JSON字符串,用来表示复杂的、非结构化的属性 public string ExtraDataJson { get; set; } }建模经验谈:
- 主键选择:自增整数(
int)是最简单高效的主键。如果数据需要全局唯一或在客户端生成,可以用string类型的GUID(System.Guid.NewGuid().ToString()),但查询性能会比整数稍差。 - 索引策略:
[Indexed]不要滥用。只为**经常用于查询条件(WHERE)、连接(JOIN)或排序(ORDER BY)**的列创建索引。比如Player.Level(用于按等级筛选或排序)、InventoryItem.PlayerId(用于关联查询)。索引会加快查询,但会减慢插入和更新速度,并增加数据库文件大小。 - 复杂数据存储:对于技能列表、装备随机属性这类结构不固定或很复杂的数据,一个实用的技巧是:在数据库里用一个
TEXT类型的字段(如ExtraDataJson)存储JSON字符串。在C#端,用JsonUtility或Newtonsoft.Json序列化和反序列化。这样保持了关系型数据库表结构的清晰,又兼顾了灵活性。
3.2 创建表与基础CRUD操作
有了模型,接下来创建表,并实现增删改查。
void Start() { InitializeDatabase(); CreateTables(); TestCRUDOperations(); } void CreateTables() { // CreateTable方法会检查表是否存在,不存在则创建,存在则忽略(不会删除重建) // 第二个参数 CreateFlags.None 表示标准创建。如果需要,可以使用 CreateFlags.AutoIncPK 等 _dbConnection.CreateTable<Player>(CreateFlags.None); _dbConnection.CreateTable<InventoryItem>(CreateFlags.None); Debug.Log("数据表已就绪。"); } void TestCRUDOperations() { // --- 插入 (Create) --- var newPlayer = new Player { Name = "UnityMaster", Level = 1, Experience = 0, LastLogin = DateTime.Now }; int rowsAffected = _dbConnection.Insert(newPlayer); Debug.Log($"插入玩家,影响行数:{rowsAffected},新玩家ID:{newPlayer.Id}"); // 插入后,自增Id会被自动填充 // 批量插入,使用事务提升性能 var itemsToAdd = new List<InventoryItem> { new InventoryItem { ItemId = "item_001", PlayerId = newPlayer.Id, ItemType = "potion", Count = 5 }, new InventoryItem { ItemId = "item_002", PlayerId = newPlayer.Id, ItemType = "sword", Count = 1 }, }; _dbConnection.RunInTransaction(() => { foreach (var item in itemsToAdd) { _dbConnection.Insert(item); } }); Debug.Log("批量插入物品完成。"); // --- 查询 (Read) --- // 1. 根据主键查询 Player fetchedPlayer = _dbConnection.Get<Player>(newPlayer.Id); if (fetchedPlayer != null) { Debug.Log($"查询到玩家:{fetchedPlayer.DisplayInfo}"); } // 2. 使用LINQ查询(最常用、最直观的方式) var highLevelPlayers = _dbConnection.Table<Player>() .Where(p => p.Level > 10) .OrderByDescending(p => p.Level) .ToList(); Debug.Log($"找到 {highLevelPlayers.Count} 个10级以上的玩家。"); // 3. 执行自定义SQL查询(更复杂的情况) var playerItems = _dbConnection.Query<InventoryItem>( "SELECT * FROM inventory_items WHERE PlayerId = ? AND Count > ?", newPlayer.Id, 0); Debug.Log($"玩家有 {playerItems.Count} 种物品。"); // --- 更新 (Update) --- fetchedPlayer.Level = 2; fetchedPlayer.Experience = 100; rowsAffected = _dbConnection.Update(fetchedPlayer); // 根据主键更新 Debug.Log($"更新玩家,影响行数:{rowsAffected}"); // 更新特定字段(部分更新),避免先查询再更新整个对象 _dbConnection.Execute("UPDATE players SET Experience = ? WHERE Id = ?", 150, fetchedPlayer.Id); // --- 删除 (Delete) --- // 根据对象删除 // _dbConnection.Delete(fetchedPlayer); // 根据主键删除 // _dbConnection.Delete<Player>(fetchedPlayer.Id); // 根据条件删除 rowsAffected = _dbConnection.Table<InventoryItem>().Delete(item => item.Count == 0); Debug.Log($"删除了 {rowsAffected} 个数量为0的物品。"); }CRUD操作的核心技巧:
- 事务(Transaction):只要是批量操作(插入、更新、删除多条记录),务必放在
RunInTransaction中。这能带来巨大的性能提升,因为SQLite默认每条语句都是一个独立的事务,频繁提交会非常慢。事务保证了操作的原子性,要么全部成功,要么全部回滚。 GetvsTable().Where:Get<T>(primaryKey)是通过主键快速检索单条记录的最高效方式。Table<T>().Where(...)则是进行复杂条件查询的入口,它返回一个TableQuery<T>对象,支持链式调用Where,OrderBy,Take,Skip等,最后通过ToList()或FirstOrDefault()执行。Update的坑:Update(object)方法默认使用对象的主键值去匹配数据库中的记录,并更新所有映射的字段。如果你只想更新一两个字段,使用Execute执行带参数的SQLUPDATE语句是更好的选择,性能更高,也避免了不小心覆盖其他字段。
4. 高级特性与性能优化实战
4.1 多线程安全访问
Unity中,我们可能在主线程加载UI数据,在另一个线程(如下载、计算线程)中写入日志或更新缓存。SQLite连接本身不是线程安全的。
错误示范:在多个线程中共享同一个SQLiteConnection实例,会导致随机崩溃或数据损坏。
正确模式:使用“单连接单线程”或“连接池”模式。对于Unity游戏,一个简单有效的策略是:为每个需要访问数据库的线程,在开始时创建自己的连接,在线程结束时关闭。但由于Unity主线程的特殊性,我们通常在主线程管理一个长期连接,而其他线程使用短期连接。
// 主线程持有的全局连接(用于UI、即时操作) private static SQLiteConnection _mainThreadConnection; private static readonly object _connectionLock = new object(); // 在工作线程中访问数据库的方法 public static void AddLogInBackground(string logMessage) { // 在新的线程或Task中 System.Threading.Tasks.Task.Run(() => { // 在线程内部创建并使用独立的连接 string dbPath = Path.Combine(Application.persistentDataPath, "game_data.db"); using (var threadLocalConnection = new SQLiteConnection(dbPath)) { threadLocalConnection.Execute("INSERT INTO logs (message, time) VALUES (?, ?)", logMessage, DateTime.Now); } // using语句会自动Dispose并关闭连接 }); } // 主线程访问方法(封装了锁,确保安全) public static List<Player> GetPlayersFromMainThread() { lock (_connectionLock) // 如果可能有多个主线程协程同时访问,需要加锁 { if (_mainThreadConnection == null) { _mainThreadConnection = new SQLiteConnection(Path.Combine(Application.persistentDataPath, "game_data.db")); } return _mainThreadConnection.Table<Player>().ToList(); } }重要提示:SQLite支持“多线程模式”,但需要编译时开启特定选项。我们使用的预编译原生库通常已开启。但即便如此,最佳实践仍然是避免多个线程同时写同一个数据库文件。上述模式(主线程一个长连接,工作线程用独立的短连接)能有效规避大部分并发问题。如果必须高并发写,可以考虑使用一个专门的队列,将所有写操作序列化到单个线程中处理。
4.2 数据库加密(保护玩家存档)
防止玩家用DB Browser for SQLite等工具轻易打开存档文件修改数据,是单机游戏常见的需求。SQLite本身支持通过SQLCipher扩展进行加密。
集成SQLCipher到sqlite-net:
- 获取加密库:你需要找到编译好的、支持SQLCipher的SQLite原生库(
.dll,.dylib,.so,.bundle),替换掉之前插件中非加密的版本。或者寻找已经集成了SQLCipher的sqlite-net变种包(如SQLite4Unity3d的某些分支可能提供)。 - 连接时提供密码:在创建
SQLiteConnection时,使用一个接受密码参数的构造函数重载。
// 假设你使用的sqlite-net版本支持加密 string password = "YourStrong!Password123"; // 密码需要妥善保存,可以考虑与设备信息绑定 _dbConnection = new SQLiteConnection(_dbPath, password, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks: true);加密的注意事项:
- 性能开销:加密解密会带来一定的CPU开销,对于频繁读写的小型数据库影响不大,但对于大型数据库或性能敏感场景需要测试。
- 密码管理:密码不能硬编码在代码里。常见的做法是:首次运行时生成一个随机密钥,将其与设备的某个唯一标识(注意用户隐私合规)结合后,用
PlayerPrefs(不安全,可混淆)或系统的安全存储API(如Android的KeyStore,iOS的Keychain)保存。这样即使玩家重装游戏,只要不清理特定数据,存档依然能解密。 - 兼容性:确保所有目标平台的SQLite原生库都使用了相同版本的SQLCipher,并且加密算法兼容。
4.3 数据库迁移(版本升级)
游戏版本更新,数据结构(表、列)可能需要改变。你不能直接删除旧的玩家存档。sqlite-net提供了简单的迁移机制。
public class DatabaseManager { private const int CURRENT_DB_VERSION = 2; // 当前数据库版本 private SQLiteConnection _conn; public void InitializeAndMigrate() { _conn = new SQLiteConnection(_dbPath); // 创建一个表来存储数据库的版本信息 _conn.CreateTable<SchemaInfo>(CreateFlags.None); var currentSchema = _conn.Table<SchemaInfo>().FirstOrDefault(); int oldVersion = currentSchema?.Version ?? 0; if (oldVersion < CURRENT_DB_VERSION) { Debug.Log($"开始数据库迁移: 从版本 {oldVersion} 到 {CURRENT_DB_VERSION}"); // 在事务中执行迁移 _conn.RunInTransaction(() => { // 版本1到版本2的迁移逻辑 if (oldVersion < 2) { // 示例:为players表添加一个新的列 _conn.Execute("ALTER TABLE players ADD COLUMN Gold INTEGER DEFAULT 0"); // 或者更复杂的迁移:创建新表,复制数据等 // _conn.CreateTable<NewTable>(); // _conn.Execute("INSERT INTO new_table SELECT ... FROM old_table ..."); } // 未来版本3的迁移可以在这里添加 else if (oldVersion < 3) ... // 更新版本号 if (currentSchema == null) { _conn.Insert(new SchemaInfo { Version = CURRENT_DB_VERSION }); } else { currentSchema.Version = CURRENT_DB_VERSION; _conn.Update(currentSchema); } }); Debug.Log("数据库迁移完成。"); } // 确保所有当前版本的表结构是最新的 _conn.CreateTable<Player>(CreateFlags.AllImplicit); // AllImplicit 会尝试添加缺失的列 _conn.CreateTable<InventoryItem>(CreateFlags.AllImplicit); // ... 创建其他表 } [Table("schema_info")] private class SchemaInfo { [PrimaryKey] public int Id { get; set; } = 1; // 单行记录 public int Version { get; set; } } }迁移策略的核心:
- 渐进式迁移:从旧版本一步步升级到最新版本,每个版本间的变更脚本要幂等(执行多次结果相同)。
ALTER TABLE ADD COLUMN是幂等的(如果列已存在会报错,需要处理)。 - 数据备份:对于重大结构变更(如删除列、拆分表),最安全的方法是在迁移前,将旧表的数据以JSON或其他格式导出备份,迁移完成后再导入。或者直接复制一份旧的数据库文件作为备份。
- 测试:务必在测试阶段模拟从各个旧版本升级到新版本的过程,确保迁移脚本正确无误,玩家数据不会丢失。
5. 跨平台部署与疑难问题排查
5.1 各平台部署要点
- Windows/Mac/Linux (Standalone):最简单,通常没有问题。确保
Plugins/x86_64(64位)或Plugins/x86(32位)文件夹下有对应的sqlite3.dll(Windows)或sqlite3.bundle(Mac)文件。 - Android:
- 确保
Plugins/Android目录下有libsqlite.so等库文件。现在的Unity插件包通常会自动处理。 - 在Player Settings -> Other Settings中,检查
Scripting Backend。如果使用IL2CPP,需要确保SQLite原生库有对应的ARMv7、ARM64版本。Mono后端则相对宽松。 - 如果遇到
DllNotFoundException,检查库文件是否被打包进APK,以及ABI(armeabi-v7a, arm64-v8a)是否匹配你的设备。
- 确保
- iOS:
- 这是问题最多的平台。确保
Plugins/iOS下有.bundle或.a文件。 - 最关键的一步:在Unity中选中iOS的SQLite库文件,在Inspector中必须勾选**“Add to Framework”**,并确保
Platform设置为iOS。 - 如果使用Xcode构建,检查
Linked Frameworks and Libraries中是否包含了该库。 - iOS文件系统沙盒严格。数据库文件必须放在
Application.persistentDataPath下才有写入权限。Application.streamingAssetsPath是只读的。
- 这是问题最多的平台。确保
5.2 常见问题与解决方案实录
问题1:在iOS真机上崩溃,报错“Unable to open database file”或找不到SQLite库。
- 排查:
- 确认数据库文件路径正确,且应用有该路径的写入权限(使用
Application.persistentDataPath)。 - 确认SQLite原生库(
.bundle)已正确导入,并在Inspector中为iOS平台勾选了“Add to Framework”。 - 检查Xcode工程的
Build Phases->Link Binary With Libraries,是否有libsqlite3.tbd或你自定义的.bundle。有时需要手动添加libsqlite3.tbd系统库。 - 尝试在Xcode中查看设备日志,获取更详细的崩溃信息。
- 确认数据库文件路径正确,且应用有该路径的写入权限(使用
问题2:数据库操作缓慢,尤其是批量插入时。
- 解决方案:
- 使用事务:这是提升批量写性能最有效的手段,能将速度提升数十倍甚至上百倍。务必用
RunInTransaction包裹批量操作。 - 调整同步模式:SQLite默认是
FULL同步(synchronous=FULL),保证数据在写入磁盘后才返回,最安全但最慢。可以在连接字符串或打开后执行PRAGMA synchronous = NORMAL;甚至OFF;来提升速度,但会牺牲一些数据安全(在系统崩溃时可能损坏数据库)。对于游戏存档,NORMAL通常是可接受的平衡点。 - 启用预写日志:执行
PRAGMA journal_mode = WAL;。WAL模式在大多数情况下比传统的回滚日志模式更快,并且支持更好的读并发(读不阻塞写,写不阻塞读)。
- 使用事务:这是提升批量写性能最有效的手段,能将速度提升数十倍甚至上百倍。务必用
问题3:数据库文件越来越大,删除数据后文件大小不减。
- 原因:SQLite使用页存储,删除数据后空间被标记为空闲,但不会自动返还给操作系统。
- 解决方案:定期执行
VACUUM;命令。这会重建数据库文件,释放未使用的空间。但这是一个重量级操作,会占用大量临时磁盘空间并在操作期间锁定数据库,务必在玩家不活跃时(如登录、退出时)进行。_dbConnection.Execute("VACUUM;");
问题4:如何查看和调试数据库内容?
- 工具:在PC上开发时,强烈推荐使用DB Browser for SQLite (SQLiteStudio)。这是一个图形化工具,可以打开你的
.db文件,查看表结构、浏览数据、执行SQL语句。 - 技巧:在
InitializeDatabase后,将_dbPath打印出来(Debug.Log),然后在文件管理器中找到这个路径,用DB Browser打开它,可以直观地验证你的操作是否生效。
问题5:IL2CPP代码裁剪导致找不到方法(MethodNotFoundException)。
- 原因:IL2CPP为了减小包体,会裁剪未使用的代码。如果sqlite-net的一些方法是通过反射调用的,可能会被错误裁剪。
- 解决方案:在项目的
Assets目录下创建一个link.xml文件,告诉IL2CPP不要裁剪特定的命名空间或程序集。
具体的程序集名称需要你查看项目编译后的输出或在Visual Studio中查看引用。<!-- link.xml --> <linker> <assembly fullname="Mono.Data.Sqlite" preserve="all"/> <!-- 如果你用的是sqlite-net,它通常编译进你的主程序集,可能需要保留System.Data --> <assembly fullname="System.Data" preserve="all"/> <assembly fullname="SQLite-net" preserve="all"/> </linker>
将SQLite集成到Unity项目里,远不止是执行一句CREATE TABLE那么简单。从工具选型、平台适配,到线程安全、加密迁移,每一个环节都有需要仔细琢磨的地方。我最开始也踩过不少坑,比如在iOS上忘了勾选“Add to Framework”折腾了一整天,或者在没有事务的情况下导入上万条配置数据等到怀疑人生。
我的建议是,在项目早期就规划好数据层,把数据库连接管理、迁移逻辑封装成一个独立的DatabaseManager单例或服务。所有的数据访问都通过这个管理器进行,这样便于统一处理错误、实施性能优化(如连接池、缓存)和后期升级。
最后,别忘了备份。虽然SQLite很稳定,但任何软件都可能遇到极端情况。对于核心的玩家存档,除了数据库本身,定期(如每天)将关键数据表导出为JSON备份到另一个位置,是一个低成本高收益的容灾策略。当你的游戏拥有了稳定可靠的数据存储能力,你才能更专注于玩法和内容的开发。
