简介
在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