Setting up references between main and additive scene

Hi,

I’m having a hard time deciding on how to solve the following problem - I can think of solutions, but they feel cumbersome to me - and would love some feedback.
My setup is as follows: I have a Level Scene containing the world and enemies and a single Player Scene that is loaded additively for each level. For MonoBehaviours that pertain to both the player and world/enemies I wrote a Service Locator (singleton with a dictionary mapping service type name to service reference). Right now, the “service” MonoBehaviours register themselves with the Service Locator in their respective Awake() methods so service users can Get() them in Start() or later down the line. Note that both service MonoBehaviours and those that use services are present in both scenes. This worked nicely when I had the Level Scene open and additively opened the Player Scene in the editor before entering play mode, but naturally doesn’t work when I load the two scenes from the main menu at runtime, as I can no longer rely on Awake() having been called before Start() for both scenes (I’m using SceneManager#LoadSceneAsync() to first load the Level Scene and add the Player Scene afterwards with LoadSceneMode.Additive).
Is there some nice solution to this that doesn’t involve every service user (repeatedly) checking whether the service was already registered first or restructuring everything?

Thanks.

You could add a way for objects to subscribe to the service locator to get a notification when the service is ready.

For example, in your service locator, add this:

Dictionary<Type, List<Action>> subscriptions = new Dictionary...

// Objects can call this to get notified when the given service type is ready
public void NotifyMe(Type t, Action a) {
  if (activeServices.Contains(t)) {
    // The service is already ready. Run the subscriber right away
   a?.Invoke();
  }
  else {
    // Otherwise, add the subscription.
    if (!subscriptions.ContainsKey(t)) {
      subscriptions[t] = new List<Action>();
    }
    var list = subscriptions[t];
    list.Add(a);
  }
}

Then, also in your service locator, in your “service registration” function:

public void RegisterMe(Type t, object instance) {
  // Your other registration code here

  // run any subscriptions waiting for this registration to happen
  if (subscriptions.TryGetValue(t, out List<Action> subs)) {
    foreach (var sub in subs) {
      sub?.Invoke();
    }
  }
}

Then for your objects that care about these services in Start/Awake, you can register to get notified when they are ready:

void Start() {
  ServiceLocator.NotifyMe(typeof(SomeServiceICareAbout), () => {
    var theService = ServiceLocator.GetService(...);
    // Do whatever I need to do with the service.
  });
}

Thank you for the answer and code snippets!
I was kind of hoping for some simple(r) solution I just couldn’t see, but maybe that doesn’t exist. I experimented with disabling allowSceneActivation on the AsyncOperations returned by SceneManager.LoadSceneAsync() until both scenes are loaded and re-enabling it afterwards, but that still doesn’t make the scene activation step happen in a single frame.
It seems there is no way around some sort of notification system but I realized that I don’t actually need fine-grained notifications on a per service level but a single “scene loading complete” event is sufficient for me.
So I decided to write a static class that just wraps SceneManager.LoadSceneAsync() with the addition of

  • a method to specify the number of scenes that are going to be loaded together which is decreased by one for each LoadSceneAsync() completion
  • a bool method to check whether that number has reached zero (meaning all scenes to be loaded have finished loading)

I then turned the Start() methods that try to get a service into Coroutines and add

yield return new WaitUntil(SceneLoader.AllScenesLoaded);

before requesting a service (I also set enabled to false before the yield statement and back to true afterwards to prevent NPEs in Update() when the service is used there, nicer than checking the service reference for null every frame). This way I don’t need to worry about subscribers and save a few null checks.

Here’s the relevant code in case someone else finds it helpful:

public static class SceneLoader
{
    private static int _sceneLoadCount;
 
    public static void SetLoadSceneCount(int count)
    {
        _sceneLoadCount = count;
    }
 
    public static void LoadScene(string sceneName, LoadSceneMode mode = LoadSceneMode.Single)
    {
        var loadOperation = SceneManager.LoadSceneAsync(sceneName, mode);
        loadOperation.completed += operation => _sceneLoadCount -= 1;
    }

    public static bool AllScenesLoaded()
    {
        return _sceneLoadCount == 0;
    }
}

Usage:

// Loading scenes:
SceneLoader.SetLoadSceneCount(2);
SceneLoader.LoadScene("_Scenes/SampleScene");
SceneLoader.LoadScene("_Scenes/PlayerScene", LoadSceneMode.Additive);

// ... waiting for loading to finish before requesting a service:
private IEnumerator Start() { // Coroutine to allow for WaitUntil
    enabled = false; // prevent uninitialized _myService field from being accessed in e.g. Update()
    yield return new WaitUntil(SceneLoader.AllScenesLoaded);
    _myService = ServiceLocator.Get<MyServiceType>();
    enabled = true;
}