Search Unity

Gathering gameobjects loaded during LoadLevelAdditiveAsync

Discussion in 'Scripting' started by lordofduct, Mar 11, 2014.

  1. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    So I need to get a reference of the gameobjects that are loaded when I call LoadLevelAdditiveAsync. I searched around for a way that unity just handed them back, but alas they don't have that built into their api (why... why oh why!?)

    I read multiple people say to get all gameobject before the load, and then after the load, and those that are new are the ones that are loaded.

    Well... not necessarily though. I'm calling this Asynchronously, and it could take a couple frames to load. During those frames some gameobjects might spawn from spawners in the level. So I need all the gameobjects just before it activates.

    So I looked into the 'allowSceneActivation' property of AsyncOperation. Oh dear, here's a fun little bag of magic values.

    So if you set 'allowSceneActivation' to false, it will never register as isDone. Instead it'll progress to 90% done, and progress returns 0.9. Which I'm not a fan of, it's not like this was in documentation anywhere... I just happened across it reading other people's experiences (as well as my own testing). Ok, so I can wait until it reaches 0.9, then get gameobjects, and then hope that the thing actually completes in one whole frame. Does it complete in one whole frame? I don't know.

    So this is what I came up with:

    Code (csharp):
    1.  
    2.             op.AsyncOperation.allowSceneActivation = false;
    3.  
    4.             //magic number - unity stops loading at 0.9 progress and waits for it to be flagged to allowSceneActivation
    5.             while (op.Progress < 0.9f)
    6.             {
    7.                 yield return null;
    8.             }
    9.  
    10.  
    11.             //we assume that it it will complete loading by next frame... as far as I know, it does.
    12.             op.AsyncOperation.allowSceneActivation = true;
    13.  
    14.             yield return new WaitForEndOfFrame(); //wait to end of this frame so anything this frame wants to spawn is spawned
    15.             var prior = GameObject.FindObjectsOfType<GameObject>();
    16.             yield return null; //wait that one frame
    17.  
    18.             while (!op.AsyncOperation.isDone)
    19.             {
    20.                 //this shouldn't even occur... but in case it does, we continue getting 'priors' in case anything spawns during this wait cycle
    21.                 yield return new WaitForEndOfFrame(); //wait to end of this frame so anything this frame wants to spawn is spawned
    22.                 prior = GameObject.FindObjectsOfType<GameObject>();
    23.                 yield return null;
    24.             }
    25.  
    26.             var objects = new GameObjectCollection((from o in Object.FindObjectsOfType<GameObject>().Except(prior) select o.transform.root.gameObject).Distinct());
    27.             objects.Name = op.SceneName;
    28.  
    Note I wait to the EndOfFrame before getting my 'prior' list, and this script is set to be the earliest update method to fire. So I'm trying to ensure that there is not time for any code out there to spawn anything between the time we get the prior list and we get the new list.

    Thing is, I don't know if that will actually only be one frame. So I have that loop to keep grabbing in case it takes even longer (I don't know if web player, or iOS or OSX act different... I only have Windows and Linux to test with). This concerns me, because I know that this call is slow. And I am calling the load level asynchronously so not to impact the game as much... defeating the purpose all together.

    Does anyone know of a better approach to this?

    Does anyone see any holes in what I've done?




    2 holes I can see is...

    1) what if I have another script somewhere that also does a 'yield new WaitForEndOfFrame()', then spawns something. And that yield causes that code to run immediately after this code. That's a gameobject that'll get included into this list.

    2) I currently have the script set to run as the earliest update. The only hole here is if I'm stupid enough to allow a script to be created that operates even earlier. Sure... I just have to make sure I don't do that (this includes when adding any 3rd party code)... but for something so critical, that's just... dirty, IMO.



    Also, I have considered using something like tags to tag those root gameobjects and just do it that way. But this comes with the necessity of having my artist tag root gameobjects of scenes constantly. For something so generic as 'get all gameobjects that were loaded by a call to LoadLevelAdditive', I shouldn't require so much work from the person drawing a scene. It leaves potential for someone forgetting to tag something and breaking something that can be cryptic to debug when you don't know that someone forgot such a thing.
     
    Last edited: Mar 11, 2014
    TruffelsAndOranges likes this.
  2. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    Make all your objects register themselves to your manager when they start up?
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Thanks for the response.

    Two problems I can see with this.

    1) it requires work from the person drawing the scene to register those objects.

    2) this adds another level of work as it requires whatever is attached to said objects to also flag which scene it was a member of when loaded. So not only does the designer of the scene need to flag the objects, they also need to remember to include the unique identifier as well.
     
    TruffelsAndOranges likes this.
  4. Gibbonator

    Gibbonator

    Joined:
    Jul 27, 2012
    Posts:
    204
    Maybe you could write an editor script that uses the PostProcessSceneAttribute and auto generate an object that lists everything in the scene. I've not used this myself and there is little documentation so it may not turn out to be a workable solution. Just an idea.

    Edit: You may be able to use AssetDatabase.GetAssetOrScenePath in the script to work out if the objects are part of the relevant scene.
     
    Last edited: Mar 11, 2014
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Hrmm... I could look into that.



    Does anyone have any further information about 'allowSceneActivation' and how to work with it?
     
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,698
    Not to hijack the thread, but what about taking a different approach? (I'm mostly pasting this from here.)

    Organize each scene into a single parent GameObject for that scene. Then you know that all of the just-loaded objects are children of it. There's a little onus on the scene designer, but just to move everything into a single parent GameObject. The objects don't have to register themselves or anything complicated like that.

    When you need to unload a scene, just can destroy the parent GameObject. To make it easier, you can organize all of the scenes under a master GameObject, such as:
    Code (csharp):
    1.  
    2. [Scene Manager]
    3.     [Scene 1]
    4.         {scene 1 contents}
    5.     [Scene 2]
    6.         {scene 2 contents}
    7.     ...
    8.  
    You can put a script on the Scene Manager object that maintains a list of which scenes are loaded, with functions to load and unload scenes.

    When you build each individual scene, put everything in a GameObject named Scene N (where N is the scene number).

    When Scene Manager loads the scene, it should find the Scene N GameObject, child it under the Scene Manager GameObject, and add it to the script's list.

    Unloading unused assets is a different topic. You could just let Unity handle this, or call Resources.UnloadUnusedAssets() at non-time-critical times.

    I thought I'd put together some code that demonstrates what I was writing about. You can download the attached package and watch a quick video on how it works:
    [video=youtube_share;W30CHkGhRY8]http://youtu.be/W30CHkGhRY8

    Alternate download link: http://www.pixelcrushers.com/scene-loader/

    View attachment 89477
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    I actually already do that. The 'roots' that get collected up in this scene really end up only being 1 if the designer did set up the scene correctly.

    I was just attempting to:

    1) abstract this SceneManager (that's the class this is in) out further so who ever uses it doesn't have to concern themselves with that aspect.

    2) there's still the issue of if somewhere, someone, spawns a gameobject into the global hierarchy as opposed to inside the gameobject.

    I had gone about an approach that even had a script you attached to that root gameobject for the scene, but this just increased the onus on who designed the scene.



    Oh, and in another thread, someone had said they tried waiting until the 0.9, and it blocked forever on them. I'm starting to think the magic value isn't going to work and that it might vary from system to system. Ruins my plan... ugh.

    post was here: http://forum.unity3d.com/threads/233473-quot-allowSceneActivation-quot-locks-up-the-editor
     
    Last edited: Mar 11, 2014
  8. gilley033

    gilley033

    Joined:
    Jul 10, 2012
    Posts:
    1,191
    Another possibility would be to assign all the objects you are going to load Asynchronously a unique tag, and then use FindGameObjectsWithTag once your scene loads to find them all.

    According to the thread here (which is from 2006 so who knows if it's still accurate), Unity keeps a list of all objects that have a tag (i.e., anything other than "Untagged" is kept in this list), and searches this list when using FindGameObjectsWithTag.

    So, if using this method, once you've found your newly loaded objects, you should change the tag to "Untagged" so the object is removed from this internal list and doesn't bog down future searches.

    Obviously this method wouldn't be ideal if you need to use tags for other purposes, or if you have a lot of tagged objects in your scene already that would slow down the performance of FindGameObjectsWithTag.

    But it is another option!
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Note the last paragraph of my original post:

     
    TruffelsAndOranges likes this.
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    In each scene, you add a "Manager". Anytime you change the hierarchy in the editor, the Manager rebuilds a list of all GameObject in that specific scene. (Requires an editor script using the event EditorApplication.hierarchyWindowChanged)

    When you load a new scene, you look for your manager, and there! List of all GameObject just for you.

    That way, the data reflection on a scene content is within the scene itself.
     
    Last edited: Apr 12, 2014
  11. gilley033

    gilley033

    Joined:
    Jul 10, 2012
    Posts:
    1,191
    Hmm, I don't know how I missed that! Apologies.
     
    dragonBRP likes this.
  12. TruffelsAndOranges

    TruffelsAndOranges

    Joined:
    Nov 9, 2014
    Posts:
    92
    This is probably the best way to do it, even though it's a really crappy way. As the author of the thread described, this behaviour forces the level designer to think about managers and scene managing, instead of just creating his/her scene.

    Kinda sad really, but Unity 5.3 has a new scene manager, so I hope they thought things through this time around.