You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity iOS项目对接Google Cloud Storage报错:JSON凭证反序列化失败

问题描述

为iOS平台的Unity应用测试新功能,需收集用户跳过特定场景的速度数据,计划用Google Cloud Storage(GCS)存储日志.txt文件。脚本在Unity编辑器中运行正常,但iOS设备构建后出现以下错误:

Error uploading file to Google Cloud Storage: Error deserializing JSON credential data.
d__18:MoveNext()
System.Runtime.CompilerServices.AsyncVoidMethodBuilder:Start(TStateMachine&)
ReturnHome:UploadLogFileToGCS(String)
ReturnHome:OnSkipButtonClicked()
UnityEngine.Events.UnityEvent:Invoke()
UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)
UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()
UnityEngine.EventSystems.StandaloneInputModule:Process()

关联的C#脚本如下:

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Video;
using UnityEngine.UI;
using System.IO;
using System;
using Google.Cloud.Storage.V1;
using Google.Apis.Auth.OAuth2;

public class ReturnHome : MonoBehaviour
{
    public Canvas canvas; // 在Inspector面板中赋值该变量
    public GameObject uiButton; // 在Inspector面板中赋值该变量
    public SpriteRenderer playButton; // 在Inspector面板中赋值该变量
    public GameObject skipButtonPrefab; // 在Inspector面板中赋值该变量
    public GameObject SkipButtonVerticalPrefab; // 在Inspector面板中赋值该变量
    public VideoPlayer videoPlayer; // 在Inspector面板中赋值该变量
    private bool videoStarted = false; // 用于检测视频播放器是否已开始播放的标志
    private float delay = 10f; // 延迟时间(秒)
    private bool buttonPressed = false; // 用于检测UI按钮是否被按下的标志
    private float videoStartTime;
    private string serviceAccountKeyPath;

    private void Awake()
    {
        serviceAccountKeyPath = Path.Combine(Application.streamingAssetsPath, "configgoogle.json");
    }

    private void Start()
    {
        // 在场景开始时禁用UI按钮
        Debug.Log("UI button disabled");

        // 若未赋值playButton对象,则查找该对象
        if (playButton == null)
        {
            playButton = GameObject.Find("PlayButton").GetComponent<SpriteRenderer>();
        }

        // 若未赋值videoPlayer对象,则查找该对象
        if (videoPlayer == null)
        {
            videoPlayer = GameObject.Find("VideoPlayer").GetComponent<VideoPlayer>();
        }

        // 场景加载时重置标志
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        // 若未赋值canvas对象,则查找该对象
        if (canvas == null)
        {
            canvas = FindObjectOfType<Canvas>();
        }

        videoStarted = false;
    }

    private void LoadSceneDelayed()
    {
        SceneManager.LoadScene("MainMenu");
    }

    private void Update()
    {
        // 检测视频播放器是否已开始播放,并启动延迟
        if (!videoStarted && videoPlayer != null && videoPlayer.isPlaying)
        {
            videoStarted = true;
            Debug.Log("Video Player started playing");
            videoStartTime = Time.time;
            Invoke("GenerateSkipButton", delay);
        }
    }

    private void GenerateSkipButton()
    {
        GameObject skipButtonPrefabToInstantiate = skipButtonPrefab;
        Vector3 skipButtonPosition = new Vector3(720, 320, 0);

        // 获取当前场景名称
        string sceneName = SceneManager.GetActiveScene().name;

        if (sceneName == "Scene Boxing")
        {
            skipButtonPrefabToInstantiate = SkipButtonVerticalPrefab;
            skipButtonPosition = new Vector3(-450, 0, 0);
        }

        GameObject skipButton = Instantiate(skipButtonPrefabToInstantiate, Vector3.zero, Quaternion.identity);
        skipButton.transform.SetParent(canvas.transform, false);
        skipButton.transform.localPosition = skipButtonPosition;

        // 设置跳过按钮的点击事件处理器
        Button skipButtonComponent = skipButton.GetComponent<Button>();
        skipButtonComponent.onClick.AddListener(OnSkipButtonClicked);
    }

    private void OnSkipButtonClicked()
    {
        // 定义文件路径变量
        string sceneName = SceneManager.GetActiveScene().name;
        string dateTime = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
        string filePath = Application.persistentDataPath + "/log_" + sceneName + "_" + dateTime + ".txt";

        // 将跳过按钮按下事件记录到文件中
        File.AppendAllText(filePath, "Skip button pressed at: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "\n");

        // 将日志文件上传至Google Cloud Storage
        UploadLogFileToGCS(filePath);

        // 加载主菜单场景
        SceneManager.LoadScene("MainMenu");
    }

    private async void UploadLogFileToGCS(string filePath)
    {
        try
        {
            // 加载Google Cloud服务账号密钥文件
            GoogleCredential credential;
            using (var stream = new FileStream(serviceAccountKeyPath, FileMode.Open, FileAccess.Read))
            {
                credential = GoogleCredential.FromStream(stream);
            }

            // 创建存储客户端实例
            var storageClient = StorageClient.Create(credential);

            // 读取文件数据
            byte[] fileData = File.ReadAllBytes(filePath);
            string fileName = Path.GetFileName(filePath);
            string bucketName = "hapbetastorage"; // 替换为你的Google Cloud Storage存储桶名称

            // 将文件上传至Google Cloud Storage
            using (var stream = new MemoryStream(fileData))
            {
                await storageClient.UploadObjectAsync(bucketName, fileName, null, stream);
            }

            Debug.Log("Uploaded file to Google Cloud Storage: " + filePath);
        }
        catch (Exception e)
        {
            Debug.LogError("Error uploading file to Google Cloud Storage: " + e.Message);
        }
    }


    private void OnApplicationQuit()
    {
        // 定义文件路径变量
        string sceneName = SceneManager.GetActiveScene().name;
        string dateTime = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
        string filePath = Application.persistentDataPath + "/log_" + sceneName + "_" + dateTime + ".txt";

        // 将应用退出事件记录到文件中
        File.AppendAllText(filePath, "Application quit at: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "\n");

        // 将日志文件上传至Google Cloud Storage
        UploadLogFileToGCS(filePath);
    }
}
解决方案

此问题并非iOS与GCS方案根本性不兼容,核心是JSON密钥文件的读取逻辑不符合iOS文件系统特性,以下是具体修复步骤:

1. 修正StreamingAssets文件读取方式

iOS平台下Application.streamingAssetsPath是特殊URL格式,无法直接用FileStream读取,改用UnityWebRequest异步读取JSON内容:

using UnityEngine.Networking;
using System.Threading.Tasks;

private async Task<GoogleCredential> LoadGoogleCredentialAsync()
{
    string jsonContent = string.Empty;
    using (UnityWebRequest www = UnityWebRequest.Get(serviceAccountKeyPath))
    {
        await www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Failed to load credential file: " + www.error);
            return null;
        }
        jsonContent = www.downloadHandler.text;
    }

    using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(jsonContent)))
    {
        return GoogleCredential.FromStream(stream);
    }
}

2. 确保密钥文件正确打包

  • 在Unity Inspector中选中configgoogle.json,将Text Encoding设为UTF-8,Asset Type设为TextAsset。
  • 勾选Copy to StreamingAssets,确保文件被包含在iOS构建包中。

3. 调整异步方法逻辑

原代码中async void在iOS IL2CPP环境下可能存在回调异常,改为async Task并正确等待:

private async void OnSkipButtonClicked()
{
    // 原有日志写入逻辑保持不变
    string sceneName = SceneManager.GetActiveScene().name;
    string dateTime = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
    string filePath = Application.persistentDataPath + "/log_" + sceneName + "_" + dateTime + ".txt";
    File.AppendAllText(filePath, "Skip button pressed at: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "\n");

    // 等待上传完成再跳转场景
    await UploadLogFileToGCS(filePath);
    SceneManager.LoadScene("MainMenu");
}

private async Task UploadLogFileToGCS(string filePath)
{
    try
    {
        GoogleCredential credential = await LoadGoogleCredentialAsync();
        if (credential == null) return;

        var storageClient = StorageClient.Create(credential);
        byte[] fileData = File.ReadAllBytes(filePath);
        string fileName = Path.GetFileName(filePath);
        string bucketName = "hapbetastorage";

        using (var stream = new MemoryStream(fileData))
        {
            await storageClient.UploadObjectAsync(bucketName, fileName, null, stream);
        }
        Debug.Log("Uploaded file to Google Cloud Storage: " + filePath);
    }
    catch (Exception e)
    {
        Debug.LogError("Error uploading file to Google Cloud Storage: " + e.Message);
    }
}

4. IL2CPP编译适配

  • 在Unity Player Settings > iOS中,将Scripting Backend设为IL2CPP,勾选Enable Null Reference Exceptions和Enable Full Stacktrace,便于调试。
  • 确保使用的Google Cloud NuGet包(Google.Cloud.Storage.V1、Google.Apis.Auth.OAuth2)为.NET Standard 2.0兼容版本。

5. 备选简化方案

若GCS密钥处理过于繁琐,小规模Beta测试可改用Firebase Storage,它对Unity iOS的集成更友好,无需手动管理服务账号密钥,通过Firebase控制台配置即可快速实现文件上传。

内容的提问来源于stack exchange,提问作者jnl1565

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 21:57:02