Unity开发抖音小游戏存储的几种方法分析

发布于:2024-12-08 ⋅ 阅读:(159) ⋅ 点赞:(0)

存储方案介绍

前段时间写小游戏为了省事一直就没有研究抖音小游戏的存储方案,一直使用的是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)会弹出 “请替换新文件存储接口“的提示。线上环境 不会有提示。)

原有数据迁移

  1. 应尽可能早的调用StarkSDKSpace.StarkFileSystemManager.MigratingData方法,该方法会尝试迁移IndexedDB中的数据到本地,并标记迁移完成。如果不主动调用,那么在后续使用新文件存储系统接口时,会在对象实例化时自动调用一次,这样能保证正确读取到旧的数据。当数据迁移完成后,之后将无法使用C#标准文件存储接口(File.xxx等)访问原有数据,需要调用StarkFileSystemManager接口来访问。
  2. 需将项目中的File.xxx 和FileStream 改为 StarkSDK.API.GetStarkFileSystemManager()。UnityEngine.PlayerPrefs替换为 StarkSDK.API.PlayerPrefs,或者使用全局名字空间的PlayerPrefs(但如果有旧的存储,要确保主动调用了数据迁移接口)。
  3. 当项目中主动调用了StarkFileSystemManager接口,那么我们认为项目已经准备好了使用新文件系统,之后不会再去访问老的IndexedDB系统。
  4. 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}, 无需删除");
        }
    }

}

总结

感谢大家的支持


网站公告

今日签到

点亮在社区的每一天
去签到