Unity使用反射机制和PlayerPrefs来存储游戏数据

前言

Unity中有一个PlayerPrefs用来给游戏存储数据。这个类有三个存储三种特定类型的方法:SetInt用来存储int类型的数据,SetFloat用来存储float类型的数据,SetString用来存储string类型的数据,虽然只能存储三种类型的数据,但是对于一般的游戏而言这三种类型完全够用了。本文封装了一个游戏数据管理类,使用PlayerPrefs来存储和读取游戏数据。这样就不用每次在需要存储数据时不停的调用PlayerPrefs,写很多繁琐的代码。

利用C#中的反射机制来获取数据类型从而对症下药,对不同类型的数据进行不同方式存储,如果你对于反射机制不甚了解也可以先看下去,我会慢慢解释要使用的反射知识点。

需要存储和读取数据类型的类如下所示

class PlayerInfo
{
    public int age ;
    public string name ;
    public float height ;
    public bool sex ;
    public List<int> list;
    public Dictionary<string, int> dic ;

    public ItemInfo itemInfo;
    public List<ItemInfo> itemList;
    public Dictionary<int, ItemInfo> dic2 = new Dictionary<int, ItemInfo>();
}

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

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

    //这里要加上无参构造  负责无法通过反射实例化这个类
    public ItemInfo()
    {
    }
}

这个类中有很多类型的数据,还包括其他类的数据,我们要想办法把这些数据存到本地。 

数据存储读取类

本文所有的代码都是写在一个脚本中的,我会逐步解释不同部分的代码。首先创建一个脚本,这个脚本不需要继承MonoBehaviour。

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

public class PlayerPrefsDataManager 
{


}

以下所有的代码都是写在这个类中的。 

单例模式

首先这个类是一个数据管理类,所以使用单例模式。代码如下

    private static PlayerPrefsDataManager instance = new PlayerPrefsDataManager();

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

    private PlayerPrefsDataManager()
    {

    }

这里解释一点,将构造函数的访问修饰符私有化,防止外部实例化这个类。

存储数据 

这里先给出存储数据的函数SaveDate,再来解释

   public void SaveData(object data, string keyName)
    {
        //通过Type得到传入对象的所有字段
        //然后结合PlayerPrefs来进行存储

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

        #endregion

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

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

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

            //要根据定义的key的拼接规则来进行key的生成
            //Player1_PlayerInfo
            saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;

            //现在得到了Key  按照规则  直接存储
            //如何获取值
            //info.GetValue(data);

            //这里额外封装了一个方法来存储值
            SaveValue(info.GetValue(data), saveKeyName);
        }
        //为了游戏崩溃时数据不丢失  直接存储
        PlayerPrefs.Save();
        #endregion
    }

为了存储一个PlayerInfo类,需要获取其内部的数据。比如我们在游戏开始时实例化了一个PlayerInfo类,这个类中的数据比如年纪等会随着游戏的进行发生变化。在玩家需要存储数据或者在某些特定情况下存储数据时就会调用这个函数。

该函数需要传入两个参数,第一个参数是我们实例化出来的类对象或者各种类型数据,因此使用万物之父object作为类型,第二个参数是一个名字,这个名字不要重复,因为Unity中的PlayerPrefs存储数据是按照键值对的形式存储的,如果名字相同那么数据就会被覆盖,造成数据的丢失,所以当你要存储数据时,保证传入的keyName是独一无二的。

下面来解释第一步

一个类中有很多类型的数据,通过反射可以获取一个类中的所有字段,这里主要使用了两个函数,GetType()和GetFields(),此时fieldInfos中就包含着类的字段(成员变量)的 信息

解释第二步

第二步中我没有写任何代码,我们需要在这一部分设置一个规则,从而保证每一个存储的数据的key是不一样的,我的命名规则在第三步中是这样的:saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;

解释第三步:

遍历第一步中获取的fieldInfos数组。也就是依次对每一个类的每一个成员变量进行存储。通过GetValue来获取每个fieldInfo中存储的具体成员变量信息。这里额外封装了一个存储数据的方法SaveValue。

这里举个例子。

假如我实例化了一个PlayerInfo类p,然后随着游戏逻辑的进行,p内部的年纪变为18,名字变成了小明,身高变为了175,性别为男,就像下面这样

        PlayerInfo p = new PlayerInfo();
        p.age = 18;
        p.name = "小明";
        p.height = 175;
        p.sex = true;
        //存储游戏数据
        PlayerPrefsDataManager.Instance.SaveData(p,"Player1");

这个时候要存储数据,我们调用SaveData方法,将p作为第一个参数,第二个参数自己取一个,我这里就起一个Player1。此时系统开始存储数据,首先第一步通过反射机制获取了p中的所有字段信息,它们存储在fieldInfos中,然后我们依次存储这些字段,需要给每个字段起一个不同的名字,比如age是这样的名字:Playe1_PlayerInfo_Int32_age ,你可以对应每一部分理解一下。然后我通过GetValue方法获取到具体的值,这里age是18,那么最终存储的数据就是18,名字也是唯一的。

最后为了防止游戏崩溃时数据丢失,直接调用Save方法对数据直接进行存储。

存储方法

这边具体来对每一种数据类型进行存储,先给出代码

   private void SaveValue(object value,string keyName)
    {
        //通过PlayerPrefs来进行存储
        //这里需要根据数据类型的不同来决定采用哪一个API来进行存储
        //PlayerPrefs只支持3种类型存储
        //判断数据类型是什么类型  然后调用具体的方法来存储
        Type field = value.GetType();
        
        //类型判断
        if(field == typeof(int))
        {
            //Debug.Log("存储int    " + keyName);
            PlayerPrefs.SetInt(keyName, (int)value);
        }
        else if(field == typeof(float))
        {
            //Debug.Log("存储float   " + keyName);
            PlayerPrefs.SetFloat(keyName,(float)value);
        }
        else if(field == typeof(string))
        {
            //Debug.Log("存储string     " + keyName);
            PlayerPrefs.SetString(keyName, (string)value);
        }
        else if(field == typeof(bool))
        {
            //Debug.Log("存储bool    " + keyName);
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }
        else if(typeof(IList).IsAssignableFrom(field))
        {
            //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;
            }
        }
        //通过父类判断子类
        else if(typeof(IDictionary).IsAssignableFrom(field))
        {
            //Debug.Log("存储Dictionary    " + keyName);
            IDictionary dictionary = value as IDictionary;
            //先存长度
            PlayerPrefs.SetInt(keyName, dictionary.Count);
            //遍历存储字典里面的具体值
            //区分标识
            int index = 0;
            foreach (object key in dictionary.Keys)
            {
                SaveValue(key, keyName + "_key_" + index);
                SaveValue(dictionary[key], keyName + "_value_" + index);
                ++index;
            }
        }
        //基础数据类型都不是  那么就是自定义类型
        else
        {
            SaveData(value, keyName);
        }
    }

这个函数的逻辑是针对不同类型的数据结合PlayerPrefs的现有的三种存储方法对数据进行存储。

该函数中首先通过反射来获取输入的数据是什么类型,也就是  Type field = value.GetType();

然后根据field不同的类型对数据进行存储。

常用类型

对于int,float,string,PlayerPrefs都有专门的存储方法。对于bool类型的数据,是这样存储的,它为true时存储为1,为false时存储为0,这里写了一个三目运算符来判断。

List和Dictionary方法

列表和字典有着独特的存储方法。先来具体介绍列表

首先通过IsAssignableFrom函数来判断field是不是列表,C#中列表单独继承了一个接口IList,这个接口是List特有的。通过这一个条件typeof(IList).IsAssignableFrom(field)来判断field是不是继承了IList,只有List才继承了这个接口,所以当条件为真时,那么field一定是List。

接下来通过里氏替换原则将函数传入的value转换为IList。

对于List的存储,需要首先存储List的长度,然后再存储具体的值。这里有一个递归的逻辑。来举个例子。

假如有一个List<int> list = new List<int>() {1,2,3}。来看看是如何存储的。

首先经过判断之后会将整个List装载到一个IList中,然后去遍历这个IList,第一个数据是1,那么会使用int的存储方法进行存储,第二个第三个也是int存储。存储起来的1的keyName各位可以思考下。

对于Dictionary也是一样的道理,只不过相较于List,Dictionary需要存储键值对。

自定义数据类型

当要存储的数据是自定义数据类型时直接使用SaveData即可。比如要存储的是ItemInfo类型的数据item1,当条件判断到最后一个,会调用SaveData函数,这样会将这个数据以类型于PlayerInfo的样式存储起来,那么存储的item1的keyName是什么呢?各位可以思考下。

读取数据

有了存储,读取也很简单

    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];
            loadKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;

            //填充数据到data种
            info.SetValue(data, loadValue(info.FieldType, loadKeyName));
        }

        return data;
    }

传入的是数据类型和数据的名称

首先使用Activator.CreateInstance来实例化一个对象,注意在有参构造的类中要额外加上无参构造,这样才可以通过这个方法实例一个类对象。

然后要通过loadValue将数据存储到data中然后返回出去。

读取方法
    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))
        {
            //得到长度
            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))
        {
            //得到长度
            int count = PlayerPrefs.GetInt(keyName, 0);
            IDictionary dictionary = Activator.CreateInstance(fieldType) as IDictionary;
            Type[] kvType = fieldType.GetGenericArguments();
            for (int i = 0; i < count; i++)
            {
                dictionary.Add(loadValue(kvType[0], keyName + "_key_" + + i), loadValue(kvType[1], keyName + "_value_" + i));
            }
            return dictionary;
        }
        else
        {
            return LoadData(fieldType, keyName);
        }
    }

这里说一下List和Dictionary的读取,这里额外使用了获得泛型的方法fieldType.GetGenericArguments(),比如List<int>,那么该方法得到的就是int,如果是Dictionary<string,int>,那么得到的就是一个数组,里面包含string和int。

其余的和存储的差不多原理。这里不在过多赘述。

测试

创建一个测试脚本。内容如下

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> list;
    public Dictionary<string, int> dic ;

    public ItemInfo itemInfo;
    public List<ItemInfo> itemList;
    public Dictionary<int, ItemInfo> dic2 = new Dictionary<int, ItemInfo>();
}

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

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

    //这里要加上无参构造  负责无法通过反射实例化这个类
    public ItemInfo()
    {
    }
}

public class Test : MonoBehaviour
{
    
    void Start()
    {
        //先清空数据,方便测试
        PlayerPrefs.DeleteAll();
        //读取数据
        //PlaeyerInfo p2 = PlayerPrefsDataManager.Instance.LoadData(typeof(PlaeyerInfo), "Player1") as PlaeyerInfo;

        //读取数据  
        //这里不会有数据 
        PlayerInfo p = PlayerPrefsDataManager.Instance.LoadData(typeof(PlayerInfo), "Player1") as PlayerInfo;

        //在游戏逻辑中我们回去修改玩家的数据
        p.age = 18;
        p.name = "小明";
        p.height = 175;
        p.sex = true;

        //存了一次数据  再执行代码  里面就有3的数据  字典的key不可以重复
        p.itemList.Add(new ItemInfo(1, 99));
        p.itemList.Add(new ItemInfo(2, 100));

        p.dic2.Add(3, new ItemInfo(3, 101));
        p.dic2.Add(4, new ItemInfo(4, 102));

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

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

先将Unity运行一下这个代码,别忘了挂载脚本。然后结束运行。回到脚本中注释掉PlayerPrefs.DeleteAll();这一句代码,在 PlayerInfo p = PlayerPrefsDataManager.Instance.LoadData(typeof(PlayerInfo), "Player1") as PlayerInfo;打上一个端点,附加到Unity再运行游戏,回到了VS中F10一下。

你可以看到如下效果

这里p里面已经有数据了。说明读取成功了。

不要再运行代码了,因为接下来的代码会继续向源数据的字典中相加相同的键。这会导致报错。

来说一下上面每一步的意义

当我们开启游戏时,会去读取角色数据。此时会将数据赋给新实例化的类对象。

然后在游戏中经过一系列操作,角色的数据会发生变化,比如我们要存盘的时候就将现有的数据存起来了覆盖掉原有的数据

然后我们关闭游戏,再次开启时读取的就是修改后的数据了。

大家可以好好理解下。

最后再说一下存储的数据在哪里查看。

右击Windows图标,点击运行,输入regeit,如下图

点击确认,然后查看这个目录HKCU\Software\Unity\UnityEditor\[公司名称]\[产品名称]中,公司名称和项目名称可以在这里查看

这样你可以看到具体的数据了,如下图

对照着每一个key,你可以判断之前自己思考的keyName对不对。

你也可以在这里直接修改数据,比如这里存储的有一个数据时攻击力是10,你可以直接改成1000,然后你下次读取游戏你的攻击力就变成1000了,这就是开挂(狗头)。但是数据一般都会加密,所以一般的单机游戏不可能这么傻乎乎的存储的,死了这条心吧。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值