For my level-based game I’ve came up with a setup where 3 scenes are loaded additively when you load a level:
Boot scene (never unloaded, common managers, handles scene switch and loading screen, reused in levels as well as menus).
LevelsCommon scene (re-loaded every time, all gameplay managers live here, as well as everything that’s common to any level - UI, camera rig, etc.)
Level1/Level2/ etc. (only level specific objects - geometry, environments, game logic, etc.)
I also relied heavily on using Start function to use related objects. These relations sometimes span between scenes:
a) Some global managers in LevelsCommon use FindObjectOfType in Start to get hold of an object in current level.
b) Some objects register themselves with global managers in Start function via singleton pattern.
My SceneSwitcher (that loads levels) uses LoadSceneAsync() with Additive mode to load the Level scene and the Common scene at the same time. But the problem is - the actual activation of scenes always happens sequentially. During this activation, first Awake and OnEnable are called for all components. Then Start is called. But because Awake/OnEnabled in the second scene will only be called after Start (and even the first Update) in the first scene, I will inevitably have errors accessing references objects from Start when these objects haven’t been really loaded yet.
I tried various ways to “synchronize” the second stage of activation of scenes (so first Await is called for both, then Start called for both, like when you use single scenes), but none of them worked:
Using AsyncProcess.allowSceneActivation = false, wait until all scenes are 90% loaded, then resume. Doesn’t work, because at 90% when scene is “kinda loaded”, rootCount is 0 and no scene objects have been loaded yet.
Waiting until all scenes return true in Scene.isLoaded and then use SceneManager.MergeScenes somehow. Doesn’t work, the isLoaded of the second scene will be set to True only after first scene is fully activated (Start/Update is called).
Doing the same with SceneManager.sceneLoaded event. Doesn’t work, same reason.
Now I only see these options to resolve the issue:
Use prefabs for Common stuff instead of separate scene. A viable option now when Nested Prefabs are a thing. But how did people live before 2018.3? Also I know it’s a common practice to separate complicated levels into multiple scenes, so surely there must be a proper solution.
Only access referenced objects in Start in one direction (eg. Level objects access Common objects, never the other way around). Should work because it seems scene load their objects in the same order as the original LoadSceneAsync() calls. Don’t like this approach because it places restrictions on MonoBehavior code which should work regardless of external factors.
Invent some kind of delayed Init method for affected components. Call it either from my SceneSwitcher, using SceneManager or subscribe to custom LevelLoaded global event. (I have EventManager in my Boot scene). Don’t like this as well because it’s a “bicycle” (reinventing the wheel).
Is there any other solution I’m missing to synchronize activation of multiple scenes or I have to tweak my components to overcome this limitation?
Order of them activating are actually unknown.
It may differ from build to build. In editor it may seems as they’re loaded one by one, but in the build its not.
Reason for this is that Editor loads scenes in a synchronous manner, faking async loading. Build does have a complete async loading instead.
Tips:
Decouple your logic. Probably the best one I can give you for this case.
And yes, you’ll have to tweak your components. (This is also second of your own list)
Alternatively, move your managers code to the scene that is loaded always first. Wait for it to load, then load the rest.
1.1. Do not rely on “luck” of the script execution order, as that one will not work for you.
1.2. Circular dependencies are always evil. In Unity or even outside it. Try avoiding them.
Notify the required managers when you’ve loaded something to catch up.
Initialize components when needed manually.
Reverse control entities. Instead of gathering them via .Find etc.
Create an event, that is called by the entity when its ready for the manager to be picked up.
This I didn’t know. I guess I can wait for the first scene to fully load, and then call LoadSceneAsync for the second scene. To be 100% sure they are loaded in the order I want them to, since docs don’t mention if the order of LoadSceneAsync calls matters or not. Could as well assume it doesn’t.
I have no circular dependencies on component-level. Just for some cases it was convenient to call the manager for the object being managed, and in other case the other way around. Maybe I should choose only one direction for this and try to make it work.
I’d avoid these if possible. I’ll have to add complexity to these components so they know to wait for scene load or have additional initialization stage. By keeping things simple and sticking to standard MonoBehavior events I can reuse my components later.
What do you mean exactly? Since you can’t have direct references between objects on different scenes, you either use Find functions or make managed components register themselves by accessing the manager via Singleton, DI container or some other means.
public class EntityManager : MonoBehaviour {
public static void AddEntity(Entity entity) {
// Store them into collection of your choice
// Do anything as well
}
public static void RemoveEntity(Entity entity) {
// Remove it
}
}
public class Entity : MonoBehaviour {
private void OnEnable() {
EntityManager.AddEntity(this);
}
private void OnDisable() {
EntityManager.RemoveEntity(this);
}
}
Just make sure your manager exists before loading any entity.
Note that you can have references between scenes in the script but not in via the serialized variables in the inspector.
What you propose is almost exactly the same as Singleton pattern, since you rely on static C# variables. I asked a very specific question, I didn’t ask for basic architecture advice. Don’t assume ignorance in others when it’s not called for.
I’m not. You can use Singleton or whatever you want, its not like it is prohibited.
My idea is that you fill the collection with entities, and process that collection only, instead of performing finds.
This way you’ll avoid timing issues.
Alternatively, perform actions in the manager same way as you do.
I do not know what you’re doing in the manager, so that’s up to you.
I’ll rephase it. Notify your manager when your entity is ready. Do not expect it to be ready upon Awake / Start.
Adding / Removing is just an example.
Replace that with event system if you like. Call the “ready” event when your entity is ready.
Subscribe to that event by the manager and catch that components ready. Then do whatever you like with it.
This will only be helpful if the manager component was loaded before (in Boot scene) or if it can be lazy-created. Some of my managers have serialized fields to tweak their behavior in Inspector, so I need those specific instances from the scene file. In the end, this just moves the problem to other side - Manager will be expected to be ready when Entity is initializing.
For this Manager<->Entity problem I’ll need either to use some third mediator (like Level Loaded event from Boot mentioned before) or to settle on one direction of references and stick to it. In the latter case probably it makes more sense to load the common stuff first, and then the level itself. I’ll have to fix code either way.
I have similar problem with components that are not managers, they rely on specific order of scene activation currently and use Find methods to attach themselves to some objects, which may be on another scene.
It seems “Level loaded” event option asks to be used to solve this. I’m sure it WILL work, but not sure how clean the result will be in terms of component isolation. Gonna have to try and see.
In turned out only 2 components needed level objects to exist on Start, so the rest of components remained as is (relying on managers being loaded first and registering themselves via Singleton). So far so good, will continue to evaluate this approach.