【Unity百宝箱】游戏中的用户数据存档
【Unity百宝箱】游戏中的用户数据存档
Hi 大家好,我是游戏区Bug打工人小棋。
在联网游戏中,往往会把一些用户核心资产信息存储在服务器端,等到用户登录时由服务器下发给用户进行初始化。而单机游戏则往往更加简单,只需要将这些数据序列化保存在本地即可(文本形式)。
今天小棋给大家分享一套简单易用的本地存储框架,希望对同学们有所帮助。
框架设计
我们首先定义一个管理类:LocalConfig.cs
,专门用于管理本地化数据。
接着创建玩家数据类:UserData
,他包含用户基础信息:姓名
和等级
。
public class UserData
{
public string name;
public int level;
}
这里主要做两件事:
- 将内存中的用户数据进行序列化,以文本格式保存在本地
- 将文本格式从硬盘中读取出来,反序列化为内存中的数据
分别对应代码中的:
- SaveUserData
- LoadUserData
public class LocalConfig
{
public static void SaveUserData(UserData userData)
{
// 保存用户数据为文本
}
public static UserData LoadUserData(string userName)
{
// 读取用户数据到内存
}
}
工具选用
在填写上述代码之前,我们需要先做一些调研和准备工作。
- 首先解决第一个问题:存取数据,存在哪?从哪取?
Unity中为我们提供了许多特殊文件路径,经过我与ChatGPT
一分钟的愉快交流后,我了解到Unity中的PersistentDataPath
是一个不错的选择。
这个路径是一个可读写的目录,符合我们的基本需求。根据官方文档的描述,不同平台下他对应的路径有所不同,但是我们可以直接通过Unity为我们提供的:Application.persistentDataPath
来获取到最终路径,非常方便。
- 接下去是第二个问题:如何进行序列化和反序列化
这个名字对于初学者可能有些绕口,但其实非常简单,用通俗的话讲就是:把C#数据转化为文本,以及将文本转化为C#代码。
业界对于序列化已经有非常成熟的方案,比如json、bson等等,你甚至可以自己写一套序列化和反序列化框架。
本节课程我们选用json,它支持数字、字符串、列表、字典等数据结构,对于大多数游戏来说已经完全够用了。
- 最后一个问题:选用哪个Json框架
虽然官方推荐使用JsonUtility
,但是这里我并不打算使用他。原因是他仅支持能显示在inspector窗口中的数据格式,也就是说字典、以及一些嵌套的数据结构都无法使用,这会给我们带来很多麻烦。
因此我最终选用的框架是:using Newtonsoft.Json;
在使用上只需要关注两个方法:
- 序列化:
JsonConvert.SerializeObject
- 反序列化:
JsonConvert.DeserializeObject
非常简单易用。
逻辑书写
最后让我们来书写存取框架的具体逻辑叭~
// 用于文件读写
using System.IO;
// 用于json序列化和反序列化
using Newtonsoft.Json;
// Application.persistentDataPath配置在这里
using UnityEngine;
public class LocalConfig
{
// 保存用户数据为文本
public static void SaveUserData(UserData userData)
{
// 在persistentDataPath下再创建一个/users文件夹,方便管理
if (!File.Exists(Application.persistentDataPath + "/users"))
{
System.IO.Directory.CreateDirectory(Application.persistentDataPath + "/users");
}
// 转换用户数据为JSON字符串
string jsonData = JsonConvert.SerializeObject(userData);
// 将JSON字符串写入文件中(文件名为userData.name)
File.WriteAllText(Application.persistentDataPath + string.Format("/users/{0}.json", userData.name), jsonData);
}
// 读取用户数据到内存
public static UserData LoadUserData(string userName)
{
string path = Application.persistentDataPath + string.Format("/users/{0}.json", userName);
// 检查用户配置文件是否存在
if (File.Exists(path))
{
// 从文本文件中加载JSON字符串
string jsonData = File.ReadAllText(path);
// 将JSON字符串转换为用户内存数据
UserData userData = JsonConvert.DeserializeObject<UserData>(jsonData);
usersData[userName] = userData;
return userData;
}
else
{
return null;
}
}
}
框架使用
下面我们来验证下这套框架的使用效果,这里我书写了两个GM指令。
此处使用到了MenuItem
这个特性,帮助我们在编辑器窗口生成快捷按钮。
SaveLocalConfig
: 保存名称为xiaoqi0-4
的用户数据GetLocalConfig
: 读取名称为xiaoqi0-4
的用户数据,并打印数据
using UnityEditor;
using UnityEngine;
class GMCmd
{
[MenuItem("GMCmd/SaveLocalConf")]
public static void SaveLocalConfig()
{
for (int i = 0; i < 5; i++)
{
UserData userData = new UserData();
userData.name = "xiaoqi" + i.ToString();
userData.level = i;
LocalConfig.SaveUserData(userData);
}
Debug.Log("Save End!!!!!!!!!!!!");
}
[MenuItem("GMCmd/GetLocalConfig")]
public static void GetLocalConfig()
{
for (int i = 0; i < 5; i++)
{
string name = "xiaoqi" + i.ToString();
UserData userData = LocalConfig.LoadUserData(name);
Debug.Log(userData.name);
Debug.Log(userData.test);
}
}
}
- 点击保存数据:
SaveLocalConf
根据官方文档的说明,我们知道在 windows 上Application.persistentDataPath
对应:
Windows Store Apps: Application.persistentDataPath points to C:\Users\<user>\AppData\LocalLow\<company name>.
而这个<company name>
在 projecting setting 中可以找到:
因此我最终找到我保存的Json文件在这里:
文本内容也与我们预期的一致:
{"name":"xiaoqi0","level":0,"test":{}}
- 点击读取数据:
GetLocalConfig
最终结果如下:
和预期效果一致~
至此存取框架的主体部分已经完成,下面我们对这套框架进行优化。
框架优化
由于IO操作涉及到硬盘读写,性能较慢,我们可以对已经读取过的数据进行缓存。
// 修改0:新增引用命名空间
using System.Collections.Generic;
public class LocalConfig
{
// 修改1:增加usersData放在内存中
public static Dictionary<string, UserData> usersData = new Dictionary<string, UserData>();
// 保存用户数据文本
public static void SaveUserData(UserData userData)
{
// ...
// 修改2:保存缓存数据
usersData[userData.name] = userData;
// ...
}
public static UserData LoadUserData(string userName)
{
// 修改3:读取时,如果userData已经存在,就直接使用
// ...
}
}
数据加密
一些聪明的玩家,可以根据我们保存的json字段猜测其语义,比如直接修改level=100
,这样无异于开挂,因此对于上线的游戏,我们还需要对数据进行加密处理。
这里我演示下最简单的一种亦或加密法。
首先介绍下亦或操作,简单理解就是:
- 当两次输入不同时得到结果为1,即正确
- 当两次输入相同时得到结果为0,即错误
输入 | 运算符 | 输入 | 结果 |
---|---|---|---|
1 | ^ | 1 | 0 |
1 | ^ | 0 | 1 |
0 | ^ | 1 | 1 |
0 | ^ | 0 | 0 |
这个运算符有个特性,就是对任意输入进行两次相同的亦或,会复原结果。
比如:
假设一个数为 0
- 第一步:0^1 = 1
- 第二步:1^1 = 0 (复原结果)
建设一个数为 1
- 第一步:1^1 = 0
- 第二步:0^1 = 1 (复原结果)
利用这个性质,我们可以将保存的文本文件进行首次亦或,得到乱码数据,这样玩家看到的就是乱码。
然后当我们读取数据的时候再次进行一次亦或,即可复原数据,这样我们看到的就是正确数据。
废话不多说,上代码:
public class LocalConfig
{
// 随便选取一些用于亦或的字符(看自己喜欢:注意保密)
public static char[] keyChars = { 'a', 'b', 'c', 'd', 'e' };
// 加密
public static string Encrypt(string data)
{
char[] dataChars = data.ToCharArray();
for (int i = 0; i < dataChars.Length; i++)
{
char dataChar = dataChars[i];
char keyChar = keyChars[i % keyChars.Length];
// 重点:通过亦或得到新的字符
char newChar = (char)(dataChar ^ keyChar);
dataChars[i] = newChar;
}
return new string(dataChars);
}
// 解密
public static string Decrypt(string data)
{
// 两次亦或执行的是同样的操作
return Encrypt(data);
}
// 修改:存数据的时候进行第一次亦或
public static void SaveUserData(UserData userData)
{
// ...
string jsonData = JsonConvert.SerializeObject(userData);
jsonData = Encrypt(jsonData);
// ...
}
// 修改:存数据的时候进行第二次亦或(复原数据)
public static UserData LoadUserData(string userName)
{
// ...
if (File.Exists(path))
{
string jsonData = File.ReadAllText(path);
jsonData = Decrypt(jsonData);
// ...
}
// ...
}
}
测试结果如下:
- 保存数据到本地
可以看到保存后的数据是乱码,玩家再也没办法开挂了!!!
- 读取数据到内存
可以看到最后读取的数据复原成功了
总结
本文通过框架设计、工具选用、逻辑书写、框架使用、框架优化、数据加密这六部分内容,层层剖析,向大家介绍了一种简单易用的本地化存取框架,希望能对大家有所帮助。
最后将成果代码贴出来,由于还没有经过项目实践,仅仅是理论分享,如果代码有疏漏欢迎交流指正。
- 框架部分
// 用于文件读写
using System.IO;
// 用于json序列化和反序列化
using Newtonsoft.Json;
// Application.persistentDataPath配置在这里
using UnityEngine;
// 修改0:使用字典命名空间
using System.Collections.Generic;
public class LocalConfig
{
// 修改1:增加usersData缓存数据
public static Dictionary<string, UserData> usersData = new Dictionary<string, UserData>();
// 加密1:选择一些用于亦或操作的字符(注意保密)
public static char[] keyChars = {'a', 'b', 'c', 'd', 'e'};
// 加密2: 加密方法
public static string Encrypt(string data)
{
char [] dataChars = data.ToCharArray();
for (int i=0; i<dataChars.Length; i++)
{
char dataChar = dataChars[i];
char keyChar = keyChars[i % keyChars.Length];
// 重点: 通过亦或得到新的字符
char newChar = (char)(dataChar ^ keyChar);
dataChars[i] = newChar;
}
return new string(dataChars);
}
// 加密3: 解密方法
public static string Decrypt(string data)
{
return Encrypt(data);
}
// 保存用户数据文本
public static void SaveUserData(UserData userData)
{
// 在persistentDataPath下创建一个/users文件夹,方便管理
if(!File.Exists(Application.persistentDataPath + "/users"))
{
System.IO.Directory.CreateDirectory(Application.persistentDataPath + "/users");
}
// 修改2:保存缓存数据
usersData[userData.name] = userData;
// 转换用户数据为JSON字符串
string jsonData = JsonConvert.SerializeObject(userData);
#if UNITY_EDITOR
// 加密4
jsonData = Encrypt(jsonData);
#endif
// 将JSON字符串写入文件中(文件名为userData.name)
File.WriteAllText(Application.persistentDataPath + string.Format("/users/{0}.json", userData.name), jsonData);
}
// 读取用户数据到内存
public static UserData LoadUserData(string userName)
{
// 修改3: 率先从缓存中取数据,而不是从文本文件中读取
if(usersData.ContainsKey(userName))
{
return usersData[userName];
}
string path = Application.persistentDataPath + string.Format("/users/{0}.json", userName);
// 检查用户配置文件是否存在
if(File.Exists(path))
{
// 从文本文件中加载JSON字符串
string jsonData = File.ReadAllText(path);
#if UNITY_EDITOR
// 加密5
jsonData = Decrypt(jsonData);
#endif
// 将JSON字符串转换为用户内存数据
UserData userData = JsonConvert.DeserializeObject<UserData>(jsonData);
return userData;
}
else
{
return null;
}
}
}
public class UserData
{
public string name;
public int level;
}
- 使用案例
using UnityEngine;
using UnityEditor;
public class GMCmd
{
[MenuItem("CMCmd/SaveLocalConfig")]
public static void SaveLocalConfig()
{
for (int i=0; i<5; i++)
{
UserData userData = new UserData();
userData.name = "xiaoqi" + i.ToString();
userData.level = i;
LocalConfig.SaveUserData(userData);
}
Debug.Log("Save End !!!!!!!!!!!!!!!!!!!!");
}
[MenuItem("CMCmd/LoadLocalConfig")]
public static void LoadLocalConfig()
{
for (int i = 0; i < 5; i++)
{
string name = "xiaoqi" + i.ToString();
UserData userData = LocalConfig.LoadUserData(name);
Debug.Log(userData.name);
Debug.Log(userData.level);
}
}
}
- 最终路径
参考官方文档
https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html
- tips
在编辑器模式下,我们不需要对数据进行加密解密,这会影响到我们的开发效率,可以使用UNITY_EDITOR
这个宏进行判断,具体逻辑参考上文代码。
#if UNITY_EDITOR
jsonData = Decrypt(jsonData);
#endif
2023/4/16补充
bili 沃忆同学提出,对于Vector3,JsonConvert并不支持序列化,可以使用下面这种方法添加序列化方式
最后记得调用下:
private void Start()
{
AddSerializedJson.AddAllConverter();
}
序列化方式定义
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace Lyf.SaveSystem
{
public static class AddSerializedJson
{
public static void AddAllConverter()
{
AddVector3Converter();
}
private static void AddVector3Converter()
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = { new Vector3Converter() }
};
}
}
public class Vector3Converter : JsonConverter // 用于将Vector3序列化转换为Json
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var vector = (Vector3)value;
var obj = new JObject
{
{ "x", vector.x },
{ "y", vector.y },
{ "z", vector.z }
};
obj.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var x = (float)obj["x"];
var y = (float)obj["y"];
var z = (float)obj["z"];
return new Vector3(x, y, z);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(UnityEngine.Vector3);
}
}
}
最后
本文洋洋洒洒写了一万两千多字,绝对是干货中的干货,希望对大家有所帮助,请大家多多点赞收藏评论,你的支持是小棋最大的动力。
也欢迎同学们持续关注:bilibili (视频)、知乎、CSDN 同名
一起加油 :)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律