Unity uses reflection mechanism and PlayerPrefs to store game data

Preface

There is a PlayerPrefs in Unity that is used to store data for the game. This class has three methods for storing three specific types: SetInt is used to store int type data, SetFloat is used to store float type data, and SetString is used to store string type data. Although only three types of data can be stored, For general games, these three types are completely sufficient. This article encapsulates a game data management class and uses PlayerPrefs to store and read game data. This eliminates the need to call PlayerPrefs every time you need to store data and write a lot of tedious code.

Use the reflection mechanism in C# to obtain the data type so as to prescribe the right medicine, and store different types of data in different ways. If you don't know much about the reflection mechanism, you can read on first. I will slowly explain the reflection knowledge points to be used.

The classes that need to store and read data types are as follows

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()
    {
    }
}

There are many types of data in this class, including data of other classes. We have to find a way to store these data locally.​ 

Data storage reading class

All the code in this article is written in a script, and I will explain the different parts of the code step by step. First create a script, which does not need to inherit MonoBehaviour.

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

public class PlayerPrefsDataManager 
{


}

All the following code is written in this class.​ 

Singleton pattern

First of all, this class is a data management class, so it uses singleton mode. code show as below

    private static PlayerPrefsDataManager instance = new PlayerPrefsDataManager();

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

    private PlayerPrefsDataManager()
    {

    }

Here is a little explanation: privatize the access modifier of the constructor to prevent external instantiation of this class.

Storing data 

Here we first give the function SaveDate to store data, and then explain it.

   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
    }

In order to store a PlayerInfo class, you need to obtain its internal data. For example, we instantiate a PlayerInfo class at the beginning of the game. The data in this class, such as age, will change as the game progresses. This function is called when the player needs to store data or store data under certain circumstances.

This function needs to pass in two parameters. The first parameter is the class object or various types of data we instantiated, so we use object, the father of all things, as the type. The second parameter is a name. Do not repeat this name, because PlayerPrefs in Unity stores data in the form of key-value pairs. If the names are the same, the data will be overwritten, causing data loss. Therefore, when you want to store data, ensure that the keyName passed in is unique.

Let’s explain the first step

There are many types of data in a class. All fields in a class can be obtained through reflection. Two functions are mainly used here, GetType() and GetFields(). At this time, fieldInfos contains the fields (member variables) of the class. Information

Explain step two

I didn't write any code in the second step. We need to set a rule in this part to ensure that the key of each stored data is different. My naming rule in the third step is like this: saveKeyName = keyName + "_" + type.Name + "_" + info.FieldType.Name + "_" + info.Name;

Explain step three:

Traverse the fieldInfos array obtained in the first step. That is, each member variable of each class is stored in turn. Get the specific member variable information stored in each fieldInfo through GetValue. An additional method SaveValue for storing data is encapsulated here.

Here's an example.

If I instantiate a PlayerInfo class p, then as the game logic progresses, the age inside p becomes 18, the name becomes Xiao Ming, the height becomes 175, and the gender is male, like the following

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

To store data at this time, we call the SaveData method, taking p as the first parameter, and take one of the second parameters. I will use Player1 here. At this point, the system starts to store data. First, the first step is to obtain all the field information in p through the reflection mechanism. They are stored in fieldInfos. Then we store these fields in turn. We need to give each field a different name. For example, age is Such a name: Playe1_PlayerInfo_Int32_age, you can understand each part accordingly. Then I get the specific value through the GetValue method. Here age is 18, so the final stored data is 18, and the name is also unique.

Finally, in order to prevent data loss when the game crashes, the Save method is directly called to store the data directly.

Storage method

Here we will specifically store each data type, first give the code

   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);
        }
    }

The logic of this function is to store different types of data in combination with the three existing storage methods of PlayerPrefs.

In this function, the type of input data is first obtained through reflection, that is, Type field = value.GetType();

Then the data is stored according to different types of fields.

Common types

There are special storage methods for int, float, string and PlayerPrefs. For bool type data, it is stored like this. When it is true, it is stored as 1, and when it is false, it is stored as 0. A ternary operator is written here to judge.

List and Dictionary methods

Lists and dictionaries have unique storage methods. Let’s first introduce the list in detail

First, use the IsAssignableFrom function to determine whether the field is a list. Lists in C# inherit an interface IList alone, which is unique to List. Use this condition typeof(IList).IsAssignableFrom(field) to determine whether the field inherits IList. Only List inherits this interface, so when the condition is true, then the field must be List.

Next, the value passed in by the function is converted into an IList through the Liskov substitution principle.

For the storage of List, you need to store the length of the List first, and then store the specific value. There is a recursive logic here. Let’s give an example.

Suppose there is a List<int> list = new List<int>() {1,2,3}. Let’s see how it is stored.

First, after judgment, the entire List will be loaded into an IList, and then the IList will be traversed. If the first data is 1, then the int storage method will be used to store it, and the second and third data will also be stored in int. You can think about the stored keyName of 1.

The same is true for Dictionary, except that compared to List, Dictionary needs to store key-value pairs.

custom data type

When the data to be stored is a custom data type, just use SaveData directly. For example, the data item1 of type ItemInfo is to be stored. When the condition is judged to the last one, the SaveData function will be called, which will store the data in a style of type PlayerInfo. So what is the keyName of the stored item1? You can think about it.

Read data

With storage, reading is also very simple

    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;
    }

What is passed in is the data type and the name of the data

First use Activator.CreateInstance to instantiate an object. Note that an additional no-parameter construct must be added to the class with parameter construction, so that a class object can be instantiated through this method.

Then store the data into data through loadValue and then return it.

Reading method
    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);
        }
    }

Let’s talk about the reading of List and Dictionary. Here we use the additional method fieldType.GetGenericArguments() to obtain generic types. For example, List<int>, then what this method gets is int. If it is Dictionary<string,int>, then we get is an array containing string and int.

The rest is similar to storage. I won’t go into too much detail here.

test

Create a test script. The content is as follows

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()
    {
        
    }
}

First run this code in Unity, don’t forget to mount the script. Then end the run. Go back to the script and comment out the code PlayerPrefs.DeleteAll();, add an endpoint in PlayerInfo p = PlayerPrefsDataManager.Instance.LoadData(typeof(PlayerInfo), "Player1") as PlayerInfo;, attach it to Unity and run the game. Go back to VS and press F10.

You can see the following effect

There is already data in p here. It indicates that the reading was successful.

Don't run the code again, because the rest of the code will continue to add the same keys to the dictionary of the source data. This will result in an error.

Let’s talk about the meaning of each step above.

When we start the game, we will read the character data. At this point the data will be assigned to the newly instantiated class object.

Then after a series of operations in the game, the character's data will change. For example, when we want to save, we will save the existing data and overwrite the original data.

Then we close the game, and when we open it again, the modified data will be read.

Everyone can understand it well.

Finally, let’s talk about where to view the stored data.

Right-click the Windows icon, click Run, enter regeit, as shown below

Click Confirm, and then view this directory HKCU\Software\Unity\UnityEditor\[Company Name]\[Product Name]. The company name and project name can be viewed here

This way you can see the specific data, as shown below

By comparing each key, you can determine whether the keyName you thought about before is correct.

You can also directly modify the data here. For example, if there is a data stored here, the attack power is 10, you can directly change it to 1000, and then the next time you load the game, your attack power will become 1000. This is cheating. (dog head). But the data is generally encrypted, so ordinary stand-alone games cannot be stored in such a stupid way, so give up on this idea.

Guess you like

Origin blog.csdn.net/qq_68117303/article/details/134845617