Unity数据持久化之PlayerPrefs,详细

本博客主要记录PlayerPrefs的使用,比较长

1、什么是数据持久化

数据持久化就是将内存中的数据模型转化为存储模型,以及将存储模型转化为内存中数据模型的通称
简述:将游戏中的数据存储到硬盘,然后可以下次进游戏的时候读取硬盘中的数据到内存。

2、什么是PlayerPrefs

是Unity提供的可以用于存储玩家数据的公共类

3、存储的相关API

  • PlayerPrefs的数据存储 类似于键值对存储 一个键对应一个值
  • 提供了存储3种数据的方法 int float string
  • 键: string类型
  • 值:int float string 对应3种API
示例:
	    PlayerPrefs.SetInt("myAge", 18);
        PlayerPrefs.SetFloat("myHeight", 177.5f);
        PlayerPrefs.SetString("myName", "Adeng");
  • 只要调用该方法 就会马上存储到硬盘中
	PlayerPrefs.Save();

4、注意点

  • 直接调用Set相关方法 只会把数据存到内存里
  • 当游戏结束时 Unity会自动把数据存到硬盘中
  • 如果游戏不是正常结束的 而是崩溃 数据是不会存到硬盘中的

  • PlayerPrefs是有局限性的 它只能存3种类型的数据
    • 如果你想要存储别的类型的数据 只能降低精度 或者上升精度来进行存储
      • 例如存储bool需要这样
          bool sex = true;
          PlayerPrefs.SetInt("sex", sex ? 1 : 0);
      
  • 如果不同类型用同一键名进行存储 会进行覆盖

5、读取的相关API

  • 运行时 只要你Set了对应键值对,即使你没有马上存储Save在本地,也能够读取出信息
  • PlayerPrefs.GetInt(string , value) 第二个参数就是默认值,若不存在,那么就直接取第二个参数作为返回值
  • GetInt、GetFloat、GetString同理
  • PlayerPrefs.HasKey(string) 判断string为key的值是否存在
  • PlayerPrefs.DeleteKey(“myAge”); 删除某个键值对
  • PlayerPrefs.DeleteAll();删除所有的键值对
 	 //int
      int age = PlayerPrefs.GetInt("myAge");
      print(age);
      //前提是 如果找不到myAge对应的值 就会返回函数的第二个参数 默认值
      age = PlayerPrefs.GetInt("myAge", 100);
      print(age);

      //float
      float height = PlayerPrefs.GetFloat("myHeight", 1000f);
      print(height);

      //string
      string name = PlayerPrefs.GetString("myName");
      print(name);
     //判断数据是否存在
    if( PlayerPrefs.HasKey("myName") )
      {
          print("存在myName对应的键值对数据");
      }
     //删除指定键值对
      PlayerPrefs.DeleteKey("myAge");
      //删除所有存储的信息
      PlayerPrefs.DeleteAll();

练习题

练习1 做一个普通的可存储的

现在有玩家信息类,有名字,年龄,攻击力,防御力等成员。另有装备信息类,装备类中有id,数量两个成员。
现在为其封装两个方法,一个用来存储数据,一个用来读取数据
注意,装备是个list,所以存储时候需要遍历

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

public class Item
{
    public int id;
    public int num;
}

public class Player
{
    public string name;
    public int age;
    public int atk;
    public int def;
    //拥有的装备信息
    public List itemList;

    //这个变量 是一个 存储和读取的一个唯一key标识
    private string keyName;

    /// 
    /// 存储数据
    /// 
    public void Save()
    {
        PlayerPrefs.SetString(keyName +"_name", name);
        PlayerPrefs.SetInt(keyName + "_age", age);
        PlayerPrefs.SetInt(keyName + "_atk", atk);
        PlayerPrefs.SetInt(keyName + "_def", def);
        //存储有多少个装备
        PlayerPrefs.SetInt(keyName + "_ItemNum", itemList.Count);
        for (int i = 0; i < itemList.Count; i++)
        {
            //存储每一个装备的信息
            PlayerPrefs.SetInt(keyName + "_itemID" + i, itemList[i].id);
            PlayerPrefs.SetInt(keyName + "_itemNum" + i, itemList[i].num);
        }

        PlayerPrefs.Save();
    }
    /// 
    /// 读取数据
    /// 
    public void Load(string keyName)
    {
        //记录你传入的标识
        this.keyName = keyName;

        name = PlayerPrefs.GetString(keyName + "_name", "未命名");
        age = PlayerPrefs.GetInt(keyName + "_age", 18);
        atk = PlayerPrefs.GetInt(keyName + "_atk", 10);
        def = PlayerPrefs.GetInt(keyName + "_def", 5);

        //得到有多少个装备
        int num = PlayerPrefs.GetInt(keyName + "_ItemNum", 0);
        //初始化容器
        itemList = new List();
        Item item;
        for (int i = 0; i < num; i++)
        {
            item = new Item();
            item.id = PlayerPrefs.GetInt(keyName + "_itemID" + i);
            item.num = PlayerPrefs.GetInt(keyName + "_itemNum" + i);
            itemList.Add(item);
        }
    }
}

public class Lesson1_Exercises : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

        //现在有玩家信息类,有名字,年龄,攻击力,防御力等成员
        //现在为其封装两个方法,一个用来存储数据,一个用来读取数据

        //Player p = new Player();
        //p.Load();
        //print(p.name);
        //print(p.age);
        //print(p.atk);
        //print(p.def);

        //p.name = "唐老狮";
        //p.age = 22;
        //p.atk = 40;
        //p.def = 10;
        改了过后存储
        //p.Save();

 

        Player p = new Player();
        p.Load("Player1");
        p.Save();

        Player p2 = new Player();
        p2.Load("Player2");
        p.Save();
        //装备信息
        //print(p.itemList.Count);
        //for (int i = 0; i < p.itemList.Count; i++)
        //{
        //    print("道具ID:" + p.itemList[i].id);
        //    print("道具数量:" + p.itemList[i].num);
        //}

        为玩家添加一个装备
        //Item item = new Item();
        //item.id = 1;
        //item.num = 1;
        //p.itemList.Add(item);
        //item = new Item();
        //item.id = 2;
        //item.num = 2;
        //p.itemList.Add(item);

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

练习2 做一个排行榜

要在游戏中做一个排行榜功能,排行榜主要记录玩家名(可重复),玩家得分,玩家通关时间,请用PlayerPrefs存储读取排行榜相关信息
注意:这个名字可以重复就代表了不能单独的将名字当做key

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

/// 
/// 排行榜具体信息
/// 
public class RankListInfo
{
    public List rankList;

    public RankListInfo()
    {
        Load();
    }

    /// 
    /// 新加排行榜信息
    /// 
    public void Add(string name , int score, int time)
    {
        rankList.Add(new RankInfo(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);
        }
    }

    private void Load()
    {
        int num = PlayerPrefs.GetInt("rankListNum", 0);
        rankList = new List();
        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);
        }
    }
}

/// 
/// 排行榜单条信息
/// 
public class RankInfo
{
    public string playerName;
    public int playerScore;
    public int playerTime;

    public RankInfo(string name, int score, int time)
    {
        playerName = name;
        playerScore = score;
        playerTime = time;
    }
}

public class Lesson2_Exercises : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 练习题一 
        //将知识点一中的练习题,改为可以支持存储多个玩家信息
        #endregion

        #region 练习题二
        //要在游戏中做一个排行榜功能
        //排行榜主要记录玩家名(可重复),玩家得分,玩家通关时间
        //请用PlayerPrefs存储读取排行榜相关信息

        RankListInfo rankList = new RankListInfo();
        print(rankList.rankList.Count);
        for (int i = 0; i < rankList.rankList.Count; i++)
        {
            print("姓名" + rankList.rankList[i].playerName);
            print("分数" + rankList.rankList[i].playerScore);
            print("时间" + rankList.rankList[i].playerTime);
        }

        rankList.Add("唐老狮", 100, 99);
        rankList.Save();
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

6、PlayerPrefs存储在哪

以下是不同平台的位置,
要注意这个存储文件是可以找到然后手动去改的
所以要注意加密再去存储

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

public class Lesson2_SaveWhere : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 PlayerPrefs存储的数据存在哪里?
        //不同平台存储位置不一样

        #region Windows
        //PlayerPrefs 存储在 
        //HKCU\Software\[公司名称]\[产品名称] 项下的注册表中
        //其中公司和产品名称是 在“Project Settings”中设置的名称。

        //运行 regedit
        //HKEY_CURRENT_USER
        //SOFTWARE
        //Unity
        //UnityEditor
        //公司名称
        //产品名称
        #endregion

        #region Android
        // /data/data/包名/shared_prefs/pkg-name.xml 
        #endregion

        #region IOS
        // /Library/Preferences/[应用ID].plist
        #endregion

        #endregion

        #region 知识点二 PlayerPrefs数据唯一性
        //PlayerPrefs中不同数据的唯一性
        //是由key决定的,不同的key决定了不同的数据
        //同一项目中 如果不同数据key相同 会造成数据丢失
        //要保证数据不丢失就要建立一个保证key唯一的规则
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

7、缺点和优点

  • 缺点
    • 重复工作过多
    • 自定义的数据类型都需要自己去实现存储相关的内容,就像上面第一个例子
    • 代码的相似度过高,冗余
    • 数据容易被找到并修改
  • 优点
    • 适合存储一些对安全性要求不高的简单数据,例如音量的大小的保存等等
    • 另外可以对其进行简单的封装就可以让他变得方便快捷(见下面)

8、进行封装方便使用

首先封装要用到反射的知识点,
这边打算封装成一个传入类对象,内部函数就会自动检索内部的可存储变量,自动存储,
并且取数据的时候,传入类名就可以,
因为,你并不知道以后这个函数被传入哪个对象,每个对象的方法和属性都是不同的,
所以只有反射才能做出这样的功能。

8.1先了解下反射

8.11关于反射的定义

在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
类,函数,变量,对象等等,实例化它们,执行它们,操作它们

具体可看下面链接
关于反射的文章

8.12关于反射的知识点

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Father
{
}
public class Son
{
}
public class Reflection : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //反射3剑客—— 1T 两 A
        //Type —— 用于获取 类的所有信息 字段 属性 方法 等等
        //Assembly —— 用于获取程序集 通过程序集获取Type
        //Activator —— 用于快速实例化对象

        #region 知识点二 判断一个类型的对象是否可以让另一个类型为自己分配空间
        //父类装子类
        //是否可以从某一个类型的对象 为自己 分配 空间

        Type fatherType = typeof(Father);
        Type sonType = typeof(Son);

        //调用者 通过该方法进行判断 判断是否可以通过传入的类型为自己 分配空间
        if( fatherType.IsAssignableFrom(sonType) )
        {
            print("可以装");
            Father f = Activator.CreateInstance(sonType) as Father;
            print(f);
        }
        else
        {
            print("不能装");
        }
        #endregion

        #region 知识点三 通过反射获取泛型类型
        List list = new List();
        Type listType = list.GetType();
        //GetGenericArguments表示泛型类型的类型实参的 Type 对象的数组。 如果当前类型不是泛型类型,则返回一个空数组。
        Type[] types = listType.GetGenericArguments();
        //输出的是System.Int32,这个types是泛型的类型的数组
        print(types[0])
        Dictionary dic = new Dictionary();
        Type dicType = dic.GetType();
        types = dicType.GetGenericArguments();
        print(types[0]);//输出System.String
        print(types[1]);//输出System.Single
        #endregion
    }
    // Update is called once per frame
    void Update()
    {
    }
}

8.2进行封装

进行封装的目的是可以更加便捷的使用PlayerPrefs,只需要传入想要封装的类型。即使这个类型是用户自定义的类。
看下面代码:

8.21 数据封装类

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

/// 
/// PlayerPrefs数据管理类 统一管理数据的存储和读取
/// 
public class PlayerPrefsDataMgr
{
    private static PlayerPrefsDataMgr instance = new PlayerPrefsDataMgr();

    public static PlayerPrefsDataMgr Instance
    {
        get
        {
            return instance;
        }
    }

    private PlayerPrefsDataMgr()
    {

    }

    /// 
    /// 存储数据
    /// 
    /// 数据对象
    /// 数据对象的唯一key 自己控制
    public void SaveData( object data, string keyName )
    {
        //就是要通过 Type 得到传入数据对象的所有的 字段
        //然后结合 PlayerPrefs来进行存储

        #region 第一步 获取传入数据对象的所有字段
        Type dataType = data.GetType();
        //得到所有的字段
        FieldInfo[] infos = dataType.GetFields();
        #endregion

        #region 第二步 自己定义一个key的规则 进行数据存储
        //我们存储都是通过PlayerPrefs来进行存储的
        //保证key的唯一性 我们就需要自己定一个key的规则

        //我们自己定一个规则
        // keyName_数据类型_字段类型_字段名
        #endregion

        #region 第三步 遍历这些字段 进行数据存储
        string saveKeyName = "";
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            //对每一个字段 进行数据存储
            //得到具体的字段信息
            info = infos[i];
            //通过FieldInfo可以直接获取到 字段的类型 和字段的名字
            //字段的类型 info.FieldType.Name
            //字段的名字 info.Name;

            //要根据我们定的key的拼接规则 来进行key的生成
            //Player1_PlayerInfo_Int32_age
            saveKeyName = keyName + "_" + dataType.Name + 
                "_" + info.FieldType.Name + "_" + info.Name;

            //现在得到了Key 按照我们的规则
            //接下来就要来通过PlayerPrefs来进行存储
            //如何获取值
            //info.GetValue(data)
            //封装了一个方法 专门来存储值 
            SaveValue(info.GetValue(data), saveKeyName);
        }

        PlayerPrefs.Save();
        #endregion
    }

    private void SaveValue(object value, string keyName)
    {
        //直接通过PlayerPrefs来进行存储了
        //就是根据数据类型的不同 来决定使用哪一个API来进行存储
        //PlayerPrefs只支持3种类型存储 
        //判断 数据类型 是什么类型 然后调用具体的方法来存储
        Type fieldType = value.GetType();

        //类型判断
        //是不是int
        if( fieldType == typeof(int) )
        {
            Debug.Log("存储int" + keyName);
            PlayerPrefs.SetInt(keyName, (int)value);
        }
        else if (fieldType == typeof(float))
        {
            Debug.Log("存储float" + keyName);
            PlayerPrefs.SetFloat(keyName, (float)value);
        }
        else if (fieldType == typeof(string))
        {
            Debug.Log("存储string" + keyName);
            PlayerPrefs.SetString(keyName, value.ToString());
        }
        else if (fieldType == typeof(bool))
        {
            Debug.Log("存储bool" + keyName);
            //自己顶一个存储bool的规则
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }
        //如何判断 泛型类的类型呢
        //通过反射 判断 父子关系
        //这相当于是判断 字段是不是IList的子类
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            Debug.Log("存储List" + keyName);
            //父类装子类
            IList list = value as IList;
            //先存储 数量 
            PlayerPrefs.SetInt(keyName, list.Count);
            int index = 0;
            foreach (object obj in list)
            {
                //存储具体的值
                SaveValue(obj, keyName + index);
                ++index;
            }
        }
        //判断是不是Dictionary类型 通过Dictionary的父类来判断
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            Debug.Log("存储Dictionary" + keyName);
            //父类装自来
            IDictionary dic = value as IDictionary;
            //先存字典长度
            PlayerPrefs.SetInt(keyName, dic.Count);
            //遍历存储Dic里面的具体值
            //用于区分 表示的 区分 key
            int index = 0;
            foreach (object key in dic.Keys)
            {
                SaveValue(key, keyName + "_key_" + index);
                SaveValue(dic[key], keyName + "_value_" + index);
                ++index;
            }
        }
        //基础数据类型都不是 那么可能就是自定义类型
        else
        {
            SaveData(value, keyName);
        }
    }

    /// 
    /// 读取数据
    /// 
    /// 想要读取数据的 数据类型Type
    /// 数据对象的唯一key 自己控制
    /// 
    public object LoadData( Type type, string keyName )
    {
        //不用object对象传入 而使用 Type传入
        //主要目的是节约一行代码(在外部)
        //假设现在你要 读取一个Player类型的数据 如果是object 你就必须在外部new一个对象传入
        //现在有Type的 你只用传入 一个Type typeof(Player) 然后我在内部动态创建一个对象给你返回出来
        //达到了 让你在外部 少写一行代码的作用

        //根据你传入的类型 和 keyName
        //依据你存储数据时  key的拼接规则 来进行数据的获取赋值 返回出去

        //根据传入的Type 创建一个对象 用于存储数据
        object data = Activator.CreateInstance(type);
        //要往这个new出来的对象中存储数据 填充数据
        //得到所有字段
        FieldInfo[] infos = type.GetFields();
        //用于拼接key的字符串
        string loadKeyName = "";
        //用于存储 单个字段信息的 对象
        FieldInfo info;
        for (int i = 0; i < infos.Length; i++)
        {
            info = infos[i];
            //key的拼接规则 一定是和存储时一模一样 这样才能找到对应数据
            loadKeyName = keyName + "_" + type.Name +
                "_" + info.FieldType.Name + "_" + info.Name;

            //有key 就可以结合 PlayerPrefs来读取数据
            //填充数据到data中 
            info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
        }
        return data;
    }

    /// 
    /// 得到单个数据的方法
    /// 
    /// 字段类型 用于判断 用哪个api来读取
    /// 用于获取具体数据
    /// 
    private object LoadValue(Type fieldType, string keyName)
    {
        //根据 字段类型 来判断 用哪个API来读取
        if( fieldType == typeof(int) )
        {
            return PlayerPrefs.GetInt(keyName, 0);
        }
        else if (fieldType == typeof(float))
        {
            return PlayerPrefs.GetFloat(keyName, 0);
        }
        else if (fieldType == typeof(string))
        {
            return PlayerPrefs.GetString(keyName, "");
        }
        else if (fieldType == typeof(bool))
        {
            //根据自定义存储bool的规则 来进行值的获取
            return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
        }
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //得到长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个List对象 来进行赋值
            //用了反射中双A中 Activator进行快速实例化List对象
            IList list = Activator.CreateInstance(fieldType) as IList;
            for (int i = 0; i < count; i++)
            {
                //目的是要得到 List中泛型的类型 
                list.Add(LoadValue(fieldType.GetGenericArguments()[0], keyName + i));
            }
            return list;
        }
        else if( typeof(IDictionary).IsAssignableFrom(fieldType) )
        {
            //得到字典的长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个字典对象 用父类装子类
            IDictionary dic = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] kvType = fieldType.GetGenericArguments();
            for (int i = 0; i < count; i++)
            {
                dic.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                         LoadValue(kvType[1], keyName + "_value_" + i));
            }
            return dic;
        }
        else
        {
            return LoadData(fieldType, keyName);
        }


        return null;
    }
}


8.22 测试类

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

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

    public List list;

    public Dictionary dic;

    public ItemInfo itemInfo;

    public List itemList;

    public Dictionary itemDic;
}

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()
    {
        //先清除下所有的键值
        PlayerPrefs.DeleteAll();
        //读取数据
        PlayerInfo p = PlayerPrefsDataMgr.Instance.LoadData(typeof(PlayerInfo), "Player1") as PlayerInfo;

        //游戏逻辑中 会去 修改这个玩家数据
        p.age = 18;
        p.name = "D_R";
        p.height = 1000;
        p.sex = true;

        p.itemList.Add(new ItemInfo(1, 99));
        p.itemList.Add(new ItemInfo(2, 199));

        p.itemDic.Add(3, new ItemInfo(3, 1));
        p.itemDic.Add(4, new ItemInfo(4, 2));

        //游戏数据存储
        PlayerPrefsDataMgr.Instance.SaveData(p, "Player1");
    }

    // Update is called once per frame
    void Update()
    {

    }
}


8.22 封装总结

通过上面可以看出仅仅通过一句就可以实现存和取
取: PlayerInfo p = PlayerPrefsDataMgr.Instance.LoadData(typeof(PlayerInfo), “Player1”) as PlayerInfo;
存: PlayerPrefsDataMgr.Instance.SaveData(p, “Player1”);

9、加密思路

  • 一般三个方向用于加密
    • 找不到
      • 将关键数据放到无规律的文件夹内
    • 看不懂
      • 让数据的key和value看不懂,即是为这两个值用一定规则加密
    • 解不出
      • 利用一定算法让别人无法破解

一般来说单机游戏的加密只是提高别人修改你的数据的门槛,只要源代码泄漏,知道加密规则后,一切加密都没意义。但是对于一个普通玩家说,普通的加密就可以了。
PlayerPrefs位置固定了,第一条是没法用的
可以简单的加密:
例如 加密int类型的,原有基础加减,例如原本是玩家是50级,但是你存60级,取得时候减去10,就是50,这样破解者去找50的时候,就找不到。

你可能感兴趣的:(unity,C#,unity,游戏引擎,playerprefs)