1. 项目概述:为什么Unity开发者需要SQLite?
如果你正在用Unity开发游戏或者应用,尤其是需要本地存储玩家进度、配置、道具信息,或者处理一些离线数据,你大概率会遇到一个选择:用什么来存数据?PlayerPrefs?JSON文件?还是找个数据库?PlayerPrefs存点简单的键值对还行,稍微复杂点的结构化数据,比如一个背包系统,几十种道具,每种道具还有不同的属性,用PlayerPrefs来管理简直就是一场灾难,代码会变得又臭又长。JSON或者XML文件呢?读写和解析整个文件来修改一条数据,效率低不说,还容易因为读写冲突导致数据损坏。
这时候,一个轻量级、零配置、单文件的嵌入式数据库就成了刚需。SQLite就是这个领域的神器,它几乎不需要任何外部依赖,整个数据库就是一个
.db
或
.sqlite
文件,你可以像操作普通文件一样复制、移动它。但问题来了,怎么在Unity里用上SQLite呢?官方没有内置支持,自己去编译SQLite的C源码、处理各个平台(尤其是iOS和Android)的兼容性,足以让大部分开发者望而却步。
这就是
SQLite4Unity3d
出现的背景。它不是一个全新的数据库引擎,而是一个精心封装的“桥梁”插件。作者Roberto Huertas当初也和我们一样,被网上零散复杂的教程和Asset Store上的付费方案搞得很头疼,于是他基于一个成熟的C#库
sqlite-net
,打包好了所有平台需要的原生SQLite库(
.dll
,
.so
,
.bundle
等),并提供了清晰的接入方式。它的核心价值在于:
开箱即用,跨平台无忧
。你不需要关心Android该用哪个
libsqlite3.so
,也不需要处理iOS的JIT限制(当然,部分限制依然存在),更不用自己编译。它把脏活累活都干了,让你能专注于用熟悉的C#和LINQ去操作数据库。
简单来说,这个插件解决的核心痛点是: 为Unity项目提供一种近乎零成本、稳定可靠的本地关系型数据存储方案 。无论是独立游戏、移动应用,还是需要离线数据缓存的工具,它都能胜任。接下来,我会带你从零开始,在5分钟内完成集成,并深入讲解如何在实际项目中用好它。
2. 环境准备与插件集成
2.1 获取与导入插件
官方推荐的方式是从GitHub仓库下载那个
SQLite4Unity3d.zip
文件。但根据我的经验,直接Clone仓库或者下载源码是更好的选择,因为你可以随时查看示例代码和最新的文件结构。
步骤一:获取插件文件
-
访问
robertohuertasm/SQLite4Unity3d的GitHub页面。 - 不要只下载那个ZIP,点击绿色的“Code”按钮,选择“Download ZIP”下载整个仓库。或者,如果你熟悉Git,直接克隆仓库到本地。
- 解压下载的ZIP文件。
步骤二:导入到Unity项目
-
在你的Unity项目资源管理器中,找到或创建
Assets/Plugins文件夹。这是Unity识别原生插件(Native Plugins)的标准位置,对于确保不同平台使用正确的库文件至关重要。 -
将解压后文件夹中的
Plugins文件夹(里面包含了x86,x86_64,Android,iOS等子目录)整个复制到你的Assets/Plugins目录下。 注意 :是合并,而不是覆盖。确保最终你的Assets/Plugins下包含了这些平台特定的子文件夹。 -
将仓库根目录下的
SQLite.cs文件(这是sqlite-net的核心封装类)复制到你的项目脚本目录,例如Assets/Scripts或任何你存放C#脚本的地方。
注意 :很多新手在这一步会犯错,只复制了
SQLite.cs而忽略了Plugins文件夹,导致在编辑器里运行正常,打包到移动平台时却报“DLLNotFoundException”。务必确认Assets/Plugins目录结构完整。
步骤三:准备数据库文件(可选)
如果你有一个预先设计好的SQLite数据库文件(比如用DB Browser for SQLite工具创建的),你需要将它放入
Assets/StreamingAssets
文件夹。Unity在打包时,会原封不动地将这个文件夹下的内容复制到最终应用的特定可读路径(如
Application.streamingAssetsPath
)。这是Unity中存放只读资源的标准方式。
如果还没有数据库文件,也没关系,我们可以用代码直接创建一个。
2.2 理解插件结构与平台差异
导入后,让我们看看插件都包含了什么:
-
Assets/Plugins/: 这是核心。里面的x86和x86_64对应Windows和Mac编辑器和Standalone平台。Android文件夹里是libsqlite3.so,iOS文件夹里是libsqlite3.a或.bundle。插件已经根据Unity的规则设置好了这些库文件的平台标识(Platform Settings),比如是否为特定平台启用。 -
SQLite.cs: 这是你编程接口。它提供了数据库连接、创建表、增删改查(CRUD)的所有方法,并且支持LINQ查询语法。它是对原生SQLite C API的纯C#封装,性能不错,语法友好。
关于iOS的特别说明
:
iOS系统出于安全考虑,禁止动态代码生成(JIT编译)。而
sqlite-net
库在将查询结果映射到C#对象时,默认会使用反射和
System.Reflection.Emit
来动态创建映射委托,这在iOS上会触发错误。
SQLite4Unity3d
对此做了处理,但并非万能。对于简单的查询和对象映射,它可以工作。但对于非常复杂的查询或嵌套对象,可能还是会遇到
ExecutionEngineException
。如果遇到这种情况,后备方案是使用原始的SQL语句(通过
SQLite.cs
提供的
Execute
或
Query
方法)来规避反射,虽然麻烦点,但最稳妥。
3. 核心API详解与数据建模
3.1 建立数据库连接
一切操作始于一个数据库连接。
SQLite.cs
中的
SQLiteConnection
类是你的主要入口。连接字符串就是数据库文件的路径。
using UnityEngine;
using System.IO;
// SQLite.cs 已经包含了必要的命名空间
public class DatabaseManager : MonoBehaviour
{
private SQLiteConnection _dbConnection;
private string _dbPath;
void Start()
{
InitializeDatabase();
}
private void InitializeDatabase()
{
// 确定数据库文件路径
// 在Unity编辑器和Windows/Mac Standalone中,我们可以使用持久化数据路径
// 在Android/iOS上,我们需要将StreamingAssets中的数据库复制到可读写路径
#if UNITY_EDITOR
_dbPath = Path.Combine(Application.dataPath, "MyGame.db");
#elif UNITY_ANDROID || UNITY_IOS
string streamingDbPath = Path.Combine(Application.streamingAssetsPath, "MyGame.db");
_dbPath = Path.Combine(Application.persistentDataPath, "MyGame.db");
// 如果持久化路径下没有数据库,则从StreamingAssets复制(首次运行或更新后)
if (!File.Exists(_dbPath))
{
// 注意:Android上Application.streamingAssetsPath是压缩包路径,需要用UnityWebRequest或WWW读取
// 这里简化处理,假设已处理。实际项目中需要写一个协程来异步复制。
File.Copy(streamingDbPath, _dbPath, true);
}
#else
_dbPath = Path.Combine(Application.persistentDataPath, "MyGame.db");
#endif
// 创建数据库连接
_dbConnection = new SQLiteConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
Debug.Log($"Database initialized at: {_dbPath}");
}
void OnDestroy()
{
// 重要!务必在不用时关闭连接,释放资源
if (_dbConnection != null)
{
_dbConnection.Close();
_dbConnection = null;
}
}
}
关键点解析 :
-
路径选择
:
Application.persistentDataPath是跨平台的可读写目录,玩家数据应该存在这里。Application.streamingAssetsPath是只读的,用于存放初始数据库或资源。 -
Android数据库复制
:在Android上,
StreamingAssets里的文件在APK中,运行时无法直接访问。必须先将它解压复制到persistentDataPath。这个过程需要异步处理(用UnityWebRequest),否则会阻塞主线程,对于大文件会导致卡顿。 -
连接标志
:
SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create表示以读写方式打开,如果文件不存在则创建。 -
资源管理
:
SQLiteConnection最好封装成单例或通过依赖注入管理,确保在场景切换或游戏退出时正确关闭连接,避免文件锁死。
3.2 定义数据模型(创建表)
sqlite-net
使用特性(Attribute)来标注C#类,从而自动映射到数据库表。这是一种非常简洁的ORM(对象关系映射)方式。
假设我们要为一个小游戏创建玩家存档和物品表:
// 玩家表
[Table("PlayerSave")]
public class PlayerSaveData
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; } // 主键,自增
[MaxLength(50)]
public string PlayerName { get; set; }
public int Level { get; set; }
public float Experience { get; set; }
public DateTime LastLoginTime { get; set; }
[Ignore] // 这个属性不会被存入数据库
public Vector3 LastPosition { get; set; }
// 可以将复杂对象序列化为JSON字符串存储
public string InventoryJson { get; set; }
}
// 物品表
[Table("Item")]
public class Item
{
[PrimaryKey]
public string ItemId { get; set; } // 使用字符串作为主键,如"item_potion_health"
[MaxLength(100)]
public string Name { get; set; }
public ItemType Type { get; set; } // 枚举类型
public int MaxStack { get; set; } = 1;
public string Description { get; set; }
}
public enum ItemType
{
Consumable,
Equipment,
Material,
Quest
}
常用特性说明 :
-
[Table("表名")]: 指定类对应的表名,可省略,默认用类名。 -
[PrimaryKey]: 标记为主键。支持复合主键(在多个属性上标记)。 -
[AutoIncrement]: 自增,通常用于整数主键。 -
[MaxLength(长度)]: 字符串字段的最大长度,数据库会校验。 -
[NotNull]: 字段不允许为NULL。 -
[Unique]: 字段值必须唯一。 -
[Ignore]: 忽略该属性,不映射到数据库。 -
[Indexed]: 为该字段创建索引,提升查询速度。
创建表 : 定义好模型类后,创建表只需要一行代码:
_dbConnection.CreateTable<PlayerSaveData>(); // 如果表不存在则创建,存在则忽略
_dbConnection.CreateTable<Item>();
CreateTable
方法会检查表是否存在,不存在则根据类的定义(包括所有标记了特性的公共属性)生成
CREATE TABLE
语句。这是我最喜欢的功能之一,修改模型后(比如增加一个属性),再次运行
CreateTable
,它会帮你添加新的列(如果设置
CreateFlags.AutoIncProperty
等选项,行为略有不同,默认是忽略已存在的表)。
3.3 增删改查(CRUD)操作
有了连接和表,我们就可以进行数据操作了。
sqlite-net
提供了同步和异步两种API,但如前所述,
SQLite4Unity3d
主要封装了同步部分。对于Unity,为了避免阻塞主线程导致卡顿,
强烈建议将数据库操作放在后台线程
(例如使用
Task.Run
或
ThreadPool
),然后在主线程回调。
插入数据 :
// 插入单个对象
var newPlayer = new PlayerSaveData { PlayerName = "Hero", Level = 1, Experience = 0f };
int rowsAffected = _dbConnection.Insert(newPlayer);
Debug.Log($"Inserted, new ID: {newPlayer.Id}"); // 自增Id会自动回填
// 插入或替换(根据主键)
_dbConnection.InsertOrReplace(newPlayer);
// 批量插入(使用事务提升性能)
_dbConnection.RunInTransaction(() => {
for (int i = 0; i < 1000; i++)
{
var item = new Item { ItemId = $"item_{i}", Name = $"Item {i}" };
_dbConnection.Insert(item);
}
});
查询数据
:
这是
sqlite-net
的亮点,支持LINQ和Lambda表达式,写起来非常直观。
// 1. 根据主键查询
PlayerSaveData player = _dbConnection.Find<PlayerSaveData>(1); // 查找 Id == 1 的记录
// 2. 使用LINQ查询
var highLevelPlayers = _dbConnection.Table<PlayerSaveData>()
.Where(p => p.Level > 10)
.OrderByDescending(p => p.Experience)
.ToList();
// 3. 执行原始SQL查询(更灵活,处理复杂联接时常用)
var items = _dbConnection.Query<Item>("SELECT * FROM Item WHERE Type = ? ORDER BY Name", ItemType.Consumable);
// 4. 查询单个值
int playerCount = _dbConnection.Table<PlayerSaveData>().Count();
string playerName = _dbConnection.ExecuteScalar<string>("SELECT PlayerName FROM PlayerSaveData WHERE Id = 1");
更新数据 :
player.Experience += 100f;
player.Level = Mathf.FloorToInt(player.Experience / 1000);
int updatedRows = _dbConnection.Update(player); // 根据主键更新
// 或者使用 InsertOrReplace
// _dbConnection.InsertOrReplace(player);
删除数据 :
// 删除特定对象
_dbConnection.Delete(player);
// 根据主键删除
_dbConnection.Delete<PlayerSaveData>(1);
// 条件删除
_dbConnection.Table<PlayerSaveData>().Where(p => p.Level < 5).Delete();
4. 高级用法与性能优化
4.1 使用事务保证数据一致性
当你需要执行一系列数据库操作(比如玩家购买物品:扣金币、加物品、记录日志)时,必须使用事务。事务确保这些操作要么全部成功,要么全部失败回滚,防止数据出现不一致的状态(比如金币扣了但物品没加上)。
try
{
_dbConnection.BeginTransaction(); // 开始事务
// 一系列操作...
_dbConnection.Update(playerGold);
_dbConnection.Insert(newItem);
_dbConnection.Insert(purchaseLog);
_dbConnection.Commit(); // 提交事务
Debug.Log("Purchase successful!");
}
catch (System.Exception ex)
{
_dbConnection.Rollback(); // 发生异常,回滚所有操作
Debug.LogError($"Transaction failed: {ex.Message}");
// 通知玩家购买失败
}
更简洁的写法是使用
RunInTransaction
方法,它自动处理开始、提交和回滚:
_dbConnection.RunInTransaction(() => {
_dbConnection.Update(playerGold);
_dbConnection.Insert(newItem);
_dbConnection.Insert(purchaseLog);
});
性能提示 :即使不是出于一致性要求,批量插入或更新数据时,将它们包裹在事务中也能极大提升速度(有时是数量级的提升),因为SQLite默认每条语句都是一个独立的事务,频繁提交会产生大量磁盘I/O。
4.2 索引与查询优化
随着数据量增大,查询速度可能会变慢。为经常用于
WHERE
、
ORDER BY
或
JOIN
条件的字段创建索引是首要优化手段。
在模型上添加索引 :
[Table("PlayerSave")]
public class PlayerSaveData
{
...
[Indexed] // 为PlayerName创建索引,加速按名字查找
public string PlayerName { get; set; }
[Indexed("IX_Level_Exp")] // 复合索引,同时优化按Level和Experience的查询
public int Level { get; set; }
[Indexed("IX_Level_Exp")]
public float Experience { get; set; }
}
索引会在调用
CreateTable
时自动创建。你也可以通过原始SQL手动创建更复杂的索引:
_dbConnection.Execute("CREATE INDEX IF NOT EXISTS idx_item_type ON Item (Type, MaxStack)");
编写高效查询 :
-
避免
SELECT *:只查询需要的列,减少数据读取量。 -
使用参数化查询
:防止SQL注入,且SQLite能更好地缓存查询计划。
sqlite-net的LINQ和Query<T>方法默认就是参数化的。 -
警惕N+1查询问题
:例如,先查玩家列表,再循环查每个玩家的物品。应使用
JOIN或批量查询代替。
4.3 处理复杂数据类型与迁移
存储复杂对象
:
SQLite直接支持的类型有限(整数、浮点数、字符串、BLOB)。对于
Vector3
、
Color
、
List<string>
这类复杂数据,有几种策略:
-
序列化为JSON(推荐)
:使用
JsonUtility.ToJson或第三方库(如Newtonsoft.Json)序列化后存为字符串字段。查询灵活,可读性好。player.InventoryJson = JsonUtility.ToJson(inventoryList); // 读取时 var inventory = JsonUtility.FromJson<List<InventorySlot>>(player.InventoryJson); - 拆分成多个关联表 :遵循数据库范式,建立外键关联。查询更规范,但复杂度高。
-
存储为BLOB
:对于纯二进制数据,可以使用
byte[]属性。
数据库迁移(Schema升级)
这是实际项目中最头疼的问题之一。你发布游戏后,发现需要给
PlayerSaveData
表增加一个
Coins
字段,怎么办?不能直接调用
CreateTable
,因为它会忽略已存在的表。
常见的解决方案:
-
手动ALTER TABLE
:在初始化代码中检查数据库版本(可以单独用一个表来存版本号),然后执行相应的SQL。
int currentDbVersion = GetDbVersion(); if (currentDbVersion < 2) { _dbConnection.Execute("ALTER TABLE PlayerSaveData ADD COLUMN Coins INTEGER DEFAULT 0"); SetDbVersion(2); } -
使用更高级的ORM库
:有些库内置了迁移功能,但
sqlite-net比较轻量,没有直接支持。 - 备份-重建法 :对于小型数据库,可以备份数据,删除旧表,用新结构创建表,再恢复数据。这需要仔细处理外键约束。
5. 跨平台部署实战与避坑指南
5.1 Android平台专项处理
Android是问题高发区,主要围绕数据库文件的部署和访问权限。
数据库文件首次部署
:
如前所述,你需要将
StreamingAssets
里的初始数据库文件复制到
Application.persistentDataPath
。关键是要用
UnityWebRequest
或
WWW
(旧版)进行异步复制,因为Android上不能直接用
File.Copy
访问APK内的文件。
private IEnumerator CopyDatabaseFromStreamingAssets()
{
string sourcePath = Path.Combine(Application.streamingAssetsPath, "MyGame.db");
string destPath = Path.Combine(Application.persistentDataPath, "MyGame.db");
if (File.Exists(destPath))
{
yield break; // 已存在,跳过
}
UnityWebRequest www = UnityWebRequest.Get(sourcePath);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
File.WriteAllBytes(destPath, www.downloadHandler.data);
Debug.Log("Database copied successfully.");
}
else
{
Debug.LogError($"Failed to copy database: {www.error}");
// 可以考虑在这里创建一个全新的空数据库
CreateFreshDatabase(destPath);
}
}
读写权限
:
Application.persistentDataPath
在Android上是应用私有目录,不需要任何权限申请。确保你的数据库文件最终路径在这里。
5.2 iOS平台注意事项
iOS除了前面提到的JIT限制,还有文件系统路径和后台处理的问题。
路径
:iOS的
Application.persistentDataPath
指向
Library/Application Support
或类似目录,同样无需权限。从
StreamingAssets
复制文件时,iOS可以直接使用
File.Copy
,因为文件在安装时已解压。
后台线程与Unity API
:如果你在后台线程进行数据库操作(这是推荐做法),
切记不能在后台线程中调用任何UnityEngine API
,包括
Debug.Log
、
GameObject.Find
等。这会导致应用崩溃。正确的做法是将耗时的数据库操作放在
Task.Run
或
Thread
中,通过回调、事件或
UnityMainThreadDispatcher
这样的工具将结果传回主线程再处理UI或日志。
Task.Run(() =>
{
var heavyData = _dbConnection.Table<LogEntry>().Where(l => l.Timestamp > someDate).ToList();
// 不能在后台线程调用 Debug.Log 或更新 GameObject
// 将 heavyData 传回主线程处理
UnityMainThreadDispatcher.Instance().Enqueue(() => OnDataLoaded(heavyData));
});
5.3 编辑器与Standalone平台
在Windows/Mac编辑器下开发是最简单的,路径直接,没有平台限制。但要注意:
-
路径中的斜杠
:使用
Path.Combine()来构建路径,它能自动处理不同操作系统的路径分隔符。 - 多线程调试 :在编辑器中测试多线程数据库操作时,如果崩溃,错误信息可能不直观。善用Try-Catch和日志。
5.4 常见问题排查速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编辑器运行正常,打包后报
DllNotFoundException
|
Assets/Plugins
下的原生库文件缺失或平台设置错误。
|
1. 检查
Assets/Plugins
文件夹是否完整包含所有平台子文件夹。
2. 在Unity中选中每个
.dll
/
.so
/
.bundle
文件,在Inspector中检查其“Platform Settings”是否正确勾选了目标平台(如Android, iOS, Standalone)。
|
iOS设备上运行崩溃,报
ExecutionEngineException
| 查询或映射触发了iOS禁止的JIT编译。 |
1. 简化数据模型,避免复杂嵌套和匿名类型映射。
2. 对出错的查询,改用原始SQL语句
_dbConnection.Query<T>("SELECT ...")
。
3. 确保使用的
sqlite-net
版本是经过AOT(预先编译)兼容处理的。
|
| Android设备上找不到数据库文件 |
1. 数据库文件未正确复制到可读写路径。
2. 路径拼写错误。 |
1. 确保执行了从
StreamingAssets
到
PersistentDataPath
的复制流程,并用日志输出两个完整路径检查。
2. 使用
adb shell
连接真机,查看应用数据目录下文件是否存在。
|
| 插入或更新数据很慢 |
1. 未使用事务进行批量操作。
2. 未对频繁查询的字段建立索引。 3. 每次操作都打开和关闭连接。 |
1. 将批量操作放入
RunInTransaction
。
2. 分析慢查询,为条件字段添加
[Indexed]
。
3. 保持一个全局的数据库连接,或使用连接池(轻量级ORM通常单连接即可)。 |
| 查询返回的数据与预期不符 |
1. 数据模型属性与表结构不匹配(如改名、改类型)。
2. SQL语句或LINQ条件写错。 |
1. 使用DB Browser for SQLite等工具直接打开数据库文件,检查表结构和实际数据。
2. 打开SQLite的Trace输出(如果插件支持),查看实际执行的SQL语句。 |
| 数据库文件被锁,无法写入 | 多个线程或进程同时尝试写入,且未正确关闭连接。 |
1. 确保数据库操作是串行的,或使用锁机制。
2. 检查代码,确保在所有可能退出或异常的地方都调用了
_dbConnection.Close()
。
3. SQLite默认支持多线程读,但写需要序列化。 |
6. 实战:构建一个简单的玩家数据管理系统
让我们把上面的知识整合起来,创建一个简单的、线程安全的玩家数据管理器。
using UnityEngine;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Concurrent;
public class GameDataService : MonoBehaviour
{
public static GameDataService Instance { get; private set; }
private SQLiteConnection _connection;
private string _databasePath;
private readonly ConcurrentQueue<System.Action> _mainThreadActions = new ConcurrentQueue<System.Action>();
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeDatabaseAsync();
}
void Update()
{
// 在主线程执行从后台线程排队的回调
while (_mainThreadActions.TryDequeue(out var action))
{
action?.Invoke();
}
}
private async void InitializeDatabaseAsync()
{
_databasePath = Path.Combine(Application.persistentDataPath, "GameData.db");
// 检查并复制初始数据库(如果需要)
await Task.Run(() => SetupDatabaseFile());
// 在主线程打开连接(某些Unity版本/平台下,SQLite初始化可能依赖主线程上下文)
_connection = new SQLiteConnection(_databasePath);
_connection.CreateTable<PlayerProfile>();
_connection.CreateTable<InventoryItem>();
Debug.Log("GameDataService initialized.");
}
private void SetupDatabaseFile()
{
// 如果持久化路径下没有数据库,尝试从StreamingAssets复制
if (!File.Exists(_databasePath))
{
string sourcePath = Path.Combine(Application.streamingAssetsPath, "GameData.db");
#if UNITY_ANDROID && !UNITY_EDITOR
// Android需要特殊处理,这里简化,实际应用需要UnityWebRequest
// 假设我们直接创建一个新库
CreateFreshDatabase(_databasePath);
#else
if (File.Exists(sourcePath))
{
File.Copy(sourcePath, _databasePath);
}
else
{
CreateFreshDatabase(_databasePath);
}
#endif
}
}
private void CreateFreshDatabase(string path)
{
// 创建一个全新的空数据库文件并建立表结构
using (var tempConn = new SQLiteConnection(path))
{
tempConn.CreateTable<PlayerProfile>();
tempConn.CreateTable<InventoryItem>();
// 可以插入一些默认数据
tempConn.Insert(new PlayerProfile { PlayerName = "DefaultPlayer" });
}
}
// 异步保存玩家数据示例
public void SavePlayerProfileAsync(PlayerProfile profile, System.Action<bool> callback)
{
Task.Run(() =>
{
bool success = false;
try
{
_connection.RunInTransaction(() => {
_connection.InsertOrReplace(profile);
});
success = true;
}
catch (System.Exception ex)
{
Debug.LogError($"Save failed: {ex.Message}");
}
// 将回调排队到主线程执行
_mainThreadActions.Enqueue(() => callback?.Invoke(success));
});
}
// 异步加载玩家数据示例
public void LoadPlayerProfileAsync(int playerId, System.Action<PlayerProfile> callback)
{
Task.Run(() =>
{
PlayerProfile profile = null;
try
{
profile = _connection.Find<PlayerProfile>(playerId);
}
catch (System.Exception ex)
{
Debug.LogError($"Load failed: {ex.Message}");
}
_mainThreadActions.Enqueue(() => callback?.Invoke(profile));
});
}
void OnApplicationQuit()
{
_connection?.Close();
_connection = null;
}
}
// 简化的数据模型
[Table("PlayerProfile")]
public class PlayerProfile
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string PlayerName { get; set; }
public int Gold { get; set; }
// ... 其他属性
}
[Table("InventoryItem")]
public class InventoryItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public int PlayerId { get; set; } // 外键
public string ItemId { get; set; }
public int Count { get; set; }
}
这个管理器实现了单例模式、异步操作、主线程回调以及基本的容错。在实际项目中,你还需要考虑更多,比如数据加密、备份机制、更复杂的查询需求等。但它的骨架已经足够稳健,可以作为大多数中小型Unity项目数据层的起点。
最后,我个人的体会是,
SQLite4Unity3d
加上
sqlite-net
的组合,在Unity本地数据存储方案中,在易用性、性能和稳定性之间取得了非常好的平衡。它可能不适合每秒需要处理上万次读写请求的硬核网游,但对于绝大多数单机、弱联网或需要缓存大量配置数据的游戏和应用来说,它完全够用,甚至绰绰有余。关键是要理解其原理,善用事务和索引,并妥善处理跨平台的坑。当你把这些都搞定之后,你会发现,在Unity里操作数据库也可以像写普通的C#对象一样简单流畅。

6189

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



