Unity开发抖音小游戏存储的几种方法分析
存储方案介绍
前段时间写小游戏为了省事一直就没有研究抖音小游戏的存储方案,一直使用的是unity中的PlayerPrefs去做的存储,但是后来发现官方会一直提醒让使用抖音sdk中的新文件存储系统,从而降低内存占用。
DataManager
这个是我自行对PlayerPrefs封装的脚本,这个我也开源一下,里面我引入了PlayerID、bool存储、针对泛型的序列化反序列化等。
using StarkSDKSpace.UNBridgeLib.LitJson;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DataManager : Singleton<DataManager>
{
/// <summary>
/// 清楚本地数据
/// </summary>
public void CallDataManager()
{
PlayerPrefs.DeleteAll();
//Debug.LogError("清楚数据成功");
}
#region 人物ID读写
/// <summary>
/// 记录人物ID
/// </summary>
private string m_playerId = "";
/// <summary>
/// 设置人物ID
/// </summary>
/// <param name="id"></param>
public void SetID(string id)
{
m_playerId = id;
}
/// <summary>
/// 获取人物ID
/// </summary>
/// <returns></returns>
public string GetID()
{
return m_playerId;
}
#endregion
#region 泛型存储数据
/// <summary>
/// 存储指定类型
/// </summary>
public void SaveObjectDate<T>(string key,T t,bool common = false)
{
if (common)
{
PlayerPrefs.SetString(key, JsonMapper.ToJson(t));
}
else
{
PlayerPrefs.SetString(m_playerId + key, JsonMapper.ToJson(t));
}
PlayerPrefs.Save();
}
/// <summary>
/// 获取指定类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="common"></param>
/// <returns></returns>
public T GetObjectData<T>(string key,bool common = false) where T:new()
{
string Data = null;
if (common)
{
Data = PlayerPrefs.GetString(key, null);
}
else
{
Data = PlayerPrefs.GetString(m_playerId + key, null);
}
if (string.IsNullOrEmpty(Data))
{
return new T();
}
else
{
return JsonMapper.ToObject<T>(Data);
}
}
#endregion
#region 存取数据
/// <summary>
/// 存储int类型
/// </summary>
public void SaveIntDate(string key, int num, bool common = false)
{
if (common)
{
PlayerPrefs.SetInt(key, num);
}
else
{
PlayerPrefs.SetInt(m_playerId + key, num);
}
PlayerPrefs.Save();
}
/// <summary>
/// 获取int类型
/// </summary>
/// <param name="key"></param>
/// <param name="common"></param>
/// <returns></returns>
public int GetIntData(string key, bool common = false)
{
if (common)
{
return PlayerPrefs.GetInt(key, -1);
}
else
{
return PlayerPrefs.GetInt(m_playerId + key, -1);
}
}
/// <summary>
/// 存储Float类型
/// </summary>
public void SaveFloatDate(string key, float num, bool common = false)
{
if (common)
{
PlayerPrefs.SetFloat(key, num);
}
else
{
PlayerPrefs.SetFloat(m_playerId + key, num);
}
PlayerPrefs.Save();
}
/// <summary>
/// 获取Float类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="common"></param>
/// <returns></returns>
public float GetFloatData(string key, bool common = false)
{
if (common)
{
return PlayerPrefs.GetFloat(key, -1);
}
else
{
return PlayerPrefs.GetFloat(m_playerId + key, -1);
}
}
/// <summary>
/// 存储Bool类型
/// </summary>
public void SaveBoolDate(string key, bool boolean, bool common = false)
{
if (common)
{
PlayerPrefs.SetInt(key, boolean ? 1 : 0);
}
else
{
PlayerPrefs.SetInt(m_playerId + key, boolean ? 1 : 0);
}
PlayerPrefs.Save();
}
/// <summary>
/// 获取Bool类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="common"></param>
/// <returns></returns>
public bool GetBoolData(string key, bool common = false)
{
int? boolean = null;
if (common)
{
boolean = PlayerPrefs.GetInt(key, -1);
}
else
{
boolean = PlayerPrefs.GetInt(m_playerId + key, -1);
}
if (boolean == 1) return true;
else return false;
}
/// <summary>
/// 存储String类型
/// </summary>
public void SaveStrDate(string key, string str, bool common = false)
{
if (common)
{
PlayerPrefs.SetString(key, str);
}
else
{
PlayerPrefs.SetString(m_playerId + key, str);
}
PlayerPrefs.Save();
}
/// <summary>
/// 获取String类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="common"></param>
/// <returns></returns>
public string GetStrData(string key, bool common = false)
{
if (common)
{
return PlayerPrefs.GetString(key, "");
}
else
{
return PlayerPrefs.GetString(m_playerId + key, "");
}
}
#endregion
}
StarkStorage
因为小程序也是适配了Unity的PlayerPrefs,所以StarkSDKSpace.StarkStorage中也封装了对PlayerPrefs的操作,具体用法我就不说了用过PlayerPrefs的基本上看一下API就会用了很简单。
以下是对应的API
using System.Runtime.InteropServices;
using UnityEngine.Scripting;
namespace StarkSDKSpace
{
public static class StarkStorage
{
public static void SetIntSync(string key, int value)
{
StarkStorageSetIntSync(key, value);
}
public static int GetIntSync(string key, int defaultValue)
{
return StarkStorageGetIntSync(key, defaultValue);
}
public static void SetFloatSync(string key, float value)
{
StarkStorageSetFloatSync(key, value);
}
public static float GetFloatSync(string key, float defaultValue)
{
return StarkStorageGetFloatSync(key, defaultValue);
}
public static void SetStringSync(string key, string value)
{
StarkStorageSetStringSync(key, value);
}
public static string GetStringSync(string key, string defaultValue)
{
return StarkStorageGetStringSync(key, defaultValue);
}
public static bool HasKeySync(string key)
{
return StarkStorageHasKeySync(key);
}
public static void DeleteKeySync(string key)
{
StarkStorageDeleteKeySync(key);
}
public static void DeleteAllSync()
{
StarkStorageDeleteAllSync();
}
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void StarkStorageSetIntSync(string key, int value);
#else
static void StarkStorageSetIntSync(string key, int value)
{
UnityEngine.PlayerPrefs.SetInt(key, value);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern int StarkStorageGetIntSync(string key, int defaultValue);
#else
static int StarkStorageGetIntSync(string key, int defaultValue)
{
return UnityEngine.PlayerPrefs.GetInt(key, defaultValue);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void StarkStorageSetStringSync(string key, string value);
#else
static void StarkStorageSetStringSync(string key, string value)
{
UnityEngine.PlayerPrefs.SetString(key, value);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern string StarkStorageGetStringSync(string key, string defaultValue);
#else
static string StarkStorageGetStringSync(string key, string defaultValue)
{
return UnityEngine.PlayerPrefs.GetString(key, defaultValue);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void StarkStorageSetFloatSync(string key, float value);
#else
static void StarkStorageSetFloatSync(string key, float value)
{
UnityEngine.PlayerPrefs.SetFloat(key, value);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern float StarkStorageGetFloatSync(string key, float defaultValue);
#else
static float StarkStorageGetFloatSync(string key, float defaultValue)
{
return UnityEngine.PlayerPrefs.GetFloat(key, defaultValue);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void StarkStorageDeleteAllSync();
#else
static void StarkStorageDeleteAllSync()
{
UnityEngine.PlayerPrefs.DeleteAll();
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void StarkStorageDeleteKeySync(string key);
#else
static void StarkStorageDeleteKeySync(string key)
{
UnityEngine.PlayerPrefs.DeleteKey(key);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern bool StarkStorageHasKeySync(string key);
#else
static bool StarkStorageHasKeySync(string key)
{
return UnityEngine.PlayerPrefs.HasKey(key);
}
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
[Preserve]
[DllImport("__Internal")]
private static extern void StarkPointerStringify();
#endif
}
}
StarkFileSystemManager
这个是我重点要说的也是抖音官方推荐的方法。官方文档说明
跟Unity中的文件系统区别
原有实现中,C#标准的文件接口,如File.ReadAllText、File.WriteAllText、FileStream等,是将数据写入到内存文件系统,然后再在合适的时机自动同步内存数据到IndexedDB中存储。由于采用了IndexedDB文件存储系统,使得运行时内存有一定的增加,如果文件数量过多,可能会发生闪退。另外,IndexedDB文件存储系统兼容性不够好,在部分iOS系统上会无法正常使用,从而导致无法正常进入游戏的情况。
限制
- 对于 iOS WebGL 方案,StarkFileSystemManager接口提供的新文件存储系统只在新版本iOS抖音(版本号>=24.4)上有效,旧版本iOS抖音上仍然则使用默认的IndexedDB存储。
- 对于Android WebGL 方案,新旧版本抖音都支持新的文件存储系统的使用。
- UnityEngine.PlayerPrefs接口保存数据是保存在IndexedDB中的,为了使用新的文件系统存储,抖音sdk也提供了在全局名字空间的PlayerPrefs接口,或者使用StarkSDKSpace.StarkSDK.API.PlayerPrefs接口。这里提供的两个PlayerPrefs接口实现有一定的区别,全局名字空间的PlayerPrefs接口,不会自动迁移IndexedDB中的数据到本地存储,而StarkSDK.API.PlayerPrefs接口会自动迁移IndexedDB中的数据到本地。
- 对于已上线的游戏,游戏存档保存在IndexedDB中,当采用新的文件存储系统后,会将IndexedDB中的数据迁移到本地存储,之后将无法从IndexedDB中读取。
- 当使用了新版本文件存储系统后,之后不允许再使用C#标准的文件接口,不然数据将无法持久化存储,因为不会访问IndexedDB存储系统。(在新的抖音版本中,不会再对IndexedDB做操作,能够大幅降低内容占用。当前属于过渡阶段,长期会禁用File.xxx。
当前在 测试环境 如果发生了File的读写(file.xxx 或者 UnityEngine.PlayerPrefs)会弹出 “请替换新文件存储接口“的提示。线上环境 不会有提示。)
原有数据迁移
- 应尽可能早的调用StarkSDKSpace.StarkFileSystemManager.MigratingData方法,该方法会尝试迁移IndexedDB中的数据到本地,并标记迁移完成。如果不主动调用,那么在后续使用新文件存储系统接口时,会在对象实例化时自动调用一次,这样能保证正确读取到旧的数据。当数据迁移完成后,之后将无法使用C#标准文件存储接口(File.xxx等)访问原有数据,需要调用StarkFileSystemManager接口来访问。
- 需将项目中的File.xxx 和FileStream 改为 StarkSDK.API.GetStarkFileSystemManager()。UnityEngine.PlayerPrefs替换为 StarkSDK.API.PlayerPrefs,或者使用全局名字空间的PlayerPrefs(但如果有旧的存储,要确保主动调用了数据迁移接口)。
- 当项目中主动调用了StarkFileSystemManager接口,那么我们认为项目已经准备好了使用新文件系统,之后不会再去访问老的IndexedDB系统。
- StarkSDK所提供的原有的 StarkSDK.API.Save 和 StarkSDK.API.Load 接口,在数据迁移完成前,仍然使用的IndexedDB存储,当迁移完成后,内部实现会自动切换为新的文件存储系统,开发者无需修改。
使用方法
下面是我整理的一些常规常见的方法
具体的其他详细的方法可以查看官方文档
using StarkSDKSpace;
using UnityEngine;
public class DYDataManager
{
private static DYDataManager instance;
private DYDataManager() { }
public static DYDataManager Instance
{
get
{
if (instance == null)
instance = new DYDataManager();
return instance;
}
}
/// <summary>
/// 抖音文件存储系统
/// </summary>
public StarkFileSystemManager dyFileSystem { get; private set; }
/// <summary>
/// 抖音文件地址
/// </summary>
public string dyFilePath { get; private set; }
// Start is called before the first frame update
public void Init()
{
dyFileSystem = StarkSDK.API.GetStarkFileSystemManager();
dyFilePath = StarkFileSystemManager.USER_DATA_PATH;
}
/// <summary>
/// 检测是否有文件夹和文件夹
/// </summary>
/// <param name="dicPath"></param>
/// <returns></returns>
public bool IsHasDic(string dicPath)
{
return dyFileSystem.AccessSync(dicPath);
}
/// <summary>
/// 同步创建文件
/// </summary>
/// <param name="dicPath"></param>
public bool CreateDic(string dicPath)
{
//判断文件是否存在
if (!dyFileSystem.AccessSync(dicPath))
{
string result = dyFileSystem.MkdirSync(dicPath, false);
Debug.LogError("创建目录成功状态:" + result + "");
return true;
}
else
{
Debug.LogError("已经存在" + dicPath + "目录");
return false;
}
}
/// <summary>
/// 异步创建文件
/// </summary>
/// <param name="dicPath"></param>
public void CreateDicAsync(string dicPath)
{
if (!dyFileSystem.AccessSync(dicPath))
{
MkdirParam param = new MkdirParam();
param.dirPath = dicPath;
param.recursive = false;
param.fail = delegate (StarkBaseResponse response)
{
Debug.Log($"异步创建目录失败,失败原因: errCode:{response.errCode} , errMsg:{response.errMsg}");
};
param.success = response =>
{
Debug.Log($"异步创建目录成功,执行成功回调。");
};
dyFileSystem.Mkdir(param);
}
else
{
Debug.Log($"已经存在此目录:{dicPath}, 无需再次创建");
}
}
/// <summary>
/// 存储数据
/// --> 不存在,创建并写入
/// --> 已存在,读取并写入
/// PS:地址要以上面获取的 dyFilePath 作为根目录,后面则可自行创建
/// </summary>
/// <param name="filePath">文件存储地址</param>
/// <param name="fileContext">文件存储内容</param>
public void CreateFile(string filePath, string fileContext)
{
if (!dyFileSystem.AccessSync(filePath))
{
string isSucc = dyFileSystem.WriteFileSync(filePath, fileContext, "utf8");
Debug.LogError($"创建文件成功状态:{isSucc} 为空,则表示创建成功");
}
else
{
// 读取并写入 --> 注意编码格式与创建一致
string readContext = dyFileSystem.ReadFileSync(filePath, "utf8");
Debug.LogError($"读取文件内容:{readContext}");
//string isSucc = dyFileSystem.WriteFileSync(filePath, readContext + fileContext);
string isSucc = dyFileSystem.WriteFileSync(filePath, fileContext);
Debug.LogError($"读取并写入:{filePath}, 无需再次创建");
}
}
public string ReadFile(string filePath)
{
if (!dyFileSystem.AccessSync(filePath))
{
return "";
}
return dyFileSystem.ReadFileSync(filePath, "utf8");
}
/// <summary>
/// 删除文件/目录
/// </summary>
/// <param name="delPath"></param>
public void DeleteFileOrDic(string delPath)
{
if (dyFileSystem.AccessSync(delPath))
{
// 第二个参数表示:是否递归删除目录。如果为 true,则删除该目录和该目录下的所有子目录以及文件
string isSucc = dyFileSystem.RmdirSync(delPath, false);
Debug.LogError($"删除文件/目录成功状态:{isSucc} 为空,则表示删除成功");
}
else
{
Debug.LogError($"不存在此文件/目录:{delPath}, 无需删除");
}
}
}
总结
感谢大家的支持