数据库 数据存储对于游戏开发来说是必不可少的,在常规的游戏开发中,后端服务器会采用MySQL(关系型数据库) ,MongoDB(非关系型数据库) 等作为数据库存储。
关于关系型和菲关系型数据库的学习理解参考:
网络通信和服务器学习
本章节是在没有后端但又有玩家数据存储需求的前提下诞生的,通过调研SQLite(关系型数据库) 正是定位本地高效数据库而非CS结构的数据库,所以本章节通过深入学习SQLite,弄懂如何正确高效的把本地玩家数据存储到本地数据库。
SQLite What 首先来看看SQLite官网的一个介绍:SQLite is not directly comparable to client/server SQL database engines such as MySQL, Oracle, PostgreSQL, or SQL Server since SQLite is trying to solve a different problem. Client/server SQL database engines strive to implement a shared repository of enterprise data. They emphasize scalability, concurrency, centralization, and control. SQLite strives to provide local data storage for individual applications and devices. SQLite emphasizes economy, efficiency, reliability, independence, and simplicity. SQLite does not compete with client/server databases. SQLite competes with fopen(). 从上面的介绍可以看出,SQLite(C语言编写的库)虽然也是关系型数据库,但他的定位和MySQL不一样,定位的是本地高效数据库而非CS结构的数据库。
Why 那么什么时候适用于SQLite?什么时候适用于MySQL(CS)了? SQLite官网也给出了明确的介绍,详情参见官网SQLite 这里只列举下官网介绍的什么时候适用于MYSQL:
Client/Server Applications(CS结构的程序—需要大量客户端服务器通信存储)
High-volume Websites(高吞吐量的网站)
Very large datasets(数据存储量大—数据大)
High Concurrency(高并发—很多人同时写入)
How 这里本人明确了暂时是打算用于单机版游戏的本地数据库,所以倾向于用SQLite来做。 接下来会接触以下几个知识点:
Unity(游戏开发引擎)
SQLite4Unity3d(第三方给Unity封装好的SQLite库—支持Window,Android(ARM64貌似也支持了)和IOS)
Navicat(可视化数据库工具)
SQL(数据库查询语言)
目标 :
编写一个数据库操作UI界面,支持数据库的增删改查操作(方便学习使用SQLite4Unity3d提供的接口访问SQLite数据库)
利用Navicat查看Window本地存储的数据库数据(学会Navicat的基本使用和数据库SQL相关操作)
打包编译Window和Android版本到真机上查看效果(确保多平台的兼容性)
SQLite4Unity3d实战 环境准备 : Unity版本:2018.4.8f1 SQLite4Unity3d: SQLite4Unity3d Navivat: Navicat Premium SQL:SQL学习1 or SQL学习2
Note:
SQLite4Unity3d uses only the synchronous part of sqlite-net, so all the calls to the database are synchronous. (SQLite4Unity3d有这么一句官方提示,可以看出所有的数据库操作都是同步的。这里考虑到都是单机本地存储同时数据量也不会很大,所以同步异步不是很重要,所以并不影响个人使用。 )
PC实战 接下来结合SQLite4Unity3d来实战学习数据库的增删改查等操作。第1步 创建空的Unity工程,制作简单的交互UI
第2步 拷贝SQLite相关库文件: 还要拷贝SQLite.cs(SQLite4Unity3d作者封装的访问SQLite的相关接口) 通过查看SQLite.cs的源代码可以看出,作者封装的这一层通过CS层多抽象了一层数据库访问(增删改查)和表格数据结构,然后结合反射(反射查找对应类的数据库)和Linq(实现快速的数据库查询)来实现通用的数据访问。
第3步 定义数据库表结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Player { [PrimaryKey] public int UID { get ; set ; } public string FirstName { get ; set ; } public string LastName { get ; set ; } public int Age { get ; set ; } public override string ToString ( ) { return $"[Player: UId={UID}, FirstName={FirstName}, LastName={LastName}, Age={Age}]" ; } }
第4步 建立SQLite数据库连接(不存在的话会根据Flag决定是否自动创建):
1 2 3 DatabaseFolderPath = $"{Application.persistentDataPath}/" ; mSQLiteConnect = new SQLiteConnection($"{DatabaseFolderPath}/{DatabaseName}" , SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
可以看到本来StreamingAssets目录下没有数据库文件的这一下就创建成功了。
第5步 数据库里创建玩家表:
1 mSQLiteConnect.CreateTable<Player>();
因为还没有数据所以看不到数据打印,通过Navicat打开数据库,我们可以看到已经成功创建了Player表:
第6步 玩家表插入玩家数据:
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 mSQLiteConnect.Insert(new Player { UID = 1 , FirstName = "Huan" , LastName = "Tang" , Age = 29 , }); mSQLiteConnect.Insert(new Player { UID = 3 , FirstName = "XiaoYun" , LastName = "Zhou" , Age = 28 , }); mSQLiteConnect.Insert(new Player { UID = 2 , FirstName = "Jiang" , LastName = "Fan" , Age = 28 , }); mSQLiteConnect.Insert(new Player { UID = 5 , FirstName = "ZhenLiang" , LastName = "Li" , Age = 29 , }); mSQLiteConnect.Insert(new Player { UID = 4 , FirstName = "XiaoLin" , LastName = "Kuang" , Age = 28 , });
第7步 玩家表删除指定玩家ID数据:
1 mSQLiteConnect.Delete<Player>(deleteuid);
第7步 玩家表修改指定玩家Age数据:
1 2 valideplayer.Age = newage; mSQLiteConnect.Update(valideplayer);
第8步 删除玩家表所有数据:
1 var rownumber = mSQLiteConnect.DeleteAll<Player>();
可以看到我们成功删除了玩家表里的所有数据,接下来用Navicat验证下数据库里的情况:
第9步 删除数据库里的玩家表:
1 var rownumberaffected = mSQLiteConnect.DropTable<Player>();
删除玩家表后通过Navicat查看数据库: 通过查看源码,我们可以看到,DraopTable底层实际是通过调用SQL的Drop语句来实现删除表的:
1 2 3 4 5 6 7 8 9 10 11 public int DropTable<T>(){ var map = GetMapping (typeof (T)); var query = string .Format("drop table if exists \"{0}\"" , map.TableName); return Execute (query); }
关于SQL更多学习参考:SQL 教程 SQL Tutorial 可以看到通过使用SQLite4Unity3d的SQLite.cs相关的接口,成功的实现了对于数据库的创建,以及数据库表的创建,增删改查等操作。
第10步 关闭数据库连接:
Android实战 为了验证SQLite4Unity3d的跨平台能力,接下来打包验证真机: Android真机数据库操作:
Android真机数据库文件查看(本人使用的ES文件浏览器 ): ![DatabaseViewAfterOperation]/img/Database/DatabaseViewAfterOperation.png)
Android真机数据库详细信息PC查看:
可以看到Android真机上数据库的一些基础操作都成功了的。IOS这里就暂时不验证了,根据Github上的介绍理论上是支持的。
Note:
真机可读取目录修改为Application.persistentDataPath,因为Application.streamingAssetsPath在真机上是只读目录。
真机使用ES文件浏览器 来查看程序目录下的数据库文件
SQLite4Unity3d进阶 学习了SQLite4Unity3d的基础使用,接下来要考虑的是实战使用时,如何封装一层数据库管理,方便游戏里的所有数据库数据都成功的加载读取修改保存。
这里主要是对于SQLite.cs的简单封装,用于支持多数据库创建,以及数据库创建路径管理,以及自定义数据库表的封装访问。
上代码之前,先简单看下封装的代码设计:
DatabaseManager.cs(对多数据库创建访问以及路径规划进行封装支持 )
BaseDatabase.cs(数据库基类抽象 —实现对数据库操作进行封装)
BaseTableData.cs(数据库表数据基类抽象 —实现对数据库表操作进行封装)
BaseIntTableData.cs(int做主键的表数据抽象 —实现对int主键表的操作封装)
BaseStringTableData.cs(string做主键的表数据抽象 —实现对string主键表的操作封装)
DatabaseManager.cs
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 namespace TH.Modules.Data { /// <summary> /// 数据库管理类 /// 主要功能如下: /// 1. 数据库路径处理(数据库存储管理) /// 2. 多数据库支持 /// 3. 数据库连接,关闭,CUID操作等操作依然使用SQLite的封装 /// </summary> public class DatabaseManager : SingletonTemplate<DatabaseManager> { /// <summary> /// 数据库文件夹地址 /// </summary> private readonly static string DatabaseFolderPath = Application.persistentDataPath + "/Database/"; /// <summary> /// 已连接的数据库映射Map /// Key为数据库名,Value为对应的数据库连接 /// </summary> private Dictionary<string, SQLiteConnection> ConnectedDatabaseMap; public DatabaseManager() { //检查数据库文件目录是否存在 if(!Directory.Exists(DatabaseFolderPath)) { Directory.CreateDirectory(DatabaseFolderPath); } ConnectedDatabaseMap = new Dictionary<string, SQLiteConnection>(); } /// <summary> /// 打开数据库连接 /// </summary> /// <param name="databasename"></param> /// <param name="openflags"></param> /// <param name="storeDateTimeAsTicks"></param> public void openDatabase(string databasename, SQLiteOpenFlags openflags = SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, bool storeDateTimeAsTicks = false) { if(!isDatabaseConnected(databasename)) { var databasepath = DatabaseFolderPath + databasename; var connection = new SQLiteConnection(databasepath, openflags, storeDateTimeAsTicks); ConnectedDatabaseMap.Add(databasename, connection); Debug.Log($"连接数据库:{databasename}"); } else { Debug.Log($"数据库:{databasename}已连接,请勿重复连接!"); } } /// <summary> /// 获取指定数据库连接 /// </summary> /// <param name="databasename"></param> /// <returns></returns> public SQLiteConnection getDatabaseConnection(string databasename) { SQLiteConnection connection; if (ConnectedDatabaseMap.TryGetValue(databasename, out connection)) { return connection; } else { return null; } } /// <summary> /// 关闭数据库连接 /// </summary> /// <param name="databasename"></param> public void closeDatabase(string databasename) { SQLiteConnection connection; if(ConnectedDatabaseMap.TryGetValue(databasename, out connection)) { connection.Close(); ConnectedDatabaseMap.Remove(databasename); Debug.Log($"关闭数据库:{databasename}"); } else { Debug.LogError($"未连接数据库:{databasename},关闭失败!"); } } /// <summary> /// 关闭所有已连接的数据库 /// </summary> public void closeAllDatabase() { foreach (var databasename in ConnectedDatabaseMap.Keys) { closeDatabase(databasename); } } /// <summary> /// 清除 /// </summary> public void clear() { closeAllDatabase(); } /// <summary> /// 指定数据库是否已连接 /// </summary> /// <param name="databasename"></param> /// <returns></returns> private bool isDatabaseConnected(string databasename) { return ConnectedDatabaseMap.ContainsKey(databasename); } #region 辅助方法 /// <summary> /// 获取指定数据库表的所有数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="databasename">数据库名</param> /// <returns></returns> public string getTableAllDatasInOneString<T>(string databasename) where T : new() { SQLiteConnection sqliteconnection = getDatabaseConnection(databasename); if (sqliteconnection != null) { var querytable = sqliteconnection.Table<T>(); if (querytable != null) { var result = string.Empty; foreach (var data in querytable) { result += data.ToString(); result += "\n"; } return result; } else { return string.Empty; } } else { return string.Empty; } } #endregion } }
TODO:
现阶段是默认存在包外的persistentDataPath,对于数据库如何做好加密管理避免被轻易修改,这个问题还有待学习思考。
数据库表是否存在还没找到方式判定(暂时是从流程上确保表存在的)
BaseDatabase.cs
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 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 namespace TH.Modules.Data { public abstract class BaseDatabase { protected SQLiteConnection mSQLiteConnect; public virtual string DatabaseName { get { return $"{GetType().Name}.db" ; } } protected BaseDatabase ( ) { } public void LoadDatabase ( ) { mSQLiteConnect = DatabaseManager.Singleton.openDatabase(DatabaseName); } public bool IsConnected ( ) { return mSQLiteConnect != null ; } public bool CreateTable<T>() where T : BaseTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,创建类名:{typeof(T).Name}表失败!" ); return false ; } mSQLiteConnect.CreateTable<T>(); return true ; } public bool InsertDataI<T>(T data) where T : BaseIntTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } mSQLiteConnect.Insert(data); return true ; } public bool InsertOrReplaceDataI<T>(T data) where T : BaseIntTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } mSQLiteConnect.InsertOrReplace(data); return true ; } public bool InsertDataS<T>(T data) where T : BaseStringTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } mSQLiteConnect.Insert(data); return true ; } public bool InsertOrReplaceDataS<T>(T data) where T : BaseStringTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } mSQLiteConnect.InsertOrReplace(data); return true ; } public bool DeleteDataByUIDI<T>(int uid) where T : BaseIntTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,删除类名:{typeof(T).Name}表UID:{uid}数据失败!" ); return false ; } var deletedNumber = mSQLiteConnect.Delete<T>(uid); Debug.Log($"数据库:{DatabaseName},删除类名:{typeof(T).Name}表UID:{uid}数量:{deletedNumber}" ); return deletedNumber > 0 ; } public bool DeleteDataByUIDS<T>(string uid) where T : BaseStringTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,删除类名:{typeof(T).Name}表数据失败!" ); return false ; } var deletedNumber = mSQLiteConnect.Delete<T>(uid); Debug.Log($"数据库:{DatabaseName},删除类名:{typeof(T).Name}表UID:{uid}数量:{deletedNumber}" ); return deletedNumber > 0 ; } public bool UpdateDataI<T>(T data) where T : BaseIntTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } var updatedNumber = mSQLiteConnect.Update(data); Debug.Log($"数据库:{DatabaseName},更新类名:{typeof(T).Name}表UID:{data.UID}数量:{updatedNumber}" ); return updatedNumber > 0 ; } public bool UpdateDataS<T>(T data) where T : BaseStringTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,插入类名:{typeof(T).Name}表数据失败!" ); return false ; } var updatedNumber = mSQLiteConnect.Update(data); Debug.Log($"数据库:{DatabaseName},更新类名:{typeof(T).Name}表UID:{data.UID}数量:{updatedNumber}" ); return updatedNumber > 0 ; } public T GetDataByUIDI<T>(int uid) where T : BaseIntTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,获取类名:{typeof(T).Name}表数据失败!" ); return null ; } var data = mSQLiteConnect.Find<T>((databaseTable) => databaseTable.UID == uid); if (data == null ) { Debug.LogError($"数据库:{DatabaseName},获取类名:{typeof(T).Name}的UID:{uid}表数据不存在!" ); return null ; } return data; } public T GetDataByUIDS<T>(string uid) where T : BaseStringTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,获取类名:{typeof(T).Name}表数据失败!" ); return null ; } var data = mSQLiteConnect.Find<T>((databaseTable) => databaseTable.UID == uid); if (data == null ) { Debug.LogError($"数据库:{DatabaseName},获取类名:{typeof(T).Name}的UID:{uid}表数据不存在!" ); return null ; } return data; } public TableQuery<T> GetAllData<T>() where T : BaseTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,获取类名:{typeof(T).Name}表数据失败!" ); return null ; } var tbData = mSQLiteConnect.Table<T>(); if (tbData == null ) { Debug.LogError($"数据库:{DatabaseName},获取类名:{typeof(T).Name}表所有数据不存在!" ); return null ; } return tbData; } public int DeleteTableAll<T>() where T : BaseTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,删除类名:{typeof(T).Name}表所有数据失败!" ); return 0 ; } var deletedNumber = mSQLiteConnect.DeleteAll<T>(); Debug.Log($"数据库:{DatabaseName},删除类名:{typeof(T).Name}表所有数据数量:{deletedNumber}" ); return deletedNumber; } public int DeleteTable<T>() where T : BaseTableData, new () { if (!IsConnected()) { Debug.LogError($"数据库:{DatabaseName}未连接,删除类名:{typeof(T).Name}表失败!" ); return 0 ; } var deletedNumber = mSQLiteConnect.DropTable<T>(); Debug.Log($"数据库:{DatabaseName},删除类名:{typeof(T).Name}表数量:{deletedNumber}" ); return deletedNumber; } public void CloseDatabase ( ) { DatabaseManager.Singleton.closeDatabase(DatabaseName); mSQLiteConnect = null ; } } }
BaseTableData.cs
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 namespace TH.Modules.Data { public abstract class BaseTableData { public string TableName { get { return GetType().Name; } } public BaseTableData ( ) { } public override string ToString ( ) { return $"[TableName:{TableName}]" ; } } }
BaseIntTableData.cs
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 namespace TH.Modules.Data { public abstract class BaseIntTableData : BaseTableData { [PrimaryKey] public int UID { get ; set ; } public BaseIntTableData ( ) : base ( ) { } public BaseIntTableData (int uid ) : base ( ) { UID = uid; } public override string ToString ( ) { return $"[TableName:{TableName} UID:{UID}]" ; } } }
BaseStringTableData.cs
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 namespace TH.Modules.Data { public abstract class BaseStringTableData : BaseTableData { [PrimaryKey] public string UID { get ; set ; } public BaseStringTableData ( ) : base ( ) { } public BaseStringTableData (string uid ) : base ( ) { UID = uid; } public override string ToString ( ) { return $"[TableName:{TableName} UID:{UID}]" ; } } }
GameDatabase.cs(自定义游戏数据库)
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 public class GameDatabase : BaseDatabase { public static GameDatabase Singleton { get { if (mSingleton != null ) { return mSingleton; } mSingleton = new GameDatabase(); return mSingleton; } } private static GameDatabase mSingleton; public GameDatabase ( ) : base ( ) { } }
Player.cs(自定义玩家数据表结构)
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 public class Player : BaseIntTableData { public string FirstName { get ; set ; } public string LastName { get ; set ; } [Indexed] public int Age { get ; set ; } public Player ( ) : base ( ) { } public Player (int uid, string firstName, string lastName, int age ) : base (uid ) { FirstName = firstName; LastName = lastName; Age = age; } public override string ToString ( ) { return $"[Player: UID={UID}, FirstName={FirstName}, LastName={LastName}, Age={Age}]" ; } }
进阶版的实战代码见Github源码:
SQLiteStudy
结论
SQLite适用于非CS结构,存储数据量不大,不会多人并发操作数据库的情况(好比单机游戏的数据库)。
数据量大或者多人操作频繁或者需要CS架构支持,更适合MySQL+Redis
SQLite4Unity3d通过反射和Linq对SQLite的封装实现了对于SQL无感和ORM(Object Relational Mapping)数据库访问方式。
SQLite4Unity3d支持多平台,适合单机游戏数据存储
Reference SQLite4Unity3d
Github地址 SQLiteStudy