Unity 之 转微信小游戏本地数据存储方法分享

问题背景

近期在将Unity转换为小游戏的时候发现在读写本地文件的时候,使用Application.persistentDataPath缓存路径来保存文件失败,原因是WebGL的平台限制。所以导致了原有读写本地文件的代码需要根据平台进行修改。

一种最简单的方式就是将原来存储到文件中的内容,在WebGL平台使用PlayerPrefs来存储。比如这样的写法:

使用PlayerPrefs的存在第一次读取慢的问题,可以使用微信小游戏的插件进行添加用到的Key。使用方式:微信小游戏 -> PlayerPrefs优化:

微信插件中覆盖Unity中的API
在这里插入图片描述

所以当需要存储文件很多或者数据很多的时候,我们还是希望在尽量不修改原有的读写逻辑的情况下进行完成文件的读写。所以微信给我们提供了WX.env.USER_DATA_PATH


微信小游戏读写本地文件

Unity转成微信小游戏以后File.WriteAllTextFile.ReadAllText由于路径问题不生效。

改为使用微信小游戏插件提供的路径即可,插件中的代码:

示例代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using WeChatWASM;

public class SaveFile : MonoBehaviour
{
    // 文件类型
    private PlayerData m_PlayerData;

    // 文件名称
    private string fileName = "/PlayerData.txt";

    private void OnGUI()
    {
        if (GUI.Button(new Rect(100, 100, 200, 200), "读取文件"))
        {
            LoadPlayerData();
        }

        if (GUI.Button(new Rect(100, 400, 200, 200), "写入文件"))
        {
            WritePlayerData();
        }
    }

    /// <summary>
    /// 读取文件
    /// </summary>
    private void LoadPlayerData()
    {
#if UNITY_WEBGL
        WXFileSystemManager fs = WX.GetFileSystemManager();
        
        // 判断是否存在目录
        if (fs.AccessSync(WX.env.USER_DATA_PATH + fileName).Equals("ok"))
        {
            // 读取内容
            string playerDataString = fs.ReadFileSync(WX.env.USER_DATA_PATH + fileName, "utf-8");
            if (playerDataString != "")
            {
                m_PlayerData = LitJson.JsonMapper.ToObject<PlayerData>(playerDataString);
            }
        }

        Debug.Log($" 读取文件 姓名:{m_PlayerData.name} 年龄:{m_PlayerData.age}");
#endif
    }

    /// <summary>
    /// 写入文件
    /// </summary>
    private void WritePlayerData()
    {
        m_PlayerData = new PlayerData();
        m_PlayerData.name = "Czhenya";
        m_PlayerData.age = 18;
        Debug.Log($" 写入文件 姓名:{m_PlayerData.name} 年龄:{m_PlayerData.age}");

        string playerData = LitJson.JsonMapper.ToJson(m_PlayerData);
#if UNITY_WEBGL
        WXFileSystemManager fs = WX.GetFileSystemManager();
        fs.WriteFileSync(WX.env.USER_DATA_PATH + fileName, playerData, "utf-8");
#endif
    }
}

public class PlayerData
{
    public string name;
    public int age;
}

测试结果:
不校验是否存在本地目录就进行文件读取的报错:
可以看到后面的读取和保存成功了:


PS:若只需要进行读取文件,在StreamingAssets文件夹下面的文件,在WebGL平台是可以通过UnityWebRequest来进行读取的。


WebGL平台的一些限制

由于平台限制,有些功能在 WebGL 上是不支持的:

  • 不支持多线程,因为 JavaScript 不支持多线程,所以 System.Threading 命名空间下的类不要使用;
  • 不能直接使用 Socket,包括 System.Net下的任何类型,以及 System.Net.Sockets 下的部分类型,以及 UnityEngine.Network,如果需要在 WebGL 平台使用网络功能,可以使用 WWW或者 UnityWebRequest这些都是基于 Http协议的实现,如要需要高实时性,可以选择 WebSockets或者 WebRTC;
  • WebGL 1.0是基于 OpenGL ES 2.0,WebGL 2.0基于 OpenGL ES 3.0,所以存在相应的限制;
  • WebGL 是 AOT(ahead of time,即静态编译平台,因此不能使用 System.Reflection.Emit 下的类型进行代码生成,IL2CPP和 iOS 也是如此。

在这里插入图片描述


一个创建目录的bug

经过测试发现,目前版本的SDK,不能递归创建目录的上级目录后再创建该目录。官方提供的
wxFileSystem.MkdirSync( WX.env.USER_DATA_PATH + @"/Test/Test1", true); 接口参数为:true时不好用,一层目录都创建不出来。只能将参数调整为false,一级一级创建目录。

创建多层目录返回结果,和使用时不存在目录的报错日志:

GetFileSystemManager 33333 res:mkdirSync:fail permission denied, open http:/usr/Storage/P

fs.js:130 Error: writeFileSync:fail permission denied, open http:/usr/Storage/P/tmp_Data1
at o (VM19 WAGame.js:1)

在这里插入图片描述
在微信开发工具中可以看到工程使用的本地路径:
在这里插入图片描述

Demo测试代码分享,里面实现了:实现了校验目录、创建目录、删除目录、校验文件、新建文件、读取文件和复制文件等功能。

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using WeChatWASM;

public class WXFileSave : MonoBehaviour
{
    private WXFileSystemManager wxFileSystem;

    private string rootPath;
    
    private string filePath;
    
    private string fileName;
    
    void Start()
    {
        wxFileSystem = new WXFileSystemManager();
        rootPath = WX.env.USER_DATA_PATH;
        filePath = "/Test/Data";
        fileName = "/text";
    }

    private void OnGUI()
    {
        if (GUI.Button(new Rect(10 ,10, 60, 60), "同步创建目录"))
        {
            CreateDirectoryMkdir(Path.Combine(rootPath, filePath));
        }

        if (GUI.Button(new Rect(10, 100, 60, 60), "创建目录"))
        {
            CreateDirectory(Path.Combine(rootPath, filePath));
        }
        
        if (GUI.Button(new Rect(10, 200, 60, 60), "校验目录"))
        {
            CheckDirectoryExists(Path.Combine(rootPath, filePath));
        }

        if (GUI.Button(new Rect(10, 300, 60, 60), "删除目录"))
        {
            DeleteDirectory(Path.Combine(rootPath, filePath));
        }       
       
        if (GUI.Button(new Rect(100 ,10, 60, 60), "写入文件"))
        {
            CreateFile(rootPath+ filePath + fileName, "测试写入内容");
        }
        
        if (GUI.Button(new Rect(200 ,10, 60, 60), "文件带后缀"))
        {
            CreateFile(rootPath+ filePath + fileName + ".txt", "测试写入内容");
        }

        if (GUI.Button(new Rect(100, 100, 60, 60), "读取文件"))
        {
            ReadFile(Path.Combine(rootPath, filePath + fileName));
        }
        
        if (GUI.Button(new Rect(100, 200, 60, 60), "校验文件"))
        {
            CheckFileExists(Path.Combine(rootPath, filePath));
        }

        if (GUI.Button(new Rect(100, 300, 60, 60), "复制文件"))
        {
           // CopyFile();
        }
    }

    // 校验目录是否存在
    public bool CheckDirectoryExists(string path)
    {
        Debug.Log($"校验路径:{path},是否存在{wxFileSystem.AccessSync(path).Equals("access:ok")}");
        return path != null && wxFileSystem.AccessSync(path).Equals("access:ok");
    }
    
    // 同步创建目录
    public void CreateDirectoryMkdir(string path)
    {
        // *** 参数为:true 不好用,不能递归创建目录的上级目录后再创建该目录。*** 
        // *** 只能一级一级创建目录 *** 
        string res = wxFileSystem.MkdirSync( WX.env.USER_DATA_PATH + @"/Test", false);
        Debug.Log($"同步创建目录{path}:创建结果:{res}");
    }

    /// <summary>
    /// 创建目录
    /// </summary>
    /// <param name="path"></param>
    public void CreateDirectory(string path)
    {
        Debug.Log($"同步创建目录:{path}");
        MkdirParam mkdirParam = new MkdirParam();
        //  *** 只能一级一级创建目录 *** 
        mkdirParam.dirPath = WX.env.USER_DATA_PATH + @"/Test1"; // path
        mkdirParam.recursive = true;
        wxFileSystem.Mkdir(mkdirParam);
        Debug.Log($"CreateDirectory 创建目录{path}");
    }

    // 删除目录
    public void DeleteDirectory(string path)
    {
        string res = wxFileSystem.UnlinkSync(path);
        Debug.Log($"DeleteDirectory 删除目录结果:{res}");
    }

    // 校验文件是否存在
    public bool CheckFileExists(string path)
    {
        Debug.Log($"校验文件是否存在:{path},是否存在{wxFileSystem.AccessSync(path).Equals("ok")}");
        return path != null && wxFileSystem.AccessSync(path).Equals("ok");
    }

    // 新建文件并写入内容
    public void CreateFile(string path, string content)
    {
        Debug.Log($"新建文件 路径:{path},写入内容:{content}");
        wxFileSystem.WriteFileSync(path, content);
    }

    // 读取文件内容
    public void ReadFile(string path)
    {
        // 读取文件内容
        wxFileSystem.ReadFile(new ReadFileParam()
        {
            filePath = path,
            encoding = "utf-8",
            success = (res) =>
            {
                Debug.Log("读取数据成功5555 **** :" + res.stringData);
            },
            fail = (res) =>
            {
                Debug.LogError("读取数据 报错5555 ---- :" + res.errMsg);
            }
        });
    }

    // 复制文件
    public void CopyFile(string sourcePath, string destPath)
    {
        UnityEngine.Debug.Log("复制文件:***** " + sourcePath);
        wxFileSystem.CopyFileSync(sourcePath, destPath);
        UnityEngine.Debug.Log("复制文件:/ " + sourcePath);
    }
}

报错查看方法分享

转小游戏后可以通过打开调试模式在手机上看到报错日志,报错日志如下:

exception thrown : Runtiseerror : null function orfunction signature ismatch . Rurtimcerror : nullfunction or function signature mismatch

Mini Progran Error null function or function signature mismatchRuntimeerror: null function or function signaturemismatc

这两段报错是同一个问题触发的。第一次看到这种报错一脸茫然不知从何下手,再往下看两行就发现了我自己写的函数:ScreenRatiosFilter类中的Fit方法。都定位到报错方法了,之后的问题就不用我再说了吧。

既然你都看到这里了,还是告诉你一下秘诀吧:实在判断不出来是哪里报的错,而在编辑器又复现不出来,补日志,然后复现一下错误,这样就可以定位到具体报错行数了。

还遇到过一个奇怪的问题,一并记录一下。希望对你有所启发:一段游戏逻辑只在小游戏中报错,编辑器和其他端都没问题。后来加了一个try...catch捕获了一次,没做其他任何处理,然后竟然好用了…