Unity数据持久化之PlayerPrefs

一、什么是数据持久化  

     大家都玩过游戏吧,大家玩完游戏之后肯定希望自己的游戏数据得以保存。那么就需要用到数据持久化,数据并不仅仅只是在内存中,更要存储在硬盘上,才能保证游戏数据不丢失。

     在 Unity 中,数据持久化是指在游戏运行结束后,某些数据(如玩家的游戏进度、设置、或统计信息)能够被保存下来,并在下次启动游戏时仍然可用。数据持久化是游戏开发中的常见需求,用于确保玩家的游戏体验不会因为退出游戏而丢失重要信息。

二、数据持久化之PlyerPrefs

        在unity中有很多实现数据持久化的方式,本文我们仅介绍如何利用PlayerPrefs实现最简单的数据持久化功能,以后我们在学习更加高级方便的技术。

2.1PlayerPrefs是什么

Unity 提供了一个简单的键值对存储系统,称为 PlayerPrefs,用于保存简单的游戏数据。

  • 适用场景:小型数据(如分数、音量设置、用户名等)。
  • 优点:易于实现,跨平台支持。
  • 缺点:数据只能存储字符串、整数和浮点数,安全性较低(容易被玩家修改)。

2.2利用PlayerPrefs进行数据存储

        //PlayerPrefs的数据存储 类似于键值对 一个键就对应一个值
        //提供了存储三种数据的方法 int string float
        //键:string类型
        //值:int string float 对应三种API

        PlayerPrefs.SetInt("myAge", 1);
        PlayerPrefs.SetFloat("myHeight", 1.7f);
        PlayerPrefs.SetString("myName", "未命名");

其中:

PlayerPrefs.SetInt
PlayerPrefs.SetFloat
PlayerPrefs.SetString

        分别为存储数据使用的API,是不是很简单,仅仅只需要调用这么一句话就可以将你的数据存进去了!那么会有小伙伴想问了,那如果我想存储其他类型的数据怎么办嘞,比如数组,列表,等等。很抱歉,PlayerPrefs本身并没有直接提供能够让我们可以使用的API进行特殊数据类型的存储,于是我们可以自己想办法,通过设置一定的规则进行这些特殊的存储。

        对了,以上的这些API并不是当你运行游戏的时候,一边运行一边保存,而是等到运行结束后才会存到硬盘上。那又有人要问了,那那要是游戏崩溃了咋办呢,别担心,Unity还贴心的准备了一个API叫做

        PlayerPrefs.Save();

这个API可以帮助我们在运行的时候,只要想保存数据就可以随时调用它。

2.3存储的位置

        每个平台上数据的存储的位置是不一样的。

Windows:

#region Windows
//PlayerPrefers存储的数据在Windows平台存储在注册表里
//HKCU\Software\[公司名称]\[产品名称] 项下的注册表中
//其中 公司和产品名称 是在“Project Settings”中设置得名称

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

Android:

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

IOS:

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

2.4利用PlayerPrefs进行数据读取 

        刚才前面讲了存储,存进去了肯定要读得出来才是有用的数据!不然就白存了,也就前面都在浪费精力。下面讲述读取使用的API,也是超级简单。

//int
int age = PlayerPrefs.GetInt("myAge");
int age = PlayerPrefs.GetInt("myAge",18);
//float
float height = PlayerPrefs.GetFloat("myHeight");
float height = PlayerPrefs.GetFloat("myHeight", 100f);
//string
string name = PlayerPrefs.GetString("myName");
string name = PlayerPrefs.GetString("myName","123");

        每个API都有一个重载,后面用来填默认值,也就是说如果你的数据是没有的,不存在的,你可以在读取的时候自己顺手加一个!还有一个没啥用的API:这个就是用来判断你的数据存不存在的(一般你都读取了还判断干啥)

 //判断数据是否存在
 if (PlayerPrefs.HasKey("myAge"))
 {
     print("存在");
 }

        注意: PlyerPrefs里面的数据存储类似于键值对匹配,也就是说这个名字的数据你只能存一个,如果你有两个不同类型的数据存储到了一个名字中,他只会根据你读取的方式来判断数据是什么。比如,你将一个float类型的数据存储到了一个int类型的名字中,那么使用GetInt读取到的是0,数值类型默认值为0,字符串默认为空。如果不同类型用同一键名进行存储 会进行覆盖 其实只要是相同键都会被覆盖

三、利用PlayerPrefs设计一个存储数据管理器

这个存储数据管理器可以存储int float string bool list dictionary 自定义类这几种数据类型,大部分情况下是够用了的,当然你可以自己选择扩展。

3.1必备小知识之反射

 #region 知识点一 反射知识的回顾
 //反射三剑客 -- 1T 和两A
 //Type --用于获取类的所有信息 字段 属性 方法 等等
 //Assembly --用于获取程序集的信息 通过程序集获取Type
 //Activator --用于快速实例化对象
 #endregion

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

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

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

 #endregion

 #region 知识点三 通过反射获取泛型类型

 List list = new List();
 Type listType = list.GetType();
 //获取泛型类型
 Type[] genericArguments = listType.GetGenericArguments();
 foreach (Type type in genericArguments)
 {
     Debug.Log(type);
 }
 Dictionary dic = new Dictionary();
 Type dicType = dic.GetType();
 genericArguments = dicType.GetGenericArguments();
 foreach (Type type in genericArguments)
 {
     Debug.Log(type);
 }
 #endregion

上面比较重要的就是两个API,一个是IsAssignableFrom()用于判断是否可以为自己分配存储空间,和父类装子类差不多,他返回的是一个bool值,还有一个就是获取对象的类型的API,记忆一下,稍后会使用的。

3.2单例模式

由于我们想实现一个数据管理器,就是可以存储多种类型,而且可以随时进行数据存储,不用像上面一样每次都要调用多个API,所以我们想到设计一个单例模式的数据存储管理器,来帮助我们进行开发高效率。

示例:

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

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

    public List list;

    public Dictionary dic = new Dictionary()
    {
        {1,"123"},
        {2,"456"}
    };

    public ItemInfo itemInfo;

    public List itemInfoList;

    public Dictionary itemInfoDic;
}

public class ItemInfo
{
    public int id;
    public string name;

    public ItemInfo()
    {
    }
    public ItemInfo(int id, string name)
    { 
        this.id = id;
        this.name = name;
    }
}
public class test : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //PlayerPrefs.DeleteAll();
        //读取数据
        PlayerInfo p2 = PlayerPrefsDataManager.Instance.LoadData(typeof(PlayerInfo),"player1") as PlayerInfo;

        //游戏逻辑中 会去修改这个玩家数据
        p2.name = "张三";
        p2.age = 18;
        p2.height = 1.88f;
        p2.width = 1.88f;
        p2.sex = true;

        p2.itemInfoList.Add(new ItemInfo(1, "123"));
        p2.itemInfoList.Add(new ItemInfo(2, "456"));
        p2.itemInfoDic.Add(1, new ItemInfo(8, "123"));
        p2.itemInfoDic.Add(2, new ItemInfo(9, "456"));

        //保存数据
        PlayerPrefsDataManager.Instance.SaveData(p2, "player1");
    }

}

上述示例代码就是利用数据管理器实现了数据的快速存储和读取。下面我们逐步实现这个数据管理器。

3.2补充

单例实现:
 

    private static PlayerPrefsDataManager instance = new PlayerPrefsDataManager();

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

    private PlayerPrefsDataManager()
    {

    }

3.3存储数据

        存储数据咱们分三步走,第一步,明白你存储的数据是什么类型的和名字;第二步,自定义个数据存储的规则,前面说到给重复名字的赋值就会进行覆盖操作,为了避免我们的数据被覆盖掉,所以我们必须得给每个数据定一个独一无二的名字,于是第二步很重要;第三步,就是将第一步获取到的数据类型和名字,逐一遍历然后存进咱们的硬盘中。怎么样听起来是不是很简单,好!接下来我们逐步实现。

3.3.1三步走存储数据

第一步:

 //就是要通过 Type得到传入数据对象的所有的字段
 //然后结合 PlayerPrefs存储字段

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

 #endregion

第二步:

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

//我们定的规则是:
//keyName_数据类型_字段类型_字段名
#endregion

第三步:

string saveKeyName = "";

FieldInfo info = null;
#region 第三步 遍历这些字段 进行数据存储
for (int i = 0; i < infos.Length; i++)
{
    //得到具体的字段信息
    info = infos[i];
    //通过FieldInfo 得到字段名 字段类型 字段值
    //字段的类型 info.FieldType.Name
    //字段的名字 info.Name
    //字段的值 info.GetValue(data)
    //要根据我们定的Key的规则来拼接key
    //Player1_PlayerInfo_Int32_age
    saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;
    //现在得到了key 按照我们的规则存储数据
    //如何获取值
    //info.GetValue(data);

    //封装了一个方法 专门用来存储值
    SaveValue(info.GetValue(data), saveKeyName);
}

PlayerPrefs.Save();
#endregion

有同学发现了我们在进行数据存储的时候并没有直接使用刚才提到的几个关键API而是写了一个函数进行存储值,这有什么好处呢,我们接着往下看。

3.3.2实现存储数据

存储数据完整代码SaveData:

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

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

    #endregion

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

    //我们定的规则是:
    //keyName_数据类型_字段类型_字段名
    #endregion
    string saveKeyName = "";

    FieldInfo info = null;
    #region 第三步 遍历这些字段 进行数据存储
    for (int i = 0; i < infos.Length; i++)
    {
        //得到具体的字段信息
        info = infos[i];
        //通过FieldInfo 得到字段名 字段类型 字段值
        //字段的类型 info.FieldType.Name
        //字段的名字 info.Name
        //字段的值 info.GetValue(data)
        //要根据我们定的Key的规则来拼接key
        //Player1_PlayerInfo_Int32_age
        saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;
        //现在得到了key 按照我们的规则存储数据
        //如何获取值
        //info.GetValue(data);

        //封装了一个方法 专门用来存储值
        SaveValue(info.GetValue(data), saveKeyName);
    }

    PlayerPrefs.Save();
    #endregion
}

接下来我们讲讲这个SaveValue里面的名堂

因为你不知道存的是个什么东西,所以我们想到使用object来装,和一个字符串记录存储的数据名字所以可以这样来声明函数。

 private void SaveValue(object value, string keyName)
 {

    
 }

好了接下来不就是在函数里面对我们每一种 类型进行存储嘛,简单。我们继续往下走:

首先你要获取它的类型:
 

       //直接通过PlayerPrefs存储数据
       //就是直接根据数据类型的不同 来决定使用哪一个API来进行存储
       Type fieldType = value.GetType();

获取到类型了接下来就好办了:

①int类型的数据:

        if (fieldType == typeof(int))
        {
            //Debug.Log("存储int类型的数据" + keyName);
            PlayerPrefs.SetInt(keyName, (int)value);
        }

②float类型的数据:

        else if (fieldType == typeof(float))
        {
            //Debug.Log("存储float类型的数据" + keyName);
            PlayerPrefs.SetFloat(keyName, (float)value);
        }

③string类型的数据:

        else if (fieldType == typeof(string))
        {
            //Debug.Log("存储string类型的数据" + keyName);
            PlayerPrefs.SetString(keyName, value.ToString());
        }

④bool类型的数据:

        大家可以想想,它里面本来没有bool类型的数据我们该怎样进行存储呢?当然是自定义规则的啦,我们可以利用三目运算符,规定 1 =t rue 0 = false,这样我们就只用存储int类型的数据不就可以了吗!你简直就是个天才。

        else if (fieldType == typeof(bool))
        {
            //Debug.Log("存储bool类型的数据" + keyName);
            //自己定义的存储bool的规则
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }

⑤list类型的数据:

        这是一个难点,因为list是一个泛型,也就是有超级无敌多的类型,咱们总不可能写无穷多个吧,显然不可能。这里我们就要用到前面提到的父类装子类的方法,也就是利用IsAssignableFrom这个API来进行判断,某个类可不可以给咱们分配空间。我们就找啊找,发现所有的list都继承了IList这个接口,也就是说只要是继承了这个接口的都是list类型,对不对,这样就找到了突破口。于是设计代码如下:

        那对list类型的数据应该如何存储呢,首先应该存有多少个吧,也就是list的长度,其次也就是里面的内容吧,所以要遍历一个一个存。注意一个一个存的时候,由于PlayerPrefers数据唯一性,所以我们要保证key的唯一性,故使用index。

        //PlayerPrefs中不同数据的唯一性
        //是由Key决定的 不同的Key决定了 不同的数据
        //同一项目中 如果不同的数据逇Key相同 会造成数据的丢失
        //要保证数据不丢失 就要建立一个保证Key唯一性的规则

//之所以使用的是IList是因为发现List的最底层继承的是这个
//通过反射 判断父子关系
//这相当于是判断 字段是不是Ilist的子类
else if (typeof(IList).IsAssignableFrom(fieldType))
{
    //Debug.Log("存储IList类型的数据" + "_" + keyName);
    //父类装子类
    IList list = (IList)value;
    //先存储 数量
    PlayerPrefs.SetInt(keyName, list.Count);
    int index = 0;
    foreach (object obj in list)
    {
        SaveValue(obj, keyName + index);
        index++;
    }
}

⑥Dictionary类型的数据:

        字典不就是复杂一点的列表咯,原理和上面一样,也是找一个所有字典都有的共性然后分配空间,设计代码如下:

//判断是不是Dictionary类型 通过Dictionary的父类进行判断
else if (typeof(IDictionary).IsAssignableFrom(fieldType))
{
    //Debug.Log("存储IDictionary类型的数据" + "_" + keyName);
    //父类装子类
    IDictionary dic = (IDictionary)value;
    //先存字典长度
    PlayerPrefs.SetInt(keyName, dic.Count);
    //遍历存储字典中的具体的值
    //用于区分 表示的 区分key
    int index = 0;
    foreach (object key in dic.Keys)
    {
        //存储key
        SaveValue(key, keyName + "_key_" + index);
        SaveValue(dic[key], keyName + "_value_" + index);
        ++index;
    }
}

⑦自定义类型

        你看,你前面定义了这么多种类型剩下的是不是只有自定义类型了呢,所以很简单,代码如下:

        //判断是不是自定义类型
        else
        {
            SaveData(value, keyName);
        }

这里我们要好好理解一下,他这里其实调用的是上面的函数,也就是他在不断的递归,他通过递归将自己这个自定义的类中的不同的类型的数据逐一存到了硬盘中。也就是将自己这个类的数据存了进去,所以是自定义的类存储数据。

SaveValue函数实现:

   private void SaveValue(object value, string keyName)
   {
       //直接通过PlayerPrefs存储数据
       //就是直接根据数据类型的不同 来决定使用哪一个API来进行存储
       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是因为发现List的最底层继承的是这个
       //通过反射 判断父子关系
       //这相当于是判断 字段是不是Ilist的子类
       else if (typeof(IList).IsAssignableFrom(fieldType))
       {
           //Debug.Log("存储IList类型的数据" + "_" + keyName);
           //父类装子类
           IList list = (IList)value;
           //先存储 数量
           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("存储IDictionary类型的数据" + "_" + keyName);
           //父类装子类
           IDictionary dic = (IDictionary)value;
           //先存字典长度
           PlayerPrefs.SetInt(keyName, dic.Count);
           //遍历存储字典中的具体的值
           //用于区分 表示的 区分key
           int index = 0;
           foreach (object key in dic.Keys)
           {
               //存储key
               SaveValue(key, keyName + "_key_" + index);
               SaveValue(dic[key], keyName + "_value_" + index);
               ++index;
           }
       }
       //判断是不是自定义类型
       else
       {
           SaveData(value, keyName);
       }
   }

3.4读取数据

        有存就必然会读取数据,不然存取也就失去了意义,接下来咱们实现数据的读取。

        假设你前面的看懂了,那么读取自然也会是简简单单的。同样我们数据的读取也分为读取类型和名字函数,设置值函数。

LoadData:既然我都读取数据了,那么肯定我是知道我读取的数据类型是什么(因为是你自己存的),所以我们设计函数时候,只需要传入两个参数,一个是类型,一个是名字。

 /// 
 /// 读取数据
 /// 
 /// 想要读取的数据类型
 /// 数据对象的唯一key,自己控制
 /// 
 public object LoadData(Type type, string keyName)
 {

     return data;
 }

接下来和上面的差不多,先根据你获取到的类型创建一个对象,然后往这个对象中赋值,最后传出这个对象就可以了!设计代码如下:

//根据你传入的类型 和 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 就可以结合PlayPrefs来获取数据
    //填充数据到data中
    info.SetValue(data, LoadValue(info.FieldType, loadKeyName));
}

然后我们看看这个LoadValue

和上面差不多就是根据不同的类型读出不同的数据:

①int类型:

        //根据字段类型 判断用哪个API来读取数据
        if (fieldType == typeof(int))
        {
            return PlayerPrefs.GetInt(keyName, 0);
        }

②float类型

        else if (fieldType == typeof(float))
        {
            return PlayerPrefs.GetFloat(keyName, 0);
        }

③string类型

        else if (fieldType == typeof(string))
        {
            return PlayerPrefs.GetString(keyName, " ");
        }

④bool类型

        else if (fieldType == typeof(bool))
        {
            //根据自定义的规则来读取数据
            return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
        }

⑤list类型

 else if (typeof(IList).IsAssignableFrom(fieldType))
 {
     //Debug.Log("读取IList类型的数据" + "_" + keyName);
     //得到List的长度
     int count = PlayerPrefs.GetInt(keyName, 0);
     //实例化一个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;
 }

        注意这里我们使用了一个GetGenericArguments的函数这个是为了获取列表的泛型类型的,这里也用到了递归,获取到了这个类型,然后在根据这个类型进行数据的下一步存储。

        为什么要实例化呢,因为你没东西装呀不实例化的话。你没东西装你怎么返回出去值呢。下面也是同理。 

⑥Dictionary类型

else if (typeof(IDictionary).IsAssignableFrom(fieldType))
{
    //Debug.Log("读取IDictionary类型的数据" + "_" + keyName);
    //得到字典的长度
    int count = PlayerPrefs.GetInt(keyName, 0);
    //实例化一个Dictionary 用父类装子类
    IDictionary dict = Activator.CreateInstance(fieldType) as IDictionary;
    Type[] kvType = fieldType.GetGenericArguments();
    for (int i = 0; i < count; i++)
    {
        dict.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                 LoadValue(kvType[1], keyName + "_value_" + i));
    }
    return dict;
}

⑦自定义类型

这里和存储数据的时候一样的,递归反复调用进入下一次存储。

        else
        {
            //Debug.Log("读取其他类型的数据" + "_" + keyName);
            return LoadData(fieldType, keyName);
        }

LoadValue完整代码:

 /// 
 /// 得到单个数据的方法
 /// 
 /// 字段类型 用于判断 用哪个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))
     {
         //根据自定义的规则来读取数据
         return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
     }
     else if (typeof(IList).IsAssignableFrom(fieldType))
     {
         //Debug.Log("读取IList类型的数据" + "_" + keyName);
         //得到List的长度
         int count = PlayerPrefs.GetInt(keyName, 0);
         //实例化一个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))
     {
         //Debug.Log("读取IDictionary类型的数据" + "_" + keyName);
         //得到字典的长度
         int count = PlayerPrefs.GetInt(keyName, 0);
         //实例化一个Dictionary 用父类装子类
         IDictionary dict = Activator.CreateInstance(fieldType) as IDictionary;
         Type[] kvType = fieldType.GetGenericArguments();
         for (int i = 0; i < count; i++)
         {
             dict.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                      LoadValue(kvType[1], keyName + "_value_" + i));
         }
         return dict;
     }
     else
     {
         //Debug.Log("读取其他类型的数据" + "_" + keyName);
         return LoadData(fieldType, keyName);
     }
 }

四、总结

        PlayerPrefs只是Unity中最简单的数据存储工具,可以先简单了解,然后为以后学高级技术打基础。读者可以尝试自定义一些其他的类型例如double,sbyte等数据类型进行自我练习。刚才文章中的难就是在自定义list和字典时候,设定的规则比较特殊,不过你反复摸索后肯定能够实现的!一起加油!

附:数据管理器完整代码

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

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

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

    private PlayerPrefsDataManager()
    {

    }

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

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

        #endregion

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

        //我们定的规则是:
        //keyName_数据类型_字段类型_字段名
        #endregion
        string saveKeyName = "";

        FieldInfo info = null;
        #region 第三步 遍历这些字段 进行数据存储
        for (int i = 0; i < infos.Length; i++)
        {
            //得到具体的字段信息
            info = infos[i];
            //通过FieldInfo 得到字段名 字段类型 字段值
            //字段的类型 info.FieldType.Name
            //字段的名字 info.Name
            //字段的值 info.GetValue(data)
            //要根据我们定的Key的规则来拼接key
            //Player1_PlayerInfo_Int32_age
            saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;
            //现在得到了key 按照我们的规则存储数据
            //如何获取值
            //info.GetValue(data);

            //封装了一个方法 专门用来存储值
            SaveValue(info.GetValue(data), saveKeyName);
        }

        PlayerPrefs.Save();
        #endregion
    }

    private void SaveValue(object value, string keyName)
    {
        //直接通过PlayerPrefs存储数据
        //就是直接根据数据类型的不同 来决定使用哪一个API来进行存储
        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是因为发现List的最底层继承的是这个
        //通过反射 判断父子关系
        //这相当于是判断 字段是不是Ilist的子类
        else if (typeof(IList).IsAssignableFrom(fieldType))
        {
            //Debug.Log("存储IList类型的数据" + "_" + keyName);
            //父类装子类
            IList list = (IList)value;
            //先存储 数量
            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("存储IDictionary类型的数据" + "_" + keyName);
            //父类装子类
            IDictionary dic = (IDictionary)value;
            //先存字典长度
            PlayerPrefs.SetInt(keyName, dic.Count);
            //遍历存储字典中的具体的值
            //用于区分 表示的 区分key
            int index = 0;
            foreach (object key in dic.Keys)
            {
                //存储key
                SaveValue(key, keyName + "_key_" + index);
                SaveValue(dic[key], keyName + "_value_" + index);
                ++index;
            }
        }
        //判断是不是自定义类型
        else
        {
            SaveData(value, keyName);
        }
    }
    /// 
    /// 读取数据
    /// 
    /// 想要读取的数据类型
    /// 数据对象的唯一key,自己控制
    /// 
    public object LoadData(Type type, string keyName)
    {
        //不用object对象传入 而使用Type传入
        //主要目的是为了节约一行代码(在外部)
        //假设现在你要 读取一个Player类型的数据 如果是object 你就必须得在外部new一个对象传入
        //现在有Type类型,你只需要传入Player类型即可 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 就可以结合PlayPrefs来获取数据
            //填充数据到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))
        {
            //根据自定义的规则来读取数据
            return PlayerPrefs.GetInt(keyName, 0) == 1 ? true : false;
        }
        else if (typeof(IList).IsAssignableFrom(fieldType))
        {
            //Debug.Log("读取IList类型的数据" + "_" + keyName);
            //得到List的长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个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))
        {
            //Debug.Log("读取IDictionary类型的数据" + "_" + keyName);
            //得到字典的长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            //实例化一个Dictionary 用父类装子类
            IDictionary dict = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] kvType = fieldType.GetGenericArguments();
            for (int i = 0; i < count; i++)
            {
                dict.Add(LoadValue(kvType[0], keyName + "_key_" + i),
                         LoadValue(kvType[1], keyName + "_value_" + i));
            }
            return dict;
        }
        else
        {
            //Debug.Log("读取其他类型的数据" + "_" + keyName);
            return LoadData(fieldType, keyName);
        }
    }
}

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