【PlayerPrefs】Unity学习笔记——数据持久化之PlayerPrefs的简单理解与应用

知识基础:

  • C#基础知识&反射相关
  • 设计模式&单例模式相关

一、PlayerPrefs概述

1.1 PlayerPrefs

  • Q:什么是PlayerPrefs?
    • A:PlayerPrefs,它是一个Unity官方提供的,可以用于存储读取玩家数据,实现数据持久化的一个公共类
  • Q:什么是数据持久化?
    • A:数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的过程。
    • 用人话说:关游戏前将一部分数据从内存到硬盘,开游戏的时候从硬盘取数据到内存来进游戏,这样的过程就是数据持久化。

1.2 PlayerPrefs基本方法

1.2.1 存

Unity中关于PlayerPrefs的数据存储,类似于字典,采用键值对的方式。
具体表现为用一个string类型的,对应三种类型(int float string)的

PlayerPrefs.SetInt("myAge", 22);
PlayerPrefs.SetFloat("myHeight", 175f);
PlayerPrefs.SetString("myName", "小明");

但是注意此处直接调用Set相关方法,只会把数据暂时存到内存中。一般当游戏结束时,Unity会自动把数据存到硬盘。
但如果游戏不是正常结束,而是崩溃时,数据是不会存到硬盘中的,所以我们往往需要下面的API来直接存储:

PlayerPrefs.Save();

1.2.2 读

读取时我们提供存入的string类型的键即可获取。
这里有重载,可以让我们在没有对应的值时返回可自定义的参数。

int age = PlayerPrefs.GetInt("myAge");
float height = PlayerPrefs.GetFloat("myHeight");
string name1 = PlayerPrefs.GetString("myName");
//重载方法:当传入的键错误或没有对应值时返回后面的参数
string name2 = PlayerPrefs.GetString("Name", "张麻子");
Console.WriteLine(age)               //22
Console.WriteLine(height)           //175
Console.WriteLine(name1)           //小明
Console.WriteLine(name2)          //张麻子

1.2.3 查

查的时候和读取差不多,提供键,判断对应key的值是否存在。

//判断数据是否存在
if( PlayerPrefs.HasKey("myName") )
{
    print("存在myName对应的键值对数据");
}

1.2.4 删

删一样,提供键即可。也可以直接删全部。

//删除指定键值对
PlayerPrefs.DeleteKey("myAge");
 
//删除所有存储的信息
PlayerPrefs.DeleteAll();

1.3 PlayerPrefs存储位置

Windows

  • HKCU/Software/[公司名称]/[产品名称] 项下的注册表中
  • (其中公司和产品名称是可以在"Project Settings"中进行自定义的名称。)
  • (由于PlayerPrefs的特性,我们可以在注册表中找到对应存储的值,来直接更改内容,所以安全性需要额外考虑。)

安卓

  • /data/data/包名/shared_prefs/pkg-name.xml

IOS

  • /Library/Preferences/[应用ID].plist

1.4 PlayerPrefs的简单实践

这里我们使用最基础的PlayPrefs实现一个简单排行榜的功能,要求记录玩家名(可重复)、得分、通关时间。

//排行榜由一条一条的排行信息组成,所以先编写排行信息类
public class SingleRankInfo()
{
   public string playerName;
   public int playerScore;
   public int playerTime;
   
   //构造函数
   public RankInfo(string name, int score, int time)
   {
       playerName = name;
       playerScore = score;
       playerTime = time;
   }
}



//排行榜类,由一条条排行信息组成的list列表组成
public class RankList()
{
   //存放一条条SingleRankInfo的list列表
   public List<SingleRankInfo> rankList;
   
   //构造函数
   public RankList()
   {
       //每次初始化的时候读取一下
       Load();
   }
   
   //往排行榜里加数据的功能
   public void Add(string name, int score, int time)
   {
       rankList.Add(new SingleRankInfo(name, score, time));
   }
   
   //排行榜需要存数据的功能
   public void Save()
   {
       //存储有多少条数据
       PlayerPrefs.SetInt("rankListNum", rankList.Count);
       for (int i = 0; i < rankList.Count; i++)
       {
           RankInfo info = rankList[i];//排行榜第几条
           PlayerPrefs.SetString("rankInfo" + i, info.playerName);
           PlayerPrefs.SetInt("rankScore" + i, info.playerScore);
           PlayerPrefs.SetInt("rankTime" + i, info.playerTime);
       }
       PlayerPrefs.Save();
   }
   
   //排行榜需要读数据的功能
   private void Load()
   {
       int num = PlayerPrefs.GetInt("rankListNum", 0);
       rankList = new List<RankInfo>();
       for (int i = 0; i < num; i++)
       {
           RankInfo info = new RankInfo(PlayerPrefs.GetString("rankInfo" + i),
                                         PlayerPrefs.GetInt("rankScore" + i),
                                         PlayerPrefs.GetInt("rankTime" + i));
           rankList.Add(info);
       }
   }
}

二、完善PlayPrefs的存读档工具

从上面的知识中,我们可以利用Unity提供的PlayerPrefs简单构建出一个记录数据的对象。但是仔细思考不难发现,对于我们日常使用来说,这其中有一部分的需求仍是无法满足,主要体现在下面三处:

“Unity中关于PlayerPrefs的数据存储,类似于字典,采用键值对的方式。
具体表现为用一个string类型的,对应三种类型(int float string)的。”

(如果我有其他类型的数据要存储怎么办?)

“由于PlayerPrefs的特性,我们可以在注册表中找到对应存储的值,来直接更改内容,所以安全性需要额外考虑。”

(如果我对安全性有一点需求怎么办?)
同时,由于对于每一条信息都要手动set、save、load的方式过于繁琐,是否可以封装一下,统一进行一个对象的存储、读取呢?
我们将在下面部分逐步讨论以上问题。

2.1 前置知识:反射、单例模式相关

我们首先进行一点前置知识的回顾:

2.1.1 反射相关

要想利用PlayerPrefs存储其他类型,我们首先得让机器根据我们传进去的类,获取我们传进去的类信息和数据,这里不难联想到可以通过C#中反射相关的知识点解决问题。

  • 反射可以获取一个类的所有信息;
  • 反射可以帮助我们获取泛型类型;
    忘了的同学可以参考这篇笔记进行复习:简单理解CSharp中的反射(暂未施工完成))

2.1.2 单例模式相关

由于我们这里的想法是将PlayerPrefs存读档工具做的更完善一点,那么最好便是将其封装起来,我们下次使用时直接调用即可,那么我们同样不难联想到这里使用单例模式制作一个PlayerPrefsDataMgr的类,这样以后直接调用就行。
忘了的同学可以参考这篇笔记进行复习:Unity中单例模式基类的几种简单写法(暂未施工完成))

2.2 完善存储与读取

2.2.1 数据管理单例类PlayerPrefsDataMgr

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

//因为是管理工具类,所以采用单例模式,且不继承MonoBehaviour
public class PlayerPrefsDataMgr
{
    //单例模式惯例起手
    private static PlayerPrefsDataMgr instance;
    public static PlayerPrefsDataMgr Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new PlayerPrefsDataMgr();
            }
            return instance;
        }
    }
    private PlayerPrefsDataMgr() { }


    //开始编写内容逻辑
    //存档(传入要存储的 数据类 和 键名 -> 通过反射解析数据类,分门存取 -> 存档)
    public void SaveData(object playerData, string keyName)
    {
        //获取playerData的类名,将字段信息存到infos列表里,拆开以待后面分析
        Type playerDataType = playerData.GetType();
        FieldInfo[] infos = playerDataType.GetFields();

        //拼接存储用的键名,为了保证唯一性,命名规则采用: keyName存档名_playerData的类名_每一条info字段的类名_每一条info字段的名字
        //                                      例: Player1_PlayerInfo_Int32_age
        string saveKeyName = "";
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            info = infos[i];
            saveKeyName = keyName + '_' + playerDataType.Name + '_' + info.FieldType.Name + '_' + info.Name;
            //有了playerData的每一个属性对应的saveKeyName,配合每一个属性的info.getValue(playerData)获取的数据进行存储。

            //※重要!
            //Q:为什么这里不直接PlayerPrefs存数据,而要单用一个函数 存具体的值 呢?
            //A:因为我们只是通过反射获取了object playerData中每一个字段的信息,
            //  但这里的每一个info字段不保证是string int float三种类型!
            //  我们仍然需要逻辑去解析是何种类型,同时,如果object playerData中有列表、字典等可以嵌套叠罗汉的数据类型,
            //  我们可以通过再次调用下面分解的SaveValue方法,来进行反向拆罗汉。

            //SaveValue(24, "Player1_PlayerInfo_Int32_age");
            //SaveValue(List<int>, "Player1_PlayerInfo_Int32_list^1");
            SaveValue(info.GetValue(playerData), saveKeyName);
        }
        //手动存档
        PlayerPrefs.Save();
        //Debug.Log("邦邦咔邦!有一个类存档成功!");
    }

    private void SaveValue(object value, string saveKeyName)
    {
        //开始拆解
        Type fieldType = value.GetType();
        if (fieldType == typeof(int))
        {
            PlayerPrefs.SetInt(saveKeyName, (int)value);
        }
        else if (fieldType == typeof(float))
        {
            PlayerPrefs.SetFloat(saveKeyName, (float)value);
        }
        else if (fieldType == typeof(string))
        {
            PlayerPrefs.SetString(saveKeyName, value.ToString());
        }
        else if (fieldType == typeof(bool))
        {
            //bool类型我们用int去存
            PlayerPrefs.SetInt(saveKeyName, (bool)value ? 1 : 0);
        }
        //如果是List<T>这种泛型类,无法直接将fieldType和typeof(list)去进行比较。
        //这里我们针对传入的value的type:fieldType,先判断fieldType是否是泛型类,
        //  接着调用.GetGenericTypeDefinition()的方法查看是否是List的泛型类
        else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>))
        {
            //如果是List<T>类型,将该泛型列表类用IList装载,
            //开始拆罗汉,获取每一个List<T>内,存储的键名和数据内容进行存储。
            IList valueList = (IList)value;
            int index = 0;
            foreach (var item in valueList)
            {
                //例:SaveValue( List<string>中的第一个string, "Player1_PlayerInfo_list^1" + '_' + 0 + '_' + string);
                //Debug.Log("即将重复存泛型类数据,基础地址为:" + saveKeyName);
                SaveValue(item, saveKeyName + '_' + index + '_' + item.GetType().Name);
                index++;
            }
            //存长度以便后续读取生成
            SaveValue(index, saveKeyName + "_count");
        }
        //字典也是一样存,但是字典有两个元素,需要分别存。
        else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
        {
            //拆罗汉,获取每一个Dictionary<,>内,存储的键名和数据内容进行存储。
            IDictionary tempDic = (IDictionary)value;
            int index = 0;
            foreach (object key in tempDic.Keys)
            {
                //例:SaveValue(List<string>中的第一个string, "Player1_PlayerInfo_dic`2_dic" + '_' + 0 + "_key_" + 0);
                //Debug.Log("即将重复存泛型类数据,基础地址为:" + saveKeyName);
                SaveValue(key, saveKeyName + '_' + index + "_key_" + key.GetType().Name);
                SaveValue(tempDic[key], saveKeyName + '_' + index + "_value_" + tempDic[key].GetType().Name);
                index++;
            }
            //存泛型列表里有几个元素,方便回头读取的时候直接创建
            SaveValue(index, saveKeyName + "_count");
        }
        else
        {
            //我们认为是自定义类,将这个类再次使用存档的根函数去解析
            //(注意是SaveData不是SaveValue!!!)
            //Debug.Log("这是一个自定义类,我们给他设置了起始存储name:" + saveKeyName);
            SaveData(value, saveKeyName);
        }
    }


    //读档(传入要读取的 数据类的类型 和 键名 ->
    //      用存档时的命名格式读取 ->
    //      通过反射解析后,直接内部实例化一个包含存档数据的类返回)
    public object LoadData(Type dataType, string keyName)
    {
        //快速实例化传进来的需要读取的存档对象(需要存档类具有无参构造)
        object data = Activator.CreateInstance(dataType);
        FieldInfo[] fieldInfos = dataType.GetFields();
        FieldInfo info;
        string loadKeyName = "";
        for (int i = 0; i < fieldInfos.Length; i++)
        {
            info = fieldInfos[i];
            //和上面一样,怎么存的怎么读
            loadKeyName = keyName + '_' + dataType.Name + '_' + info.FieldType.Name + '_' + info.Name;
            //直接设值,值用函数读取
            info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
        }
        //Debug.Log("邦邦咔邦,有一个类读取完成!");
        return data;
    }

    private object LoadValue(Type infoType, string loadKeyName)
    {
        if(infoType == typeof(Int32))
        {
            return PlayerPrefs.GetInt(loadKeyName, 0);
        }
        else if (infoType == typeof(float))
        {
            return PlayerPrefs.GetFloat(loadKeyName, 0);
        }
        else if(infoType == typeof(string))
        {
            return PlayerPrefs.GetString(loadKeyName, "");
        }
        else if(infoType == typeof(bool))
        {
            return PlayerPrefs.GetInt(loadKeyName, 0) == 1 ? true : false;
        }
        else if (infoType.IsGenericType && infoType.GetGenericTypeDefinition() == typeof(List<>))
        {
            //列表泛型类,先获取长度,再生成列表继承的基类,动态取数据放进去。
            int count = PlayerPrefs.GetInt(loadKeyName + "_count", 0);
            IList list = Activator.CreateInstance(infoType) as IList;
            for(int i = 0; i < count; i++)
            {
                //Debug.Log("正在读取list泛型类,具体地址为: " + loadKeyName + '_' + i + '_' + infoType.GetGenericArguments()[0].Name);
                list.Add(LoadValue(infoType.GetGenericArguments()[0], loadKeyName + '_' + i + '_' + infoType.GetGenericArguments()[0].Name));
            }
            return list;
        }
        else if (infoType.IsGenericType && infoType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
        {
            int count = PlayerPrefs.GetInt(loadKeyName + "_count", 0);
            IDictionary tempDic = Activator.CreateInstance(infoType) as IDictionary;
            for(int i = 0;i < count; i++)
            {
                //Debug.Log("正在读取dic泛型类,具体地址为: " + loadKeyName);
                tempDic.Add(LoadValue(infoType.GetGenericArguments()[0], loadKeyName + '_' + i + "_key_" + infoType.GetGenericArguments()[0].Name),
                            LoadValue(infoType.GetGenericArguments()[1], loadKeyName + '_' + i + "_value_" + infoType.GetGenericArguments()[1].Name));
            }
            return tempDic;
        }
        else
        {
            //Debug.Log("这是一个自定义类,我们读取了起始存储name:" + loadKeyName);
            return LoadData(infoType, loadKeyName);
        }
    }
}

2.2.2 测试用例

这里我们假设要存储的是PlayerInfo类,其中包含一些存档常见属性,以及一个自定义类型ItemInfo和由该自定义类型组成的列表itemInfoList,以及由其组成的字典itemInfoDic

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

class PlayerInfo
{
    public int age;
    public string name;
    public float height;
    public bool sex;

    public List<int> simpleList;

    public Dictionary<int, string> dic;

    public ItemInfo itemInfooo;

    public List<ItemInfo> itemInfoList;

    public Dictionary<int, ItemInfo> itemInfoDic;

    public PlayerInfo() { }
}

public class ItemInfo
{
    public int id;
    public int num;

    public ItemInfo()
    {

    }

    public ItemInfo(int id, int num)
    {
        this.id = id;
        this.num = num;
    }
}


public class Test : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("=======================================");
        //先清除下所有的键值
        PlayerPrefs.DeleteAll();
        //正常使用先读取
        PlayerInfo p = PlayerPrefsDataMgr.Instance.LoadData(typeof(PlayerInfo), "player1") as PlayerInfo;

        p.age = 18;
        p.name = "张牧之";
        p.height = 70;
        p.sex = true;
        p.itemInfoList.Add(new ItemInfo(1, 99));
        p.itemInfoList.Add(new ItemInfo(2, 199));
        p.itemInfoDic.Add(3, new ItemInfo(3, 1));
        p.itemInfoDic.Add(4, new ItemInfo(4, 2));

        //再存档
        PlayerPrefsDataMgr.Instance.SaveData(p, "player1");
    }

}

2.3 完善安全性

首先明白一点:一般对于简单的单机游戏来说,加密只是提高别人修改你的游戏数据的门槛,只要源代码泄漏,知道加密规则后,一切加密都没意义。

对于普通数据的安全性,我们一般考虑下面三点来进行保护:

  • 找不到
    • 将游戏数据存放到不容易被找到的地方,比如无规律的文件夹内、多层文件夹包裹、采用名称辨识度低的文件夹命名方式等。
    • 但是对于PlayerPrefs来说并不太适用,因为Unity已经将我们的位置固定了,暂时改不了。
  • 看不懂
    • 让我们数据的存放值让别人看不懂,即通过对存、读数据的操作,让其他人根据游戏具体数据反向查找的时候找不到。
    • 例如存取玩家A的Hp为10,我们就可以在存值时对10进行加工,比如+6,存的值为16,读取时再-6读取还是10。这样其他人在本地查找为10的值时找不到我们的Hp存放数值。
  • 解不出
    • 利用一定算法等规则,来防止其他人通过找规律等形式识破加密规则,从而强行修改游戏数据。

三、总结

在文章的最后,总结一下本篇文章,我们在第一节学习了基础的PlayerPrefs知识,在第二节针对PlayerPrefs存储类型、安全性等方面的问题进行了小小的简单讨论,并写出一个较为通用的简单存读档工具。
在后续的开发中,我们只需要导入文件,便可以使用下面一行代码,实现简单自定义数据类型的存读取了。

//存放:
PlayerPrefsDataMgr.Instance.SaveData(p, “Player1”);
//读取:
PlayerInfo p = PlayerPrefsDataMgr.Instance.LoadData(typeof(PlayerInfo), “Player1”) as PlayerInfo;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值