Unity数据持久化和SQlite
1. 比较PlayerPrefs和SQlite
一般来说数据持久化[1]用Unity提供的PlayerPrefs就可以了(实际是不应该的,后面会讲),但PlayerPrefs只支持int、string、float三种数据类型,只能保存一些简单的键值对(即哈希表),如果要保存复杂数据类型,则需要自己组织存储格式,比如用逗号将数据分隔开用来表示一个list,非常容易出错。这种键值对的表示方式是非关系型数据库,优点是速度快,缺点是数据之间缺乏联系,数据无结构化。比如有一些物品叫BlueGem、GreenGem、RedGem,他们都是item,为了保存这些道具的数量和时效,需要用
item_BlueGem_Count 12
item_BlueGem_Time 1
或者
item_BlueGem 12,1
的形式表示一个叫BlueGem的item有12个且1天后会过期,item前缀表示这个key的类型是item,BlueGem表示具体的物品,Count和Time表示物品不同的属性。如果按第一种做法,为了保存物品的两个属性就需要新加两个记录,如果物品属性有10个甚至更多,那保存的数据量将会非常庞大,而且非常不好维护。如果按第二种做法,用逗号分隔不同数据,这样做的问题在于整个值都是字符串,需要对这个字符串进行解析,而且由于它本身没有含义,就需要在文档里定义不同位置的值代表什么,这样很容易由于文档更新不及时,导致数据错乱。
将PlayerPrefs用来存储游戏存档的用法实际上偏离了Unity设计PlayerPrefs的初衷[2]。PlayerPrefs全称Player Preference,这里的player指的是游戏运行时,PlayerPrefs用来存储一些简单的游戏配置,比如是否全屏、分辨率、帧率、画面品质等,而不应该存储游戏存档。在windows平台,PlayerPrefs的数据会保存在注册表里,HKEY_CURRENT_USER\Software\<company_name>\<product_name>,注册表是Windows中的一个重要的数据库,用于存储系统和应用程序的设置信息[3],所以是不适合存储游戏数据的。正确的方法是将游戏存档保存为一个文件放在Application.persistentDataPath目录下。
为了加强数据间的联系,集中管理一类有共性的值(比如都是item),自然而然想到要使用关系型数据库,而且是可以本地读取的数据库,SQlite就是不错的选择。关系型数据库以行和列的形式表示数据,不同的列有不同的预定义含义,不同的行表示单独一条数据。比如针对上面的例子
item table
item_name | item_count | item_time |
BlueGem | 12 | 1 |
RedGem | 22 | 4 |
GreenGem | 100 | 7 |
比键值对的保存方式要好很多。同时通过使用数据库查看工具比如DB Browser、sqlitestudio,能够可视化的管理本地存储的数据库,这点非常方便。
SQlite的使用非常广泛,比如VS2015 Update 2以后 intellisense 使用sqlite代替了原先的*.sdf SQL Server Compact数据库,用来支持智能感知、代码恢复等功能[4]。
2. 使用SQlite
Unity导入sqlite需要三样东西
- Mono.Data.Sqlite.dll 和 System.Data.dll 用来提供native dll到C#的绑定以及针对不同平台的处理,在Unity安装目录Editor\Data\Mono\lib\mono\2.0\ 下能找到这两个dll
- SQlite运行库,从 https://www.sqlite.org/download.html 这里下载,通常需要
- Precompiled Binaries for Windows (x86和x86_64)
- Precompiled Binaries for Android
(IOS平台不需要SQlite运行库)
在游戏中,数据库是用来作为本地存储的,在进入游戏的loading阶段对数据库进行解析(读档),将数据全部读出来存在内存里,而不是每次查找\修改\删除都对数据库直接进行操作,因为这些操作是很耗时的,只需要在退出游戏前将内存中的数据写回数据库(存档)即可。
下面是一个处理SQlite数据库的方法类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
using UnityEngine; using System.Collections; using Mono.Data.Sqlite; using System; public class SqliteHelper { /// <summary> /// 数据库连接定义 /// </summary> private SqliteConnection dbConnection; /// <summary> /// SQL命令定义 /// </summary> private SqliteCommand dbCommand; /// <summary> /// 数据读取定义 /// </summary> private SqliteDataReader dataReader; /// <summary> /// 构造函数 /// </summary> /// <param name="connectionString">数据库连接字符串</param> public SqliteHelper(string connectionString) { try { //构造数据库连接 dbConnection = new SqliteConnection(connectionString); //打开数据库 dbConnection.Open(); } catch (Exception e) { Debug.Log(e.Message); } } /// <summary> /// 执行SQL命令 /// </summary> /// <returns>The query.</returns> /// <param name="queryString">SQL命令字符串</param> public SqliteDataReader ExecuteQuery(string queryString) { dbCommand = dbConnection.CreateCommand(); dbCommand.CommandText = queryString; dataReader = dbCommand.ExecuteReader(); return dataReader; } /// <summary> /// 关闭数据库连接 /// </summary> public void CloseConnection() { //销毁Command if (dbCommand != null) { dbCommand.Cancel(); } dbCommand = null; //销毁Reader if (dataReader != null) { dataReader.Close(); } dataReader = null; //销毁Connection if (dbConnection != null) { dbConnection.Close(); } dbConnection = null; } /// <summary> /// 读取整张数据表 /// </summary> /// <returns>The full table.</returns> /// <param name="tableName">数据表名称</param> public SqliteDataReader ReadFullTable(string tableName) { string queryString = "SELECT * FROM " + tableName; return ExecuteQuery(queryString); } /// <summary> /// 向指定数据表中插入数据 /// </summary> /// <returns>The values.</returns> /// <param name="tableName">数据表名称</param> /// <param name="values">插入的数值</param> public SqliteDataReader InsertValues(string tableName, string[] values) { //获取数据表中字段数目 int fieldCount = ReadFullTable(tableName).FieldCount; //当插入的数据长度不等于字段数目时引发异常 if (values.Length != fieldCount) { throw new SqliteException("values.Length!=fieldCount"); } string queryString = "INSERT INTO " + tableName + " VALUES (" + values[0]; for (int i = 1; i < values.Length; i++) { queryString += ", " + values[i]; } queryString += " )"; return ExecuteQuery(queryString); } /// <summary> /// 更新指定数据表内的数据 /// </summary> /// <returns>The values.</returns> /// <param name="tableName">数据表名称</param> /// <param name="colNames">字段名</param> /// <param name="colValues">字段名对应的数据</param> /// <param name="key">关键字</param> /// <param name="value">关键字对应的值</param> public SqliteDataReader UpdateValues(string tableName, string[] colNames, string[] colValues, string key, string operation, string value) { //当字段名称和字段数值不对应时引发异常 if (colNames.Length != colValues.Length) { throw new SqliteException("colNames.Length!=colValues.Length"); } string queryString = "UPDATE " + tableName + " SET " + colNames[0] + "=" + colValues[0]; for (int i = 1; i < colValues.Length; i++) { queryString += ", " + colNames[i] + "=" + colValues[i]; } queryString += " WHERE " + key + operation + value; return ExecuteQuery(queryString); } /// <summary> /// 删除指定数据表内的数据 /// </summary> /// <returns>The values.</returns> /// <param name="tableName">数据表名称</param> /// <param name="colNames">字段名</param> /// <param name="colValues">字段名对应的数据</param> public SqliteDataReader DeleteValuesOR(string tableName, string[] colNames, string[] operations, string[] colValues) { //当字段名称和字段数值不对应时引发异常 if (colNames.Length != colValues.Length || operations.Length != colNames.Length || operations.Length != colValues.Length) { throw new SqliteException("colNames.Length!=colValues.Length || operations.Length!=colNames.Length || operations.Length!=colValues.Length"); } string queryString = "DELETE FROM " + tableName + " WHERE " + colNames[0] + operations[0] + colValues[0]; for (int i = 1; i < colValues.Length; i++) { queryString += "OR " + colNames[i] + operations[0] + colValues[i]; } return ExecuteQuery(queryString); } /// <summary> /// 删除指定数据表内的数据 /// </summary> /// <returns>The values.</returns> /// <param name="tableName">数据表名称</param> /// <param name="colNames">字段名</param> /// <param name="colValues">字段名对应的数据</param> public SqliteDataReader DeleteValuesAND(string tableName, string[] colNames, string[] operations, string[] colValues) { //当字段名称和字段数值不对应时引发异常 if (colNames.Length != colValues.Length || operations.Length != colNames.Length || operations.Length != colValues.Length) { throw new SqliteException("colNames.Length!=colValues.Length || operations.Length!=colNames.Length || operations.Length!=colValues.Length"); } string queryString = "DELETE FROM " + tableName + " WHERE " + colNames[0] + operations[0] + colValues[0]; for (int i = 1; i < colValues.Length; i++) { queryString += " AND " + colNames[i] + operations[i] + colValues[i]; } return ExecuteQuery(queryString); } /// <summary> /// 创建数据表 /// </summary> + /// <returns>The table.</returns> /// <param name="tableName">数据表名</param> /// <param name="colNames">字段名</param> /// <param name="colTypes">字段名类型</param> public SqliteDataReader CreateTable(string tableName, string[] colNames, string[] colTypes) { string queryString = "CREATE TABLE " + tableName + "( " + colNames[0] + " " + colTypes[0]; for (int i = 1; i < colNames.Length; i++) { queryString += ", " + colNames[i] + " " + colTypes[i]; } queryString += " ) "; return ExecuteQuery(queryString); } /// <summary> /// Reads the table. /// </summary> /// <returns>The table.</returns> /// <param name="tableName">Table name.</param> /// <param name="items">Items.</param> /// <param name="colNames">Col names.</param> /// <param name="operations">Operations.</param> /// <param name="colValues">Col values.</param> public SqliteDataReader ReadTable(string tableName, string[] items, string[] colNames, string[] operations, string[] colValues) { string queryString = "SELECT " + items[0]; for (int i = 1; i < items.Length; i++) { queryString += ", " + items[i]; } queryString += " FROM " + tableName + " WHERE " + colNames[0] + " " + operations[0] + " " + colValues[0]; for (int i = 0; i < colNames.Length; i++) { queryString += " AND " + colNames[i] + " " + operations[i] + " " + colValues[0] + " "; } return ExecuteQuery(queryString); } } |
初始化时调用
dbConnection = new SqliteConnection(path);
时会在对应的path创建连接,如果没有对应的数据库,则会创建一个。在游戏的初始化流程里建立数据库连接,如果不存在数据库的话就新建一个,并创建数据表。存储数据库的路径最好是Application.persistentDataPath,这样能在游戏更新间保证数据持久存在。
需要注意的是数据库路径在不同平台有不同的规定,在windows和IOS需要用
"data source=" + Application.persistentDataPath + "/" + dbFileName;
Android平台需要用
"URI=file:" + Application.persistentDataPath + "/" + dbFileName;
在这里下载SQlite示例。
参考资料
[1] 数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称. 数据模型可以是任何数据结构或对象模型,存储模型可以是关系模型、XML、二进制流等。
[2] https://docs.unity3d.com/ScriptReference/PlayerPrefs.html
[3] 在Windows 3.x操作系统中,注册表是一个极小文件,其文件名为Reg.dat,里面只存放了某些文件类型的应用程序关联,大部分的设置是被放在win.ini、system.ini等多个初始化ini文件中。由于这些初始化文件不便于管理和维护,时常出现一些因ini文件遭到破坏而导致系统无法启动的问题。为了使系统运行得更为稳定、健壮,Windows 95/98/me设计师们借用了Windows NT中的注册表的思想,将注册表概念引入到Windows 95/98/me操作系统中,而且将ini文件中的大部分设置也移植到注册表中,因此,注册表在Windows 95/98/me等操作系统的启动、运行过程中起着重要的作用。
[4] https://stackoverflow.com/questions/36407386/what-is-the-vc-db-file-in-visual-studio-projects