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

