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

Unity中如何让加载UI等待关卡数据加载完成后再隐藏?

问题分析

加载UI提前隐藏、大关卡开局为空的核心原因有两个:

  1. 同步IO阻塞主线程:File.ReadAllText是同步文件读取操作,大体积JSON文件会直接卡住主线程,导致加载UI来不及渲染就进入后续逻辑,最终加载完成后直接隐藏UI,玩家看到空场景。
  2. 协程未正确让出帧:整个关卡加载(反序列化、实例化对象、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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 08:55:54