Unity中如何让加载UI等待关卡数据加载完成后再隐藏?
问题分析
加载UI提前隐藏、大关卡开局为空的核心原因有两个:
- 同步IO阻塞主线程:
File.ReadAllText是同步文件读取操作,大体积JSON文件会直接卡住主线程,导致加载UI来不及渲染就进入后续逻辑,最终加载完成后直接隐藏UI,玩家看到空场景。 - 协程未正确让出帧:整个关卡加载(反序列化、实例化对象、SpriteShape配置)逻辑都在协程的同一帧内同步执行,没有主动让出帧给UI渲染;末尾的
while (!isLevelLoaded)循环完全无效——isLevelLoaded已经在加载逻辑结束后设为true,循环根本不会等待。
解决方案
针对问题做以下优化:
- 改用异步文件读取:用
File.ReadAllTextAsync配合协程异步处理,避免阻塞主线程。 - 分批处理并让出帧:实例化大量对象或处理SpriteShape点数据时,每处理一定数量就
yield return null,让UI有机会更新。 - 移除无效等待逻辑:删掉多余的
while (!isLevelLoaded)循环,直接在加载完成(或失败)后处理UI状态。 - 统一错误处理:确保异常或文件不存在的场景下,能正确隐藏加载UI。
修改后的代码
void Start() { StartCoroutine(LoadLevel()); } private IEnumerator LoadLevel() { loadingLevelUI.SetActive(true); GameObject ground = GameObject.Find("Ground"); playerSpawn = GameObject.Find("PlayerSpawn"); isLevelLoaded = false; string createdLevelsPath = Path.Combine(Application.persistentDataPath, "CreatedLevels"); string filePath = Path.Combine(createdLevelsPath, currentLevel); if (!File.Exists(filePath)) { Debug.LogWarning("Level file not found: " + filePath); loadingLevelUI.SetActive(false); yield break; } // 异步读取文件,避免阻塞主线程 Task<string> readTask = File.ReadAllTextAsync(filePath, Encoding.UTF8); yield return new WaitUntil(() => readTask.IsCompleted); if (readTask.IsFaulted) { Debug.LogError("Failed to read level file: " + readTask.Exception.Message); loadingLevelUI.SetActive(false); yield break; } string fileContent = readTask.Result; LevelData levelData = null; try { levelData = JsonUtility.FromJson<LevelData>(fileContent); } catch (System.Exception e) { Debug.LogError("Error deserializing level data: " + e.Message); loadingLevelUI.SetActive(false); yield break; } if (levelData.defaultSpawn != null && playerSpawn) { playerSpawn.transform.position = new Vector3(levelData.defaultSpawn.x, levelData.defaultSpawn.y, -5.4f); } if (levelData.levelContent != null) { int processedCount = 0; foreach (LevelData.LevelObject levelObject in levelData.levelContent) { string objectName = levelObject.objectName.Replace("(Clone)", "").Trim(); GameObject prefab = GetPrefab(objectName); if (prefab == null) { Debug.LogWarning("Prefab for object '" + objectName + "' not found."); continue; } GameObject newObject = Instantiate(prefab); newObject.transform.position = levelObject.position; newObject.transform.rotation = levelObject.rotation; newObject.transform.localScale = levelObject.size; if (ground != null) newObject.transform.parent = ground.transform; else Debug.LogWarning("Ground object not found."); // 每处理10个对象让出一次帧,避免主线程卡死 processedCount++; if (processedCount % 10 == 0) yield return null; } } else { Debug.Log("Level has no objects in levelContent."); } if (levelData.levelTerrains != null) { foreach (LevelData.LevelTerrain levelTerrain in levelData.levelTerrains) { string objectName = levelTerrain.terrainName.Replace("(Clone)", "").Replace("_p", "").Trim(); GameObject prefab = GetPrefab(objectName); if (prefab == null) { Debug.LogWarning("Prefab for terrain '" + objectName + "' not found."); continue; } GameObject newObject = Instantiate(prefab); newObject.transform.position = levelTerrain.position; newObject.transform.rotation = levelTerrain.rotation; newObject.transform.localScale = new Vector3(levelTerrain.size.x, levelTerrain.size.y, 1f); if (ground != null) newObject.transform.parent = ground.transform; else Debug.LogWarning("Ground object not found."); SpriteShapeController terrainController = newObject.GetComponent<SpriteShapeController>(); if (terrainController == null) { Debug.LogError("Terrain prefab '" + objectName + "' does not have a SpriteShapeController component."); continue; } terrainController.spline.Clear(); int pointCount = 0; foreach (Vector3 point in levelTerrain.points) { terrainController.spline.InsertPointAt(terrainController.spline.GetPointCount(), point); int pointIndex = levelTerrain.points.IndexOf(point); if (pointIndex >= 0 && pointIndex < levelTerrain.tangentMode.Count) { ShapeTangentMode tangentMode = levelTerrain.tangentMode[pointIndex]; terrainController.spline.SetTangentMode(terrainController.spline.GetPointCount() - 1, tangentMode); if (pointIndex >= 0 && pointIndex < levelTerrain.leftTangents.Count && pointIndex < levelTerrain.rightTangents.Count) { Vector3 leftTangent = levelTerrain.leftTangents[pointIndex]; Vector3 rightTangent = levelTerrain.rightTangents[pointIndex]; terrainController.spline.SetLeftTangent(terrainController.spline.GetPointCount() - 1, leftTangent); terrainController.spline.SetRightTangent(terrainController.spline.GetPointCount() - 1, rightTangent); } else { Debug.LogWarning("Missing left/right tangent data for point " + pointIndex + " in terrain " + levelTerrain.terrainName); } } else { Debug.LogError("Missing tangent mode data for point " + pointIndex + " in terrain " + levelTerrain.terrainName); } // 每处理20个点让出一次帧 pointCount++; if (pointCount % 20 == 0) yield return null; } } } isLevelLoaded = true; // 加载完成后隐藏UI loadingLevelUI.SetActive(false); }
额外优化建议
- 对象池复用:如果关卡有大量重复对象,用对象池替代
Instantiate,可大幅降低实例化开销。 - SpriteShape数据预处理:导出JSON时预先优化点数据,减少加载时的计算量。
- 加载进度显示:在协程中计算加载进度(已处理对象数/总对象数),更新UI进度条,提升玩家体验。
内容的提问来源于stack exchange,提问作者Precoded
相关产品推荐
相关产品推荐

