【Unity百宝箱】游戏中的用户数据存档 | Json序列化和反序列化 | 数据加密和解密 | 干货游戏教程

目录

Hi 大家好,我是游戏区Bug打工人小棋。

在游戏开发过程中,我们经常有存储用户数据的这一需求,比方说:游戏音量、关卡进度、任务进度等等。
p9SO736.png

在联网游戏中,往往会把一些用户核心资产信息存储在服务器端,等到用户登录时由服务器下发给用户进行初始化。而单机游戏则往往更加简单,只需要将这些数据序列化保存在本地即可(文本形式)。

今天小棋给大家分享一套简单易用的本地存储框架,希望对同学们有所帮助。

框架设计

我们首先定义一个管理类:LocalConfig.cs,专门用于管理本地化数据。

接着创建玩家数据类:UserData,他包含用户基础信息:姓名等级

public class UserData
{
    
    
    public string name;
    public int level;
}

这里主要做两件事:

  1. 将内存中的用户数据进行序列化,以文本格式保存在本地
  2. 将文本格式从硬盘中读取出来,反序列化为内存中的数据

分别对应代码中的:

  1. SaveUserData
  2. LoadUserData
public class LocalConfig
{
    
    
    public static void SaveUserData(UserData userData)
    {
    
    
        // 保存用户数据为文本
    }

    public static UserData LoadUserData(string userName)
    {
    
    
        // 读取用户数据到内存
    }
}

工具选用

在填写上述代码之前,我们需要先做一些调研和准备工作。

  1. 首先解决第一个问题:存取数据,存在哪?从哪取?

Unity中为我们提供了许多特殊文件路径,经过我与ChatGPT一分钟的愉快交流后,我了解到Unity中的PersistentDataPath是一个不错的选择。

这个路径是一个可读写的目录,符合我们的基本需求。根据官方文档的描述,不同平台下他对应的路径有所不同,但是我们可以直接通过Unity为我们提供的:Application.persistentDataPath来获取到最终路径,非常方便。
p9SvEhn.png

  1. 接下去是第二个问题:如何进行序列化和反序列化

这个名字对于初学者可能有些绕口,但其实非常简单,用通俗的话讲就是:
把C#数据转化为文本,以及将文本转化为C#代码。

业界对于序列化已经有非常成熟的方案,比如json、bson等等,你甚至可以自己写一套序列化和反序列化框架。

本节课程我们选用json,它支持数字、字符串、列表、字典等数据结构,对于大多数游戏来说已经完全够用了。
p9SzteO.png

  1. 最后一个问题:选用哪个Json框架

虽然官方推荐使用JsonUtility,但是这里我并不打算使用他。原因是他仅支持能显示在inspector窗口中的数据格式,也就是说字典、以及一些嵌套的数据结构都无法使用,这会给我们带来很多麻烦。

p9Szkzq.png

因此我最终选用的框架是: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这个特性,帮助我们在编辑器窗口生成快捷按钮。

p9poL5V.png

  • 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);
        }
    }

}
  1. 点击保存数据:SaveLocalConf

p9poL5V.png

根据官方文档的说明,我们知道在 windowsApplication.persistentDataPath对应:

Windows Store Apps: Application.persistentDataPath points to C:\Users\<user>\AppData\LocalLow\<company name>.

而这个<company name>在 projecting setting 中可以找到:
p9pT3Pf.png

因此我最终找到我保存的Json文件在这里:p9pTJxg.png

文本内容也与我们预期的一致:

{
    
    "name":"xiaoqi0","level":0,"test":{
    
    }}
  1. 点击读取数据:GetLocalConfig

p9poL5V.png

最终结果如下:

p9pTWZR.png

和预期效果一致~

至此存取框架的主体部分已经完成,下面我们对这套框架进行优化。

框架优化

由于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);
            // ...
        }
        // ...
    }
}

测试结果如下:

  1. 保存数据到本地

可以看到保存后的数据是乱码,玩家再也没办法开挂了!!!

p9pHDvF.png

  1. 读取数据到内存

可以看到最后读取的数据复原成功了

p9pTWZR.png

总结

本文通过框架设计、工具选用、逻辑书写、框架使用、框架优化、数据加密这六部分内容,层层剖析,向大家介绍了一种简单易用的本地化存取框架,希望能对大家有所帮助。

最后将成果代码贴出来,由于还没有经过项目实践,仅仅是理论分享,如果代码有疏漏欢迎交流指正。

  1. 框架部分
// 用于文件读写
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;
}

  1. 使用案例
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);
        }
    }

}
  1. 最终路径

参考官方文档

https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html

  1. tips

在编辑器模式下,我们不需要对数据进行加密解密,这会影响到我们的开发效率,可以使用UNITY_EDITOR这个宏进行判断,具体逻辑参考上文代码。

#if UNITY_EDITOR
            jsonData = Decrypt(jsonData);
#endif

最后

本文洋洋洒洒写了一万两千多字,绝对是干货中的干货,希望对大家有所帮助,请大家多多点赞收藏评论,你的支持是小棋最大的动力。

也欢迎同学们持续关注:

扫描下方二维码,关注小棋公众号,优质资源和文章这里都有:

一起加油 :)

猜你喜欢

转载自blog.csdn.net/dagongrenxiaoqi/article/details/130190333
今日推荐