首页 > 其他分享 >【Unity百宝箱】游戏中的用户数据存档

【Unity百宝箱】游戏中的用户数据存档

时间:2023-12-28 20:34:09浏览次数:23  
标签:userData UserData string 百宝箱 存档 jsonData Unity static public

【Unity百宝箱】游戏中的用户数据存档

原创 打工人小棋 打工人小棋 2023-04-17 00:04 发表于广东

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

图片

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

图片

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

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

框架设计

我们首先定义一个管理类: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来获取到最终路径,非常方便。

图片

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

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

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

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

图片

  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

根据官方文档的说明,我们知道在 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":{}}
  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

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 同名

一起加油 :)

 

   

标签:userData,UserData,string,百宝箱,存档,jsonData,Unity,static,public
From: https://www.cnblogs.com/Jimmy104/p/17933507.html

相关文章

  • 【泰裤辣 の Unity百宝箱】Canvas组件四件套讲解
    【泰裤辣のUnity百宝箱】Canvas组件四件套讲解原创 打工人小棋 打工人小棋 2023-05-1613:24 发表于广东1.介绍在上一期内容中,我分享了一套简单易用的UI框架。没想到大家的学习热情这么高,讨论度是目前所有内容最高的。由此可见,天下苦UI(秦)久已!!!接下去,我们继续......
  • Unity解析key不确定的Json
    遇到Json的key不固定时,只需要解析value,如下Jsondata下的key(1和2)是变化的:{"status":1,"msg":"success","data":["1:":{"atitle":"test",......
  • Unity_U_OP1 ScriptableObject 替代单例
    核心思想:解耦GameManager单例模式,不再由一个单例管理所有事件触发,拆分成无数个小单例,各自管理优点:更加灵活的事件管理模式复用性高,对于相关类型的事件,只需要写一遍代码,剩下的拖拖拖就可以实现相同的功能。缺点:管理起来相对麻烦不利于维护,除非对这个系统非常了解,要不然排......
  • Maya与Unity模型尺度统一
    Maya与Unity模型尺度统一Maya建模默认使用的单位是cm,Unity使用的是m,有时候可能需要把Maya中建好的模型导入到Unity中,因此这篇文章介绍如何修改Maya的默认建模单位,从而使得二者的尺度统一。进入窗口,设置,首选项。修改为m......
  • Unity引擎2D游戏开发,敌人追击状态的转换
    思路:从敌人的位置发射一道射线或者一片区域来对玩家实体进行检测,如果检测倒玩家,则进行追击进攻利用BoxCast()即可实现BoxCast()官方文档:https://docs.unity3d.com/cn/2022.3/ScriptReference/Physics2D.BoxCast.html创建检测区域由于BoxCast需要众多参数,所以在Enemy中创建......
  • Unity3D 如何提升游戏运行效率详解
    前言Unity3D是一款非常强大的游戏引擎,但是在处理复杂场景和大量资源时,游戏运行效率可能会遇到一些问题。本文将详细介绍如何提升Unity3D游戏的运行效率,包括技术详解和代码实现。对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀使用合适的资源压缩......
  • Unity3D Shader在GPU上是如何执行的详解
    Unity3D是一款广泛应用于游戏开发的跨平台开发引擎,它提供了丰富的功能和工具来帮助开发者创建高质量的游戏。其中一个重要的功能就是Shader,它可以用来控制对象的渲染效果。在Unity3D中,Shader是在GPU上执行的,那么它是如何工作的呢?本文将详细解释Unity3DShader在GPU上的执行过程,并......
  • Unity3D Shader Compute Shader基于GPU的并发计算详解
    在游戏开发中,计算密集型的任务通常需要耗费大量的CPU资源,这可能导致游戏性能下降,影响玩家的游戏体验。为了解决这个问题,Unity3D引入了ShaderComputeShader技术,它使用GPU进行并发计算,将一些计算密集型任务从CPU转移到GPU上执行,以提高游戏的性能和效率。本文将详细介绍Unity3DSha......
  • Unity3D 基类脚本怎么分别获取多个子类脚本的组件详解
    Unity3D是一款非常流行的游戏开发引擎,它提供了丰富的功能和工具,使得开发者可以轻松地创建高质量的游戏。在Unity3D中,脚本是游戏对象的一部分,它们通过附加到游戏对象上的组件来实现特定的功能。本文将详细介绍在Unity3D中如何分别获取多个子类脚本的组件,并提供相应的代码实现。对......
  • unity 标准资源包(过时)
    unity标准资源包Unity标准资源包(StandardAssets)是由Unity官方提供的一组可复用的资源集合,包含许多常用的游戏开发资源,如场景、材质、脚本、粒子效果、声音等。使用StandardAssets可以加速游戏开发的过程,因为它们已经预先制作好了,并且经过了官方的测试和优化,开发者可以......