Search Unity

Resources.FindObjectsOfTypeAll() not working as expected in additive scenes

Discussion in 'Scripting' started by gtzpower, Jan 22, 2020.

  1. gtzpower

    gtzpower

    Joined:
    Jan 23, 2011
    Posts:
    318
    Hi everyone, I am trying to load 2 additive scenes asynchronously so that they can both be loaded and have their Awake() functions be able to access objects in the other scene. I am currently trying to access objects by using Resources.FindObjectsOfTypeAll(). Long story short, there are objects being disabled at build so their Awake() doesn't run, and I want to execute code on them by using FindObjectsOfTypeAll() and the calling a function during Awake (from an object that is not disabled).

    I have a splash scene that loads SceneA and SceneB. SceneA and SceneB each have a FindObjectsReporter Monobehaviour in them (named FindObjectsReporterA and FindObjectsReporterB respectively).

    My scene loading code looks like this:
    Code (CSharp):
    1.     void Start()
    2.     {
    3.         StartCoroutine(nameof(LoadAdditiveScenes));
    4.     }
    5.  
    6.  
    7.     IEnumerator LoadAdditiveScenes()
    8.     {
    9.         Scene splashScene = SceneManager.GetActiveScene();
    10.  
    11.         AsyncOperation asyncOpA = SceneManager.LoadSceneAsync("SceneA", LoadSceneMode.Additive);
    12.         AsyncOperation asyncOpB = SceneManager.LoadSceneAsync("SceneB", LoadSceneMode.Additive);
    13.  
    14.        asyncOpA.allowSceneActivation = false;
    15.        asyncOpB.allowSceneActivation = false;
    16.      
    17.         while(asyncOpA.progress < 0.9f || asyncOpB.progress < 0.9f)
    18.         {
    19.             // Wait for all scenes to load
    20.             yield return null;
    21.         }
    22.  
    23.         asyncOpA.allowSceneActivation = true;
    24.         asyncOpB.allowSceneActivation = true;
    25.  
    26.         // Wait for all scenes to fully load
    27.         for (int i = 0; i < SceneManager.sceneCount; i++)
    28.         {
    29.             Scene scene = SceneManager.GetSceneAt(i);
    30.             while (!scene.isLoaded)
    31.             {
    32.                 yield return null;
    33.             }
    34.         }
    35.  
    36.         AsyncOperation unloadSplash = SceneManager.UnloadSceneAsync(splashScene);
    37.  
    38.     }
    The FindObjectsOfTypeAll code looks like this (it is the same for A and B, just different class names/text in the Debug.Log() calls):
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class FindObjectsReporterA : MonoBehaviour
    4. {
    5.     public FindObjectsReporterA()
    6.     {
    7.         Debug.Log("FindObjectsReporterA constructor called.");
    8.     }
    9.  
    10.     void Awake()
    11.     {
    12.         Debug.Log("FindObjectsReporterA Awake called.");
    13.  
    14.         var objs = Resources.FindObjectsOfTypeAll(typeof(MonoBehaviour));
    15.         foreach (Object obj in objs)
    16.         {
    17.             Debug.Log("FindObjectsReporterA found:  " + obj.name);
    18.         }
    19.     }
    20. }
    21.  
    Here is my debug log showing the issue:
    upload_2020-1-22_11-52-17.png

    As you can see from the constructor Debug.Log, FindObjectsReporterB is loaded prior to FindObjectsReporterA.Awake() runs, but it is not found when calling FindObjectsOfTypeAll(). However, FindObjectsReporterB can find the MonoBehaviour in both scenes.

    The script reference says Resources.FindObjectsOfTypeAll() can find any object that is "loaded". I am wondering if "loaded" means something other than the constructor being called?

    I have attached the reproduction project I created to demonstrate this (Unity 2019.2.8, I also tested in .18). I can submit a bug report, but I wanted to check on here first to see I am just misunderstanding something or making a silly mistake.

    Just open the SplashScene, and hit run. Observe the console output.

    EDIT: had duplicate files uploaded, attempted to clean it up. Also, forgot to set allowSceneActivation to false. Updated code, still an issue.
     

    Attached Files:

    Last edited: Jan 22, 2020
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    Yes. Unity does a lot of stuff with constructors that won't necessarily match your intuitions. They may be called multiple times, they may be called before the scene is ready to do stuff.

    (If you think it through: before a Unity GameObject is fully functional and ready for action, there is a list of things it needs to do to get ready, and one of those things is to construct all of its components. Therefore, the constructors of the components are guaranteed to be called before the object is actually ready for action. And since Unity is trying to load the scene as a unit, it's actually not going to declare the scene ready until all of the objects in it are ready...)

    If you look at the timestamps on your log, you'll see the constructors have a different timestamp than the awake calls, which means they're not even being called in the same frame.

    In general, "asynchronously" does not mean "simultaneously". Things still happen in some particular order; you just don't know what that order is going to be. Asynchronous code may have performance advantages, but it actually gives you less control over timing.

    AFAIK there is no way to load two scenes and make it so that both of them are available before either one of them Awakes.

    You could have your GameObjects all be set to inactive so that they don't Awake when the scene loads, and then manually set them to active at some later time once you're sure both scenes have loaded.

    Or you could write your own function to do the job that you're currently trying to do in Awake, and then call it explicitly at an appropriate time.
     
  3. gtzpower

    gtzpower

    Joined:
    Jan 23, 2011
    Posts:
    318
    Thanks for the reply.

    My hope was that loading async but not activating until both were fully loaded would satisfy the FindObjectsOfTypeAll's definition of "loaded" in the documentation.

    I suppose this dictates that I should find a different way.

    Funnily enough, my inability to control the active state of the objects is why I am trying to do this in the first place. I am using Mirror (a continuation of uNET) which disables objects in the scene on build, and their Awake() functions are run when Mirror initializes and enables networked entities (after other objects have already Started). I'm trying to execute code on these networked objects at the same time as every other object's Awake() so that Start() on non-networked objects in the scene can work with properly configured, networked objects.

    That's what I am trying to do. I am trying to call a custom function on a bunch of objects that are disabled by Mirror, but I am trying to have a single class instance call out to all objects in all additively loaded scenes to notify them that it is time to initialize. Normally, I would have the additive scenes all subscribe to a singleton, but I would have to do this in the constructor in this case since the objects are disabled (feeling iffy about using the constructor for anything, not even sure if I cold do it for this). So, to avoid using the constructor, I was hoping to use FindObjectsOfTypeAll to get my list of instances.

    I was hoping to have the class calling FindObjectsOfTypeAll in the 1 common environment that all of the additive scenes load along side of, but it looks like that may not be an option. I don't want to put it in every additive scene as there may end up being multiple instances of the class calling the initialization function multiple times if I have 3 additive scenes. Unless I am missing something where you can FindObjectsOfTypeAll on a specific scene?

    I have other options, they just get less and less elegant. Iterating through all objects in a scene looking for components for example, and doing this from a script in each additive scene. Using the PostProcessScene attribute to build a list of objects and assign them to a non-networked entity to get a call on Awake for each scene. I was just hoping to keep it simple.

    Still wondering what "Loaded" means. LOL
     
  4. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    "Loaded" presumably means that Unity has finished creating the object and it is ready to do stuff.


    You've already got some code that waits until all scenes are loaded and then tells the splash scene to unload. My suggestion is that you should insert some code in there that finds the appropriate objects in the newly-loaded scenes and tells them to initialize. (Because that's the point when you know that all scenes are ready.)
     
  5. gtzpower

    gtzpower

    Joined:
    Jan 23, 2011
    Posts:
    318
    If only it were that easy. I need initialization to happen during Awake, before Start, but Awake and Start are both executed in the while loop where I am waiting for scene.isLoaded. If I try to get objects from the scene right before that loop, the scene contains 0 objects. If I try to get objects right after that loop, it is too late as Start has already run.

    EDIT: I thought maybe there would be a chance to pick something up in the loop. But nope. Here is the code:
    Code (CSharp):
    1.  Scene scene = SceneManager.GetSceneAt(1);
    2.         while (!scene.isLoaded)
    3.         {
    4.             Debug.Log("Loading scene...  Object count: " + scene.rootCount);
    5.             yield return null;
    6.         }
    7.  
    8.         Debug.Log("Scene fully loaded.");
    And the output:
    upload_2020-1-24_10-16-42.png

    As you can see, there is no option for me to do anything before Start.
     
    Last edited: Jan 24, 2020
  6. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    I think you have misunderstood my suggestion. I'm suggesting you take everything that you would normally do as soon as the scene is loaded--Awake, Start, OnEnable, whatever you are doing--and rewrite it so that it only happens after your loader script confirms that both scenes are loaded and sends some message to everything telling it that it's now safe to get started.

    Awake/Start happen when that scene is ready. If you need to wait for all scenes to be ready, then you can't use Awake/Start. Use something else.
     
    Kurt-Dekker likes this.
  7. gtzpower

    gtzpower

    Joined:
    Jan 23, 2011
    Posts:
    318
    I gotcha. My work here is a small piece of a very large puzzle though. I won't be able to dictate changes like that on the entire project as I would need to do. Part of my goal is to disable some other objects in certain conditions before their start runs. It is probably $100k cheaper (just a little messy) to put a script in each scene that iterates all scene root game objects to find components (in children) and then call the function on the objects in that scene.

    Thanks for confirming that I cannot do it in the centralized way that I wanted to though.