Unity杂文——本地存储管理

  1. 简介
  2. 特性
    1. 全局和个人信息存储
    2. Json文件存储
    3. 文件加密
  3. 代码如下
  4. 结语

简介

在Unity开发中,我们经常需要将一些数据存储在本地,以便在游戏或应用程序的不同运行周期中使用。虽然Unity提供了PlayerPrefs来满足这个需求,但是PlayerPrefs有一些限制,例如数据类型的限制,以及存储容量的限制。因此,我开发了一种新的本地存储结构,它使用Json文件来存储数据,既可以存储全局信息,也可以存储每个账户的个人信息。此外,这种结构还支持对Json文件进行加密,以保护用户的数据安全。

特性

全局和个人信息存储

这种结构允许存储全局信息和每个账户的个人信息。这对于需要在设备上保存用户特定数据的游戏或应用程序非常有用。

Json文件存储

所有的数据都存储在一个Json文件中,这使得数据的读取和写入变得非常方便。同时,由于Json是一种轻量级的数据交换格式,所以这种存储方式也非常高效。

文件加密

为了保护用户的数据安全,这种结构还支持对Json文件进行加密。这样,即使有人能够访问到存储文件,也无法读取其中的内容。

代码如下

注: 在使用这种存储结构时,你需要注意的是,你需要将Json的序列化和反序列化替换成你自己的接口。此外,这种结构使用了RijndaelManaged进行文件加密,你可能需要根据你的实际需求来调整加密算法。

public static class DataStorage
{
    private const string kStorageFilename = "storage.json";
    private const string kVersion = "version";
    private static Hashtable s_Root = new Hashtable();
    private static Hashtable s_Current;
    private static bool s_Refresh = false;
    private static bool s_IsInit = false;
#if USE_ENCRYPT
    private static RijndaelManaged m_Managed;
    private static ICryptoTransform m_Encryptor;
    private static ICryptoTransform m_Decryptor;
#endif

    #region 加载、保存

    /// <summary>
    /// 初始化数据
    /// </summary>
    public static void Init()
    {
        var path = GetPath();
#if USE_ENCRYPT
        m_Managed = new RijndaelManaged
        {
            Key = Encoding.UTF8.GetBytes("sd@#^%&*^&?*68(7hK%&fd"),
            IV = Encoding.UTF8.GetBytes("^%&*^&?*682K$d"),
            Mode = CipherMode.CBC,
            Padding = PaddingMode.Zeros
        };
        m_Encryptor = m_Managed.CreateEncryptor();
        m_Decryptor = m_Managed.CreateDecryptor();
#endif
        if (File.Exists(path))
        {
            try
            {
#if USE_ENCRYPT
            var bytes = File.ReadAllBytes(path);
            var data = m_Decryptor.TransformFinalBlock(bytes, 0, bytes.Length);
#else
                var data = File.ReadAllBytes(path);
#endif
                s_Root = JsonUtils.ToHashtable(data);
            }
            catch (Exception e)
            {
                File.Delete(path);
                AddNewData();
            }
        }
        else
        {
            AddNewData();
        }

        s_Current = s_Root;
        s_IsInit = true;
    }

    /// <summary>
    /// 增加新的数据
    /// </summary>
    public static void AddNewData()
    {
        s_Root[kVersion] = "1.0.0";
    }

    /// <summary>
    /// 更新数据
    /// </summary>
    public static void Update()
    {
        if (!s_IsInit) return;
        if (s_Refresh)
        {
            s_Refresh = false;
            Save();
        }
    }

    /// <summary>
    /// 卸载数据
    /// </summary>
    public static void UnInit()
    {
        s_IsInit = false;
        if (s_Refresh)
        {
            s_Refresh = false;
            Save();
        }
    }

    /// <summary>
    /// 保存数据
    /// </summary>
    private static void Save()
    {
        if (!s_IsInit) return;
        
        var path = GetPath();

        PathUtils.MakeDirectory(path);
#if USE_ENCRYPT
        var bytes = JsonUtils.ToBytes(s_Root);
        var data = m_Encryptor.TransformFinalBlock(bytes, 0, bytes.Length);
#else
        var data = JsonUtils.ToBytes(s_Root);
#endif
        File.WriteAllBytes(path, data);
    }

    /// <summary>
    /// 获取文件地址(存储文件存放的地址)
    /// </summary>
    /// <returns></returns>
    private static string GetPath()
    {
        return PathUtils.GetExternalPath(kStorageFilename);
    }

    #endregion

    #region 增删改查
    
    /// <summary>
    /// 设置数据(如果数据是默认值就会被移除掉)
    /// </summary>
    /// <param name="table"></param>
    /// <param name="primaryKey"></param>
    /// <param name="target"></param>
    /// <typeparam name="T"></typeparam>
    private static void Set<T>(Hashtable table, string primaryKey, T target) where T : IEquatable<T>
    {
        if (null == target || target.Equals(default))
        {
            table.Remove(primaryKey);
        }
        else if (table.ContainsKey(primaryKey))
        {
            table[primaryKey] = target;
        }
        else
        {
            table.Add(primaryKey, target);
        }
        s_Refresh = true;
    }

    /// <summary>
    /// 强制设置数据
    /// </summary>
    /// <param name="table"></param>
    /// <param name="primaryKey"></param>
    /// <param name="target"></param>
    /// <typeparam name="T"></typeparam>
    private static void SetForce<T>(Hashtable table, string primaryKey, T target) where T : IEquatable<T>
    {
        if (table.ContainsKey(primaryKey))
        {
            table[primaryKey] = target;
        }
        else
        {
            table.Add(primaryKey, target);
        }
        s_Refresh = true;
    }
    

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="table"></param>
    /// <param name="primaryKey"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    private static T Get<T>(Hashtable table, string primaryKey)
    {
        if (table.ContainsKey(primaryKey))
        {
            return (T)Convert.ChangeType(table[primaryKey], typeof(T));
        }
        if(table == s_Root && PlayerPrefs.HasKey(primaryKey))
        {
            switch (Type.GetTypeCode(typeof(T)))
            {
                case TypeCode.Int32:
                    return (T)Convert.ChangeType(PlayerPrefs.GetInt(primaryKey), typeof(T));
                case TypeCode.Single:
                    return (T)Convert.ChangeType(PlayerPrefs.GetFloat(primaryKey), typeof(T));
                case TypeCode.String:
                    return (T)Convert.ChangeType(PlayerPrefs.GetString(primaryKey), typeof(T));
                case TypeCode.Boolean:
                    return (T)Convert.ChangeType(PlayerPrefs.GetInt(primaryKey) == 1, typeof(T));
                default:
                    Logging.Error($"SetRoot {primaryKey} failed, type {typeof(T)} not support.");
                    break;
            }
        }
        return default;
    }

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T Get<T>(string primaryKey)
    {
        Logging.Assert(s_Current != s_Root, $"get {primaryKey} must after call Enter(..)");
        return Get<T>(s_Current, primaryKey);
    }

    /// <summary>
    /// 设置根目录数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <param name="target"></param>
    /// <param name="isForce"></param>
    /// <typeparam name="T"></typeparam>
    public static void SetRoot<T>(string primaryKey, T target, bool isForce = false) where T : IEquatable<T>
    {
        if (s_IsInit)
        {
            if (isForce)
            {
                SetForce(s_Root, primaryKey, target);
            }
            else
            {
                Set(s_Root, primaryKey, target);
            }
        }
        else
        {
            // 根据T的类型使用PlayerPrefs进行保存
            switch (Type.GetTypeCode(typeof(T)))
            {
                case TypeCode.Int32:
                    PlayerPrefs.SetInt(primaryKey, Convert.ToInt32(target));
                    break;
                case TypeCode.Single:
                    PlayerPrefs.SetFloat(primaryKey, Convert.ToSingle(target));
                    break;
                case TypeCode.String:
                    PlayerPrefs.SetString(primaryKey, Convert.ToString(target));
                    break;
                case TypeCode.Boolean:
                    PlayerPrefs.SetInt(primaryKey, Convert.ToBoolean(target) ? 1 : 0);
                    break;
                default:
                    Logging.Error($"SetRoot {primaryKey} failed, type {typeof(T)} not support.");
                    break;
            }
        }
    }
    
    /// <summary>
    /// 设置当前用户数据数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <param name="target"></param>
    /// <param name="isForce"></param>
    /// <typeparam name="T"></typeparam>
    public static void Set<T>(string primaryKey, T target, bool isForce = false) where T : IEquatable<T>
    {
        Logging.Assert(s_Current != s_Root, $"set {primaryKey} must after call Enter(..)");
        if (isForce)
        {
            SetForce(s_Current, primaryKey, target);
        }
        else
        {
            Set(s_Current, primaryKey, target);
        }
    }

    /// <summary>
    /// 设置根目录数据(含两个关键字的查找)
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <param name="secondaryKey"></param>
    /// <param name="target"></param>
    /// <param name="isForce"></param>
    /// <typeparam name="T"></typeparam>
    public static void SetRoot<T>(string primaryKey, string secondaryKey, T target, bool isForce = false)
        where T : IEquatable<T>
    {
        Hashtable hashtable;
        if (s_Root.ContainsKey(primaryKey))
        {
            hashtable = (Hashtable)s_Root[primaryKey];
        }
        else
        {
            hashtable = new Hashtable();
            s_Root.Add(
                primaryKey,
                hashtable);
        }

        if (isForce)
        {
            SetForce(hashtable, secondaryKey, target);
        }
        else
        {
            Set(hashtable, secondaryKey, target);
        }
    }

    /// <summary>
    /// 获取根目录数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T GetRoot<T>(string primaryKey)
    {
        return Get<T>(s_Root, primaryKey);
    }

    /// <summary>
    /// 获取根目录数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <param name="secondaryKey"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T GetRoot<T>(string primaryKey, string secondaryKey)
    {
        return s_Root.ContainsKey(primaryKey) ? Get<T>((Hashtable)s_Root[primaryKey], secondaryKey) : default;
    }

    /// <summary>
    /// 判断是否包含数据
    /// </summary>
    /// <param name="primaryKey"></param>
    /// <param name="isRoot"></param>
    /// <returns></returns>
    public static bool ContainsKey(string primaryKey, bool isRoot = false)
    {
        return isRoot
            ? s_Root.ContainsKey(primaryKey) || PlayerPrefs.HasKey(primaryKey)
            : s_Current.ContainsKey(primaryKey);
    }

    
    /// <summary>
    /// 进入当前用户数据(用于多角色存储不同用户数据)
    /// </summary>
    /// <param name="primaryKey"></param>
    public static void Enter(string primaryKey)
    {
        Debug.Log("primaryKey = "+ primaryKey);
        if (s_Current.ContainsKey(primaryKey))
        {
            s_Current = (Hashtable)s_Current[primaryKey];
        }
        else
        {
            var hashtable = new Hashtable();
            s_Current.Add(
                primaryKey,
                hashtable);
            s_Current = hashtable;
        }
        s_Refresh = true;
    }

    #endregion
}

结语

这种新的本地存储结构提供了一种更灵活、更安全的方式来存储和管理本地数据。如果你在使用Unity进行开发,并且需要一种更好的本地存储方案,那么你可以尝试使用这种结构。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 841774407@qq.com

×

喜欢就点赞,疼爱就打赏