Unity简单本地存档系统

前言

        本文主要记述一些我在研究Unity简单存档系统时的总结和见闻,纯属个人笔记。

前言涉及到一些本人自己的关于存档系统这一宽泛概念的总结,篇幅较长,如果不感兴趣请直接跳过到后面的脚本环节。

存档系统是绝大多数游戏不可或缺的内容(除了那些刻意为之的游戏)。看了这么多网上有关存档系统的分享,受益匪浅。大致总结了一下我所认为的一个标准的Unity存档系统应该具备的“三要素”:

1.原始数据

2.序列化方法

3.加密方法

        上面三个其实每一个都可以拉出来单独长篇大论,但是现在就先以一整个系统为核心,简单分析一下它们的作用:

1.原始数据:这不废话吗?!存档存的当然是数据!但是关键点其实是如何构建一个好的原始数据,如何让数据易于管理,易于修改,易于存储,易于拓展才是关键因素,好的原始数据可以决定构建整个存档系统的蓝图。因为整个存档系统本身就是为了数据而诞生的。

2.序列化方法:我们在上面提到了存数据是我们存档系统的目的。那么序列化则是达成这种目的途径。在我看来:序列化就是以某种协议或规则,让不同类型的数据变成同一类型以方便数据传输那么应该是哪种类型呢?当然是字节——计算机数据处理的基本单位。

        说点具体的,比如一套数据有诸如”姓名“(string),”生命值“(int),”暴击率“(float)等等许多不同的类型。为了实现它们的写入和读取,我们将它们转换为字节数组的形式或简介表示为字节数组的形式实现统一的管理。但是我怎么知道我取出来和写进去的是不是同一个数据呢?所以序列化方法就此而生。JDK、JSON、XML、Hessian、Kryo、Thrift、Protostuff、FST,二进制等等序列化方法就是人们创造来规范序列化的。下文的简单存储系统是以Json作为序列化方法。

3.加密方法:其实这个要素是可有可无,但是与我个人而言,如果游戏玩家能随意修改数值,那么游戏将会失去很多乐趣,当然除非作者刻意为之。所以我把它将其视作存档系统的重点之一(因为好像也没什么重点了)。对于加密方法其实也是可以细分好多种。但对于今天这篇文章这篇内容,本地加密方法才是重点。如果你打开Json数据文本你就会发现Json的可读性非常高,几乎把你的数据暴露完全。所以最简单的数据加密就是把你的数据以某种方法转化成”一坨大便“。让它的可读性几乎为零,当然你先要记住这种加密方法并能够在需要读取数据的时候解密。下面的存档系统就是用这种简单粗暴的方式。

        好了说了那么多还没到正餐,赶紧看看。

简单存档系统

原始数据

        这个简单存档系统包括了三样东西(除去杂七杂八的设置)

首先是一个叫SaveData的脚本:

using System;

[Serializable]
public class SaveData
{
    public int coin = 0;
}

它对应了存档系统所说的原始数据。当然我并没有让它显得很高大上,毕竟这里仅作演示,顺带一提这里可以不需要[Serializable]标签,因为我们不涉及二进制或XML的序列化。

        这个int coin就是我们要存储的数据。

下面请构建一个类似的场景帮助我们实现存档功能的基本原理。

number在运行时显示coin。

序列化方法

到这里我们需要开始编写序列化和加密操作。如下所示:

using System;
using System.IO;
using UnityEngine;
using UnityEngine.UI;

public class SaveSystem : MonoBehaviour
{    
    SaveData data = new();
    //获取“我的文档”对应的路径目录
    private string SavePath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    //存放存档文件的那个文件夹的名字
    private const string SaveFolder = "MyGame";
    //存档文件名称(这里要全称)
    private const string SaveFileName = "test.txt";

    //声明一个目录信息用来保存路径
    private DirectoryInfo directoryInfo;
    
    private void Awake()
    {
        DontDestroyOnLoad(gameObject);    
        //这里的Path.Combine()用来链接路径,实际上用 SavePath + "/" + SaveFloder 表示也行
        directoryInfo = new DirectoryInfo(Path.Combine(SavePath, SaveFolder));
    }
    //用来增加金币
    public void AddCoin()
    {
        data.coin++;
    }
    //保存
    public void Save()
    {        
        //如果该路径不存在就先将其创建出来
        if (!directoryInfo.Exists) 
        {
            directoryInfo.Create();
        }
        
        string EncryptedData = SaveDataEncryption.EncryInNegation(JsonUtility.ToJson(data));

        File.WriteAllText(Path.Combine(directoryInfo.FullName,SaveFileName),EncryptedData);

    }
    //加载
    public void Load()
    {
        //与上述Save为逆过程        
        string DecryptedData 
        = SaveDataEncryption.DecryInNegation(File.ReadAllText(Path.Combine(directoryInfo.FullName, SaveFileName)));
        data = JsonUtility.FromJson<SaveData>(DecryptedData);

    }

    private void Update()
    {
        GameObject.Find("number").GetComponent<Text>().text = data.coin.ToString();
    }


}

上面的方法与按钮对应。这里我希望读者能自行理解,因为我懒得打这么多字(不是),其实是因为这些都是非常简单的基本操作,重点就在于理解存档方法罢了。

        但我不得不提一些比较隐晦的细节。也是我在敲代码时遇到的问题。

1.类的开头我写了很多字符串,这是为了方便管理可能变换的存放路径。然而说到这个,大家大可不必与我相同,因为比较多的单机游戏的存档文件都存放到了文档(Documents)中,比如骑砍。如果你不知道文档是哪个:

那现在应该知道了吧,这里只是因为我是中文用户才把Documents自动翻译为文档。事实上路径中还是Documents。点进去你可以发现很多游戏名文件夹,那里就是存放存档文件的地方。

        当然不是所有存档文件都存放在这个位置。你大可以天马行空,把存档文件塞到玩家的系统文件夹(不是)!

2.第二点我想说的是,因为存档这个事情本身就是不太属于游戏引擎的东西,所以Unity本身并没有太多相关API,因此我们需要明白脚本顶上那四个命名空间是用来干什么的。这里我就不多说了,全部方法和属性都可以在微软官方文档中查找到。比较需要记住的是:

刚刚我说的

private string SavePath =

Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

这里用到了System中的Environment类。

和存档方法中的File类则是System.IO的一个类。值得一提的是,这里面的API挺多好玩的,也有很多不同方式实现存档系统,这里我选择了很直接的File类。除此之外还有Stream,StreamWriter,StreamReader等类也提供操作文本文件的方法。但是我使用它们的时候遇到一些问题没有解决,所以改用了File。反正这种C#自带的命名空间还是要有所了解才好啊。

3.在文中使用的序列化方法是Unity自带的JsonUtility,因为操作简单方便适合演示我就拿来用了,还有很多第三方库也可以实现。它们之间的具体区别自己去找资料吧。

4.SaveDataEncryption这个类是自己写的类,用来封装一些加密方法,接下来我们就来具体看一下:

加密方法

using System.Text;

public class SaveDataEncryption
{    
    #region 取反
    public static string EncryInNegation(string json)
    {
        byte[] bytes = Encoding.Unicode.GetBytes(json);
        for (int i = 0; i < bytes.Length; i++)
        {
            bytes[i] = (byte)(~bytes[i]);
        }
        return Encoding.Unicode.GetString(bytes);

    }
    //巧妙的是,取反加密解密正好就是完全相同的过程
    public static string DecryInNegation(string json)
    {
        return EncryInNegation(json);
    }
    #endregion
}

这就是一个简单的加密类,用来存放加密方法,当然你大可以将它写在SaveSystem中。不过因为这个加密方法只是简单示范,所以以后如果有其他加密方法需要记录,那么可以归到统一的一个类,这其实也是代码优化的范畴吧。

就像上面写的那样,我尝试用编码区域来界定不同加密方法之间的范围。这个叫做“取反”的加密方法将Json文本数据转化为字节数组后,对每一个字节取反,最后返回一个取反后的文本。

这是加密前的Json文本:

这是加密后的Json文本:

是不是真的变成“一坨大便了”!我们想要的效果实现了,玩家也不能随意修改数据了。

        为了避免大家踩坑,我有必要说清楚Unity自带的这个JsonUtility的编码规则仅支持Unicode(无符号 UTF-8),所以不要尝试在这里切换Encoding的其他编码规则,不然会报错。

最后我们来具体演示一下整个存档系统的实现过程。

存档系统演示

        因为GIF不能上传太大的,我又懒得录视频,就图片叙事吧。

点击“Give me Money”将金钱加到10。

然后点击“Save”保存数据。

可以在对应的路径上观察到文件已经保存下来了,并且文本已经是“一坨大便”了。

回到Unity重新启动,直接点击可以看到“number”又变回10了。

至此Unity简单本地存档系统为您展示完毕。

结语

        因为一边备考一边钻研这个存档系统,所以考试没考好,研究也没研究出什么。但是收获还是很多,由于是个人的总结笔记,文中出现的任何觉得有问题的点请随时找我。

下面就是我自己对存档系统的总结:

        涉及到的现在这个系统只是一个雏形,而且仅适用于本地,对于数据库端的存储应该截然不同,好在我目前只需要用到这种程度。

        对于进一步对该系统的构建有大致几个可能和设想:

1.更换JsonUtility为其他上限更高的序列化具体方法,或者完善JsonUtility的不足,比如不能存储字典之类的嵌套数据类型。除此之外,存档文件不可能只有一个,文件名称也应不同,捕获异常等也需要添加。

2.对原始数据的处理应该更加普适,现在是以单个脚本的单个唯一实例作为运行时的数据(原始数据),在这种方式下只需要将所有数据堆到,转移到这个实例中去就可以了。但是对于比较复杂的,体量比较大的游戏,还需要存储许多结构复杂的数据,此时这种方式就不适用了,可以尝试以接口的方式实现数据的统一管理(受麦扣老师的启发)。一般而言适用于GJ这样的游戏开发中。

3.对于加密方法应当给予更多选择并且更加完全,像上面那样取反操作稍微破译我想也会暴露出原始数据吧。请自行发挥想象力。当然GJ不会用到加密什么的就是了。

4.这是我第一个从整个系统的角度进行总结记录,而不是以一个两个单一脚本,这样做的优势确实是让我非常有必要了解许多内容,缺点是进度很慢很繁琐。可能以重要程度作为要不要进行系统总结的界限吧。

5.作为初学者,非常需要帮助。

6.应当强制自己每周坚持写点东西,总结收获的同时也回顾了。

猜你喜欢

转载自blog.csdn.net/m0_73087695/article/details/135560241