0

I'm developing a game where I load level data from a JSON file and instantiate objects and terrains (SpriteShape) based on the data. I'm using Unity with C#. My issue is that my loading UI hides immediately without waiting for the level data to finish loading. Here's the loading logic script:

void Start()
    {
        StartCoroutine(LoadLevel());
    }

    private IEnumerator LoadLevel()
    {
        loadingLevelUI.SetActive(true);
        GameObject ground = GameObject.Find("Ground");
        playerSpawn = GameObject.Find("PlayerSpawn");

        // Construct the full file path based on createdLevelsPath and currentLevel
        string createdLevelsPath = Path.Combine(Application.persistentDataPath, "CreatedLevels");
        string filePath = Path.Combine(createdLevelsPath, currentLevel);

        // Check if the file exists
        if (File.Exists(filePath))
        {
            // Read the file content
            string fileContent = File.ReadAllText(filePath, Encoding.UTF8); // Specify UTF-8 encoding for JSON

            try
            {
                // Deserialize the JSON data into a LevelData object
                LevelData levelData = JsonUtility.FromJson<LevelData>(fileContent);

                if (levelData.defaultSpawn != null && playerSpawn)
                {
                    playerSpawn.transform.position = new Vector3(levelData.defaultSpawn.x, levelData.defaultSpawn.y, -5.4f);
                }

                // Access and process the levelContent array
                if (levelData.levelContent != null)
                {
                    foreach (LevelData.LevelObject levelObject in levelData.levelContent)
                    {
                        // Remove "(Clone)" from the object name, if present
                        string objectName = levelObject.objectName.Replace("(Clone)", "").Trim();

                        // Find the prefab based on the object name
                        GameObject prefab = GetPrefab(objectName);

                        // Check if prefab was found
                        if (prefab == null)
                        {
                            Debug.LogWarning("Prefab for object '" + objectName + "' not found.");
                            continue; // Skip to next level object
                        }

                        // Instantiate the prefab
                        GameObject newObject = Instantiate(prefab);
                        Quaternion objectRot = levelObject.rotation;

                        // Set object position based on levelObject position
                        newObject.transform.position = levelObject.position;
                        newObject.transform.rotation = objectRot;
                        newObject.transform.localScale = levelObject.size;

                        // Parent the instantiated object to the ground
                        if (ground != null)
                            newObject.transform.parent = ground.transform;
                        else
                            Debug.LogWarning("Ground object not found.");
                    }

                    foreach (LevelData.LevelTerrain levelTerrain in levelData.levelTerrains)
                    {
                        string objectName = levelTerrain.terrainName.Replace("(Clone)", "").Replace("_p", "").Trim();

                        // Find the prefab based on the object name
                        GameObject prefab = GetPrefab(objectName);

                        // Check if prefab was found
                        if (prefab == null)
                        {
                            Debug.LogWarning("Prefab for terrain '" + objectName + "' not found.");
                            continue; // Skip to the next level terrain
                        }

                        // Instantiate the terrain prefab
                        GameObject newObject = Instantiate(prefab);
                        Quaternion objectRot = levelTerrain.rotation;
                        Vector2 size = levelTerrain.size;

                        // Set object position based on levelTerrain position
                        newObject.transform.position = levelTerrain.position;
                        newObject.transform.rotation = objectRot;
                        newObject.transform.localScale = new Vector3(size.x, size.y, 1f); // Ensure size is correctly set

                        if (ground != null)
                        {
                            newObject.transform.parent = ground.transform;
                        }
                        else
                        {
                            Debug.LogWarning("Ground object not found.");
                        }

                        Debug.Log("Instantiated Terrain: " + objectName + " at position: " + levelTerrain.position);
                        Debug.Log("Terrain size: " + levelTerrain.size);

                        // Clear existing points before loading new ones
                        SpriteShapeController terrainController = newObject.GetComponent<SpriteShapeController>();
                        if (terrainController != null)
                        {
                            terrainController.spline.Clear(); // Clear all existing points
                        }
                        else
                        {
                            Debug.LogError("Terrain prefab '" + objectName + "' does not have a SpriteShapeController component.");
                            continue; // Skip to the next level terrain if component is missing
                        }

                        // Load new points from levelTerrain.points
                        foreach (Vector3 point in levelTerrain.points)
                        {
                            Vector3 localPosition = point; // Use the saved position directly
                            terrainController.spline.InsertPointAt(terrainController.spline.GetPointCount(), localPosition);

                            int pointIndex = levelTerrain.points.IndexOf(point);
                            if (pointIndex >= 0 && pointIndex < levelTerrain.tangentMode.Count) // Check for valid index
                            {
                                ShapeTangentMode tangentMode = levelTerrain.tangentMode[pointIndex];
                                terrainController.spline.SetTangentMode(terrainController.spline.GetPointCount() - 1, tangentMode);

                                // Check if left and right tangent data exists (assuming separate lists)
                                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);
                            }
                        }
                    }
                }
                else
                {
                    Debug.Log("Level has no objects in levelContent.");
                }

                // Set isLevelLoaded to true after loading is complete
                isLevelLoaded = true;
            }
            catch (System.Exception e)
            {
                Debug.LogError("Error loading level data: " + e.Message);
            }
        }
        else
        {
            Debug.LogWarning("Level file not found: " + filePath);
        }

        // Wait until isLevelLoaded is true
        while (!isLevelLoaded)
        {
            yield return null;
        }

        // Once loaded, hide the loading UI
        loadingLevelUI.SetActive(false);
    }

It just immedietly hides the loading level UI which causing big levels to load longer, this means the level would be empty upon start.

2 Answers 2

2

How coroutines work

Coroutine is a function that works in this manner: it starts executing in the next frame and executes synchronously (like any normal function) until it reaches a yield instruction. yield break returns from the coroutine. yield return is used to wait for some other operation. In the simplest case of yield return null it simply waits until the next frame.

What happens in your code

  1. you start a coroutine in the Start method, so LoadLevel begins executing in the next frame.

  2. You load all the level objects in that same single frame. Which results either in:

    a) the level being loaded and the isLevelLoaded flag set.

    b) an error or warning message printed to the console and isLevelLoaded flag not being set.

  3. you then start a while loop, checking for isLevelLoaded flag.

    a) in case isLevelLoaded flag is already set you never enter the loop, so not a single yield return null is called before you hide the UI. this results in the UI being hidden in the same frame, before Unity actually activates everything loaded.

    b) otherwise, when isLevelLoaded is not set, you enter a loop, and your coroutine executes forever without doing any work. which will also result in your UI not being hidden at all.

Solutions

  • Option 1: remove the while loop, only do yield return null once. This should be the easiest solution.

  • Option 2: remove the while loop and do what is suggested in another answer by @maz. This will make every object loaded in a separate frame, which is also good for displaying a progress bar. But might not be what you need.

  • Option 3: still implement Option 2 but instead of using yield return null, use yield return new WaitForSeconds(0.2f);. This is to handle a very specific case when you have a progress bar on the screen, but you never see it 100% filled because it disappears too rapidly. Yes, this increases loading times, and sounds counter-intuitive for us programmers, but users usually like to see their progresses completed.

It is also good to handle the error cases gracefully. The options I can imagine are to either have an error message with an OK button, after pressing which it is convenient to either go back (e.g. to menu) or even to just literally crash. Or a message box with two buttons: same OK button, and the second Retry button, that restarts the coroutine thus allowing the user to try loading the level again. Also note that some error cases you handle can be literally redundant (for example, if the level is always there in the project, no need to handle the case when it is missing etc.)

1

You could solve it by using yield return null; after each loop. This makes sure each object and terrain point is processed in the next frame. So it would keep the loading UI visible until the level is fully loaded.

So pretty much it would look like this:

foreach (LevelData.LevelObject levelObject in levelData.levelContent)
{
    //your current code
    yield return null; // add this at the end of the loop
}

foreach (LevelData.LevelTerrain levelTerrain in levelData.levelTerrains)
{
   //your current code
    yield return null; // same with this
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.