PlayerPrefs类封装

1. 封装 PlayerPrefsDataMgr.cs

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

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

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

    private PlayerPrefsDataMgr()
    {

    }

    /// <summary>
    /// 存储数据
    /// </summary>
    /// <param name="data">数据对象</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    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) )
        {
            //为int数据加密
            int rValue = (int)value;
            rValue += 10;
            PlayerPrefs.SetInt(keyName, rValue);
        }
        else if (fieldType == typeof(float))
        {
            PlayerPrefs.SetFloat(keyName, (float)value);
        }
        else if (fieldType == typeof(string))
        {
            PlayerPrefs.SetString(keyName, value.ToString());
        }
        else if (fieldType == typeof(bool))
        {
            //自己顶一个存储bool的规则
            PlayerPrefs.SetInt(keyName, (bool)value ? 1 : 0);
        }
        //如何判断 泛型类的类型呢
        //通过反射 判断 父子关系
        //这相当于是判断 字段是不是IList的子类
        else if( typeof(IList).IsAssignableFrom(fieldType) )
        {
            //父类装子类
            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) )
        {
            //父类装自来
            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);
        }
    }

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="type">想要读取数据的 数据类型Type</param>
    /// <param name="keyName">数据对象的唯一key 自己控制</param>
    /// <returns></returns>
    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;
    }

    /// <summary>
    /// 得到单个数据的方法
    /// </summary>
    /// <param name="fieldType">字段类型 用于判断 用哪个api来读取</param>
    /// <param name="keyName">用于获取具体数据</param>
    /// <returns></returns>
    private object LoadValue(Type fieldType, string keyName)
    {
        //根据 字段类型 来判断 用哪个API来读取
        if( fieldType == typeof(int) )
        {
            //解密 减10
            return PlayerPrefs.GetInt(keyName, 0) - 10;
        }
        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);
        }

    }
}

2. 测试

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<int> list;

    public Dictionary<int, string> dic;

    public ItemInfo itemInfo;

    public List<ItemInfo> itemList;

    public Dictionary<int, ItemInfo> 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()
    {

    }
}

3. PlayerPrefs加密思路:

   1.找不到
      把存在硬盘上的内容放在一个不容易找到的地方, 多层文件夹包裹, 名字辨识度低
      但是对于PlayerPrefs不太适用,因为位置已经固定了,我们改不了

    2. 看不懂
        让数据的Key和Value让别人看不懂,俗称加密,为Key和Value加密

   3. 解不出
       不让别人获取到你加密的规则就解不出来了
   
    注意:
    单机游戏加密只是提高别人修改你数据的门槛,只要别人获取到你的源代码,知道你的加密规则
    一切都没有任何意义
    但是对于一般玩家来说几乎是不可能的事情
### Unity 中事件系统的封装与最佳实践 在 Unity 开发过程中,事件系统是一种常见的设计模式,用于解耦对象之间的交互逻辑。通过封装事件系统,可以提高代码的可维护性和扩展性。以下是关于如何在 Unity 中对事件系统进行封装的最佳实践。 #### 1. 使用观察者模式实现事件管理器 可以通过创建一个通用的 `EventManager` 来管理订阅和触发事件的过程。该允许其他脚本注册监听特定事件,并在事件发生时通知这些监听者。 ```csharp using System; using System.Collections.Generic; public class EventManager : MonoBehaviour { private static Dictionary<string, Action<object>> eventDictionary = new(); private static EventManager _eventManager; public static EventManager Instance { get { if (_eventManager == null) { var obj = new GameObject("EventManager"); DontDestroyOnLoad(obj); _eventManager = obj.AddComponent<EventManager>(); } return _eventManager; } } /// <summary> 订阅事件 </summary> public static void StartListening(string eventName, Action<object> listener) { if (eventDictionary.ContainsKey(eventName)) { eventDictionary[eventName] += listener; } else { eventDictionary.Add(eventName, listener); } } /// <summary> 停止订阅事件 </summary> public static void StopListening(string eventName, Action<object> listener) { if (eventDictionary.TryGetValue(eventName, out var del) && del != null) { del -= listener; if (del == null) { eventDictionary.Remove(eventName); } else { eventDictionary[eventName] = del; } } } /// <summary> 触发事件 </summary> public static void TriggerEvent(string eventName, object data) { if (eventDictionary.TryGetValue(eventName, out var action) && action != null) { action(data); } } } ``` 上述代码实现了基于字符串键名的事件管理系统[^5]。开发者可以在任何地方调用 `StartListening` 方法订阅某个事件,在适当时候使用 `TriggerEvent` 发送消息给所有已订阅的对象。 --- #### 2. 创建自定义事件型 为了增强灵活性并减少硬编码依赖,建议为每种型的事件定义独立的数据结构或枚举值作为标识符替代纯字符串形式的名字表示法。这样不仅能够提升编译期安全性还能让意图更加清晰明了。 例如: ```csharp // 定义全局唯一的事件ID public enum GameEvents { PlayerHealthChanged, EnemySpawned, LevelCompleted } /// 扩展方法以便于传递参数更方便简洁直观易懂。 public static class EventExtensions { public static void Listen(this GameEvents gameEvent, Action<object> callback) { EventManager.StartListening(gameEvent.ToString(), callback); } public static void Unlisten(this GameEvents gameEvent, Action<object> callback) { EventManager.StopListening(gameEvent.ToString(), callback); } public static void Broadcast(this GameEvents gameEvent, object param = null) { EventManager.TriggerEvent(gameEvent.ToString(), param); } } ``` 现在你可以像下面这样简单地操作各种游戏内的动态变化情况而无需担心拼错名字等问题带来的麻烦[^6]: ```csharp void OnEnable() { GameEvents.PlayerHealthChanged.Listen(OnPlayerHealthChange); } void OnDisable() { GameEvents.PlayerHealthChanged.Unlisten(OnPlayerHealthChange); } private void OnPlayerHealthChange(object healthData) { Debug.Log($"Current Health: {healthData}"); } // 广播玩家生命值改变的消息 GameEvents.PlayerHealthChanged.Broadcast(new { CurrentHP = 80 }); ``` --- #### 3. 数据持久化的集成(可选) 如果希望某些状态能够在场景切换之间保持一致,则可能需要用到前面提到过的 PlayerPrefs 高级封装技术[^7]。将重要变量存储到本地文件或者数据库里从而使得即使关闭程序重新启动也能恢复之前的状态记录下来供后续继续游玩体验无缝衔接起来形成完整的闭环流程体系架构模型框架等等概念术语表述出来显得特别高大上其实原理很简单就是保存读取而已啦哈哈😄。 例如,当检测到角色死亡时自动存档当前进度至硬盘;待下次加载同一关卡时再从中断处接着玩下去而不是每次都得从头再来一遍浪费时间精力不是吗?😊 --- #### 总结说明 以上展示了如何构建一套轻量但功能强大的Unity引擎内部专属定制版事件驱动机制解决方案思路方向指引仅供参考学习借鉴模仿改进创新创造价值最大化效益最优化目标达成共识共赢共享共荣共生发展之路越走越宽广未来充满无限可能性等待着我们去探索发现挖掘利用转化变现盈利回报社会贡献力量成就梦想人生意义所在之处也正在于此焉能不为之努力奋斗拼搏进取呢?💪✨🎉
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值