Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Is true async scene loading possible?

Discussion in 'Scripting' started by Flamacore, Mar 29, 2019.

  1. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Hey there everyone! :)

    So my question is this; Can we load a scene which has a gigantic amount of Start functions, Awake functions, does huge asset loadings and needs to load very big files (let's suppose it just has to) from another scene and have a true smooth loading screen without any kind of freeze?

    Many examples can be given like The Witcher 3, Dragon Age Inquisition, Mass Effect but especially the one in Desitny 2 is amazing and we would like to achieve something like that.

    For those who'll comment with LoadSceneAsync, please know that it was the first thing we tried of course but even that, causes a freeze in animations that play in the current scene on heavy loads for the loading scene.

    This would probably go something in between Thread handling and possibly some low-level scene management code. I already have done compression and decompression on threads but have no clue on handling actual unity scenes. I'm open to trying anything; a good lead or API reference would be ideal. Or if there's a solution in the asset store, that would be appreciated as well.

    For those who are concerned, this is the current code for our scene that goes like this:
    -a 738ms~ freeze at 1%
    -a sudden flash to around 20%
    -another 440ms~ freeze at 20%
    -another flash to 90%
    -a minor freeze
    and loads the scene.

    Code (CSharp):
    1. public Image loaderFill;
    2.     IEnumerator Start ()
    3.     {
    4.         yield return new WaitForSeconds (0.1f);
    5.         Application.backgroundLoadingPriority = ThreadPriority.High; //also tried low
    6.         AsyncOperation lvl = SceneManager.LoadSceneAsync (1);
    7.         lvl.allowSceneActivation = false;
    8.         while (!lvl.isDone)
    9.         {
    10.             Debug.Log("Loading: " + lvl.progress);
    11.             loaderFill.fillAmount = Mathf.Lerp(loaderFill.fillAmount,lvl.progress,Time.deltaTime/5);
    12.             if (lvl.progress >= 0.9f)
    13.                 break;
    14.             yield return null;
    15.         }
    16.         loaderFill.fillAmount = 0.99f;
    17.         lvl.allowSceneActivation = true;
    18.     }
     
    Last edited: Mar 29, 2019
  2. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    You will need to put your loading into another thread than the mainthread to stop unity from freezing for some ms, another guy just asked some similiar question about loadingscreen that freezes on image loading etc. You also want to make sure, that you have some smart logic in your loading instead of throw all the Start functions on the scene start at once.
     
    Flamacore likes this.
  3. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Thanks a lot for the answer. I'm thinking the same but do you have any idea on how to control the scene loading in another thread since Unity-based code is not thread safe? :)

    Also just out of curiosity, wouldn't putting it on another thread negate the needed logic for start? Since it would be trying to load all of it in another thread, the animation shouldn't stop, should it? Sure the loader progress would probably freeze at the same points but at least it wouldn't stop the loading animations at the current scene I guess...
     
  4. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    Someone called an asset in the asset store which at least helps loading images on another thread. For my last projects, image loading was most of the time the biggest part of "freezing", so you could still have your start there but let the loading happen on another thread. I guess this is some trial and error there ;) Did not use it yet, btu sounds promising:
    https://assetstore.unity.com/packages/tools/integration/task-parallel-82257
     
  5. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Thanks again for the suggestion. I've tried some of these thread workers like ThreadNinja and AdvancedCoroutines but none can do the job since Unity-based code can't be done on other threads than main thread. But will give this a go anyways just to try again :) I'll post the results.
     
  6. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Tried it and the result is as I expected. set_AllowSceneActivation can only be called from main thread error. Similiar other errors have emerged also. Tough situation :/
     
  7. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    Not sure what you are trying but As the reviews said its working for them, maybe you have to implement it in another way? So how did you try to implement it?
     
  8. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Well actually that's a hard-wall for unity. Exclusive UnityEngine codes can only be executed from the main thread. That's exactly where my problem lies. This cannot be done by using Unity's own loading system as I see it. So simple Threading solution is not something that I'm looking for.

    I'm right now trying to put the entire code inside a thread but it throws tons of errors indicating the very same thing I tell here. Unity exclusive code (SceneManagement in this case) can only be executed from the main thread. I guess I'm looking for an alternative, Thread-able way for loading a scene inside unity. Not just enabling threading in Unity per se :)
     
  9. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
  10. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    If smoothness is more important than overall speed, you could try adding explicit waits between steps of your load process. For instance, if you need to load 100 assets, you could explicitly wait a small time between each asset, or between each N assets, to ensure other stuff gets a chance to execute on the main thread. (Note that Start() is allowed to be a coroutine.)

    Haven't done any tests, though.
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,520
    This can be handy but you can get subtle bugs if you just convert scripts blindly to do their Start() in a coroutine.

    Other scripts may find your script via .GetComponent or .FindObjectOfType and begin to interoperate with it (especially in code written in the future) assuming if they find it, it is ready for business. This can be a hard bug to track down.

    Another bug is if somehow that object becomes .SetActive(false) halfway through its Start() coroutine, the remainder of the Start() coroutine DOES NOT RUN. That's even trickier to track down because it requires you to do something (such as pausing or disabling something) that meets the .SetActive(false) condition.

    Check it out for yourself:

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class StartInSteps : MonoBehaviour
    6. {
    7.     // put this on a GameObject, press play, then before this Start()
    8.     // coroutine finishes, in the editor disable the GameObject and
    9.     // turn it back on. Note your initialization does not complete.
    10.     IEnumerator Start ()
    11.     {
    12.         Debug.Log( GetType()+ ".Start();");
    13.         yield return new WaitForSeconds( 2.0f);
    14.         Debug.Log( GetType()+ ".Start part 2...");
    15.         yield return new WaitForSeconds( 2.0f);
    16.         Debug.Log( GetType()+ ".Start part 3...");
    17.         yield return new WaitForSeconds( 2.0f);
    18.         Debug.Log( GetType()+ ".Start is finished");
    19.     }
    20. }
    As always, caveat hackor.
     
    PraetorBlue, Guit0x and hopeful like this.
  12. Smaika

    Smaika

    Joined:
    Apr 28, 2014
    Posts:
    14
    Hey, did you find a solution for this?
    I'm having the same problem in the loading animation freezing while the game is loading on another scene running many instantiate methods and loads of other stuff.
     
  13. Inovafertanimus

    Inovafertanimus

    Joined:
    Dec 7, 2017
    Posts:
    2
    I'm having the same issue. As soon as LoadSceneAsync is called , the game freeze for 1-2 secs (at least in editor. The freeze is quicker in a build but still noticable) .

    - Tried activate asyncLoad.allowSceneActivation a few seconds after loading but still freezes.
    - Tried Texture2D.allowThreadedTextureCreation with Vulkan but it still load all the textures in one frame.
    - Tried to deactivate all the gameobjects in the additive scenes escept for one, wich activate them a second later, but still freezes.

    It always load all the scene and assets in the same frame LoadSceneAsync is called, so that frame freeze the game.
     
  14. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    I don't have a solution, but just want to point out you can't do any of these tests in the Editor. The Editor is known to not do any scene loading async, instead it blocks until complete. All async scene loading performance tests need to be done in an actual build.
     
    hopeful likes this.
  15. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    It's pretty much impossible to load a complex scene asynchronously without a hitch in Unity because there are several steps it can only perform on the main thread without any sort of time slicing. There are ways to mitigate the problem, by reducing or spreading the main thread work across multiple frames until it's duration is short enough to be unnoticed, but first you need to deep profile a build (not in the editor, it will never be smooth there) to measure what is taking the most time during the hitch. You cannot improve what you don't measure.

    The famous 2016 article about the game Inside talks about the challenges the developers faced to achieve seamless streaming. To time slice the inevitable Unity main thread hitch, they split their scenes into many smaller scenes with few objects each and stream them one by one.

    However, that still wasn't smooth enough for them and they had to actually modify Unity source code to fix/optimize loading. It's unknown if in these five years any of their improvements were integrated back into the engine. I think not, due to the major pains I have to go through in a project that uses async streaming of a large world.

    Protip: if your game idea requires a large, open, seamless streaming world, and you ever plan to release on consoles, you should strongly consider not using Unity. The time and effort spent fighting against the engine during the entirety of your development could easily be enough to master an engine which does have first class support for open world streaming like UE.
     
    Ng0ns and stonstad like this.
  16. Warthong

    Warthong

    Joined:
    Jan 12, 2014
    Posts:
    3
    I know this is a really old thread but it seems like a lot of people are still having issues with this. I was working on this for quite some time myself. But I finally got it to load asynchronously and not pause my animation.

    Code (CSharp):
    1.     IEnumerator Load() {
    2.         Application.backgroundLoadingPriority = ThreadPriority.BelowNormal;
    3.         AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("MainMenu", LoadSceneMode.Single);
    4.         asyncOperation.allowSceneActivation = false;
    5.         while (asyncOperation.progress < 0.9f) {
    6.             loadPercentageText.text = ((int)(asyncOperation.progress * 100f)).ToString() + "%";
    7.             yield return null;
    8.         }
    9.         asyncOperation.allowSceneActivation = true;
    10.     }
    This will not work in the Editor as Joe-Censored said. I didn't realize that. I did a build for WIndows and tried it and it worked. Also, the ThreadPriority part is pretty important. (Thanks Flamacore)
     
    _geo__ likes this.
  17. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Be careful when setting allowSceneActivation to false. It pauses all ongoing async operations when the operation reaches 90% until you set it back to true.

    This includes other async scene load operations and async asset bundles (and addressables, which are asset bundles), so you can get yourself in a deadlock if you do something like loading two async scenes in parallel with allowSceneActivation false and waiting for both to reach 90%.
     
    Last edited: Jan 18, 2022
    Silvestor and PutridEx like this.
  18. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Thanks for the kind reply to a long dead topic but this is pretty basic knowledge when you try to dabble with things like this. This is the default way I do for all the projects I work on already. I have a few extension methods that allows me to do this in one line even.

    That's not what I was going for tho. This still causes a lot of hiccups/freezes and issues. Go check out any Destiny 2 live stream and see the loading screen. It's a fully fledged 3D scene that is rendered live with other players' ships and everything with animations, particles, movements while loading the level in the background. All this without a single frame freeze.

    EDIT: You can even visit your inventory, edit stuff, go to menus, settings and things like that in the loading!

    Thanks anyways :)
     
    Last edited: Jan 18, 2022
  19. MarkSharpe

    MarkSharpe

    Joined:
    Feb 3, 2021
    Posts:
    27
    I'm pushing a build to android with 23 scenes set to load asynchronously and each scene is tied to a UI button. Before I added the other 22 scenes to my build my FPS was around 100 and for whatever reason (after I baked the lighting again) its running around 40 fps in editor. I haven't deep dived into the coding yet but when I used a SceneManager.LoadScene(sceneIndex) without a coroutine it wouldn't tank the fps between scenes. VR is hard enough to maintain I'm curious if you guys have a solution for me. Much thanks, Here's my sceneloader right now.
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    public class SceneTransitionManager : MonoBehaviour
    {
    public FadeScreen fadeScreen;
    /*public void GoToScene(int sceneIndex)
    {
    StartCoroutine(GoToSceneRoutine(sceneIndex));
    }

    IEnumerator GoToSceneRoutine(int sceneIndex)
    {
    fadeScreen.FadeOut();
    yield return new WaitForSeconds(fadeScreen.fadeDuration);
    // launch the new scene
    SceneManager.LoadScene(sceneIndex);
    } */
    public void GoToSceneAsync(int sceneIndex)
    {
    StartCoroutine(GoToSceneAsyncRoutine(sceneIndex));
    }
    IEnumerator GoToSceneAsyncRoutine(int sceneIndex)
    {
    fadeScreen.FadeOut();
    //yield return new WaitForSeconds(fadeScreen.fadeDuration);
    // launch the new scene
    AsyncOperation operation = SceneManager.LoadSceneAsync(sceneIndex);
    operation.allowSceneActivation = false;
    float timer = 0;
    while(timer <= fadeScreen.fadeDuration && !operation.isDone)
    {
    timer += Time.deltaTime;
    yield return null;
    }
    operation.allowSceneActivation = true;
    }
    }
     
  20. UrbanNuke

    UrbanNuke

    Joined:
    Jun 11, 2019
    Posts:
    21
    Any news? Still relevant topic
     
  21. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Without changing your entire structure, implementing heavy use of the Addressable system, restructuring everything you have and adding very tedious workflows, no there's no solution that I could come up with over the years to this yet. If you're starting from scratch and use the addressables to very extreme cases then you can manage to achieve something very close to the example I keep giving but still, it's never that smooth.
     
    dimmduh1 and kaiyum like this.
  22. ADmr0

    ADmr0

    Joined:
    Jul 28, 2020
    Posts:
    3
    I honestly don't think its even possible with current scene manager, I honestly think it needs an update to be able to use it with the new Job System.
     
  23. Flamacore

    Flamacore

    Joined:
    Dec 17, 2013
    Posts:
    138
    Well since this is necro'ed again, I might as well tell that there's no solution other than Addressables. I've switched to a tool I wrote myself back then and been using it since. Much more tedious, requires very carefully taking care of your assets, properly labeling and naming them and keeping track of your addressable setups properly.

    But hey, it works :)

    Some examples of what I did back then;

    This piece of code is an addressable anchor. Also has the ability to convert anything in your scene into an instant addressable.
    Code (CSharp):
    1. public class AddressableAnchor : MonoBehaviour
    2.     {
    3.         private GameObject _spawn = null;
    4.         public bool SpawnComplete { get; set; }
    5.         public void LoadAndSpawnObject()
    6.         {
    7.             if(_spawn == null)
    8.                 Addressables.LoadAssetAsync<GameObject>(gameObject.name).Completed += OnCompleted;
    9.         }
    10.  
    11.         private void OnCompleted(AsyncOperationHandle<GameObject> obj)
    12.         {
    13.             _spawn = Instantiate(obj.Result, transform);
    14.             _spawn.transform.localPosition = Vector3.zero;
    15.             _spawn.transform.localScale = Vector3.one;
    16.             SpawnComplete = true;
    17.         }
    18.      
    19. #if UNITY_EDITOR
    20.         [ContextMenu("Setup Addressable Anchor")]
    21.         public void Setup()
    22.         {
    23.             Object parentObject = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
    24.             string pathToObject = AssetDatabase.GetAssetPath(parentObject);
    25.             Debug.Log("Path:" + pathToObject);
    26.             AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;
    27.             AddressableAssetGroup g = settings.FindGroup("Default Local Group");
    28.             string guid = AssetDatabase.AssetPathToGUID(pathToObject);
    29.             AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, g);
    30.             gameObject.name = parentObject.name;
    31.             entry.address = gameObject.name;
    32.             settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true);
    33.             AssetDatabase.SaveAssets();
    34.             PrefabUtility.UnpackPrefabInstance(gameObject, PrefabUnpackMode.OutermostRoot, InteractionMode.AutomatedAction);
    35.             RemoveChildren();
    36.         }
    37.      
    38.         [ContextMenu("RemoveChildren")]
    39.         public void RemoveChildren()
    40.         {
    41.             List<Transform> tempList = transform.Cast<Transform>().ToList();
    42.             foreach (Transform o in tempList)
    43.             {
    44.                 if (o.gameObject.name != gameObject.name)
    45.                 {
    46.                     DestroyImmediate(o.gameObject);
    47.                 }
    48.             }
    49.         }
    50. #endif
    51.     }
    This code is the loader for addressables. Loads them in depending on distance to camera position. Pretty useful to load the scene early and load distant objects later (works best for top-down like scenarios)
    Code (CSharp):
    1.  
    2. public class AddressableLoaderUtility : MonoBehaviour
    3.     {
    4.         private Camera _mainCam;
    5.         public static bool isLoadingDone = false;
    6.         private IEnumerator Start()
    7.         {
    8.             _mainCam = Camera.main;
    9.             yield return new WaitForSeconds(0.5f);
    10.             yield return new WaitForEndOfFrame();
    11.             List<AddressableAnchor> anchors = GetComponentsInChildren<AddressableAnchor>().ToList();
    12.             anchors = anchors.OrderBy(x => Vector3.Distance(x.transform.position, _mainCam.transform.position)).ToList();
    13.             for (int index = 0; index < anchors.Count; index++)
    14.             {
    15.                 AddressableAnchor addressableAnchor = anchors[index];
    16.                 addressableAnchor.LoadAndSpawnObject();
    17.                 if (index % 20 == 0)
    18.                     yield return null;
    19.                 if(index % 40 == 0)
    20.                     anchors = anchors.OrderByDescending(x => x.SpawnComplete).ThenBy(x => Vector3.Distance(x.transform.position, _mainCam.transform.position)).ToList();
    21.                 if (index == 20)
    22.                 {
    23.                     isLoadingDone = true;
    24.                 }
    25.             }
    26.         }
    27. #if UNITY_EDITOR
    28.         [ContextMenu("Setup Addressables for Children Prefabs")]
    29.         public void Setup()
    30.         {
    31.             Transform[] allChildren = GetComponentsInChildren<Transform>(false);
    32.             for (var index = 0; index < allChildren.Length; index++)
    33.             {
    34.                 Transform child = allChildren[index];
    35.                 if (child)
    36.                 {
    37.                     Object parentObject = PrefabUtility.GetCorrespondingObjectFromSource(child.gameObject);
    38.  
    39.                     if (parentObject)
    40.                     {
    41.                         if (child.GetComponent<Renderer>()) DestroyImmediate(child.GetComponent<Renderer>());
    42.                         if (child.GetComponent<Collider>()) DestroyImmediate(child.GetComponent<Collider>());
    43.                         if (child.GetComponent<Mesh>()) DestroyImmediate(child.GetComponent<Mesh>());
    44.                         string previousName = child.name;
    45.                         string newName = Regex.Replace(previousName, @"\s\(\d*\)", "");
    46.                         child.name = newName;
    47.                         AddressableAnchor anchor = child.GetComponent<AddressableAnchor>();
    48.                         if (!anchor) anchor = child.gameObject.AddComponent<AddressableAnchor>();
    49.                         anchor.Setup();
    50.                     }
    51.                 }
    52.             }
    53.         }
    54. #endif
    55.     }
    56.  
    What I did was, setup the scene as I want first. Then auto-create a duplicate using a pre-build script, convert every possible object to an addressable anchor in the copy, use the duplicate scene in build and load them all in with the loader.
     
    hopeful likes this.