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

这篇具有很好参考价值的文章主要介绍了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一下。

你可以看到如下效果

playerprefs 和 static 静态数据存储哪个方式好,unity,游戏,游戏引擎

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

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

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

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

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

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

大家可以好好理解下。

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

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

playerprefs 和 static 静态数据存储哪个方式好,unity,游戏,游戏引擎

点击确认,然后查看这个目录HKCU\Software\Unity\UnityEditor\[公司名称]\[产品名称]中,公司名称和项目名称可以在这里查看playerprefs 和 static 静态数据存储哪个方式好,unity,游戏,游戏引擎

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

playerprefs 和 static 静态数据存储哪个方式好,unity,游戏,游戏引擎

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

你也可以在这里直接修改数据,比如这里存储的有一个数据时攻击力是10,你可以直接改成1000,然后你下次读取游戏你的攻击力就变成1000了,这就是开挂(狗头)。但是数据一般都会加密,所以一般的单机游戏不可能这么傻乎乎的存储的,死了这条心吧。文章来源地址https://www.toymoban.com/news/detail-765367.html

到了这里,关于Unity使用反射机制和PlayerPrefs来存储游戏数据的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity数据持久化之PlayerPrefs

    什么是数据持久化 数据持久化就是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。即将游戏数据存储到硬盘,硬盘中数据读取到游戏中,也就是传统意义上的存盘。 PlayerPrefs是什么 是 Unity 提供的可以用于存储读取玩家数据的公共类

    2024年02月19日
    浏览(47)
  • 【unity之数据持久化】-Unity公共类PlayerPrefs

    👨‍💻个人主页 :@元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 收录于专栏 : unity数据存储 API大全图解 windows平台存储路径 HKCUSoftware[公司名称][产品名称] 项下的注册表中 公司和产品名称是 在“Project Settings”中设

    2024年02月04日
    浏览(54)
  • Unity PlayerPrefs 持久化数据存在哪

    在游戏开发的过程中,我们经常需要存档相关的东西,称为数据的持久化。PlayerPrefs 就是Unity提供的用于本地数据持久化保存与读取的类。 PlayerPrefs会以键值对的方式存储在本地的注册表中。 1.存储数据 2.获取数据 3.删除数据 这些数据会存储在注册表中,打开注册表就能查看

    2024年02月16日
    浏览(46)
  • unity转微信小游戏数据存储

    unity程序转成微信小游戏以后 File.WriteAllText 和 File.ReadAllText由于路径问题不生效。 这个时候需要采用微信自己的存储路径来把用户信息保存下来。 private void LoadPlayerData() { #if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS if (File.Exists(filePath)) { playerDataString = File.ReadAllText(filePath); m_PlayerData

    2024年02月10日
    浏览(47)
  • 【图文详解】Unity存储游戏数据的几种方法

    在Unity中,常用的数据存储方法包括PlayerPrefs、ScriptableObject、JSON、XML和数据库等。 PlayerPrefs是Unity自带的一种简单的键值存储系统,适合存储一些简单的游戏数据。ScriptableObject是一种Unity类,可用于创建可序列化的对象并存储数据。JSON和XML是轻量级的数据交换格式,可以通过

    2023年04月08日
    浏览(51)
  • Unity3D学习之数据持久化——PlayerPrefs

    就是保存存档和读取存档。 分为两部分,存储和读取,先看存储在看读取 PlayerPrefs 是unity提供可以存储和读取玩家数据的公共类 上面定义过 PlayerPrefs.SetInt(“myAge”,18) 后面再定义PlayerPrefs.SetFloat(“myAge”,20.2f) 后面进行读取int型 myAge时,会变成默认值0 打印结果 0 和 100 1)父

    2024年01月18日
    浏览(80)
  • Unity 之 转微信小游戏本地数据存储方法分享

    近期在将Unity转换为小游戏的时候发现在读写本地文件的时候,使用 Application.persistentDataPath 缓存路径来保存文件失败,原因是WebGL的平台限制。所以导致了原有读写本地文件的代码需要根据平台进行修改。 一种最简单的方式就是将原来存储到文件中的内容,在WebGL平台使用

    2024年02月05日
    浏览(64)
  • Unity 之 抖音小游戏本地数据最新存储方法分享

    抖音小游戏是一种基于抖音平台开发的小型游戏,与传统的 APP 不同,抖音小游戏运行在抖音客户端内部,可以通过抖音的分享、推荐等功能进行传播。在抖音小游戏开发过程中,文件存储系统是一个非常重要的组成部分,本文将详细介绍抖音小游戏文件存储系统的实现原理

    2024年02月11日
    浏览(55)
  • unity链接MySQL数据库,并实现游戏数据的存储和读取。(一)

    先说明一下,本次测试中MySQL是安装在本机上的,如果你要想实现在任何地方访问你的数据库建议使用阿里云的RDS云数据库,你需要注册一个阿里云的账号,然后购买实例等等,这些购买实例、创建白名单、创建数据库、创建账号等在阿里云的帮助文档里都写的很详细了,翻

    2024年02月07日
    浏览(48)
  • unity存储信息的方式,保存读取背包、游戏进度,连接MySQL数据库

    为了在Unity中存储信息,有几种方法可供选择。最常见的方法是 PlayerPrefs、Serialization和Database 。 PlayerPrefs是一种简单的存储小量数据(如玩家偏好或游戏设置)的方法。它易于使用,不需要任何外部库。但是,不建议用于存储大量数据或敏感信息。 Serialization是另一种在Unit

    2024年02月09日
    浏览(63)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包