Accessing MonoBehaviour instances from one scene in another scene?

I’ve been exploring the Scriptable Object Pattern in Unity and came across an idea for accessing game objects from different scenes in the current scene.

The idea is to have a container ScriptableObject that can store a single reference to an object. MonoBehaviour instances can register themselves with this container in Awake(), making them accessible throughout the game.

Here’s what it looks like:

public class ContainerBase<T> : ScriptableObject where T : class
{
    private T _ref;
    public T Ref => _ref;

    public void Occupy(T @ref)
    {
        if (_ref != null)
        {
            Debug.LogError($"[{name}] Attempted to register multiple {typeof(T).Name} instances.", this);
            return;
        }

        _ref = @ref;
    }

    public void Release()
    {
        _ref = null;
    }
}

The problem is making sure a container is always occupied during runtime. My idea is to create global dependencies as prefabs and instantiate them using RuntimeInitializeOnLoad. These objects register themselves in Awake() and release in OnDestroy().

This avoids having scripts directly reference objects through singletons. I don’t think singletons are evil—they can be really powerful when used correctly—but in team projects, people tend to call them from random places just because they can, and that can turn into a mess. This way, everything depends on the container, making dependencies more structured.

One issue I see is that this doesn’t work well for lazy singletons, where instances are created only when needed.

In personal projects, I usually just go with singletons because they’re simple. But in a team setting, they often lead to spaghetti code. I’m wondering if this approach is worth using or if I should just stick to singletons. Curious to hear what others think!

Just to be clear in case your statement means “accessing objects in scenes not currently loaded”: such references will not be valid. When accessing a reference to an object not in a loaded scene, it will throw a MissingReferenceException.

A single SO for a single reference? That’s pretty limiting. And an awkward workflow because you need to physically create a new asset every time you need a new reference.

That’s also the aspect of the SO pattern that can equate to an artificial “task creation measure” (in german we have a real nice word for this: Arbeitsbeschaffungsmaßnahme … try pronouncing this! :grin: ). So you need a new variable? Okay then create a new asset, name it, place it somewhere meaningful, drag & drop it onto the Inspector or pick it from an already overwhelming long list of “variable” assets.

Ugh.

I want to be able to work in code as much as possible!

I use a Components Registry, which is essentially “reverse dependency injection” or more commonly known as “dependency lookup”. I have since made this a generic class, the article doesn’t describe the generic part.

I chose to go with a MonoBehaviour because I only have a single scene loaded with all global objects in it. All other scenes are loaded/unloaded additively, effectively making the “build index 0” scene behave like the DontDestroyOnLoad scene but with more control and flexibility.

The registry would thus behave the same if you added it to the first scene in the build, and moved it to the DDOL scene in case you want to retain the ability to single-load scenes. Personally, I wouldn’t make such a registry a ScriptableObject because SO has a different lifetime than the references it holds. It’ll work better as a MonoBehaviour simply because if you do change scenes, and the registry is not in DDOL, you needn’t even worry about unregistering references when changing scenes.

As a registry, you can register any reference to it, and acquire the reference by type. It only works with single references, ie you can’t lookup multiple components of the same types (ie each enemy) but this is easily overcome by registering a single component that provides access to its “child” components, such as some form of “enemy manager” that contains all enemy objects and likewise for other kinds of collections (players, cameras, resources, and so forth).

PS: Can’t help but point out that the SO workflow that has seen so much hype is just one way to do things and it’s rarely realized or pointed out that the benefits of that system are mainly for teams and games which are content-heavy and the designers want or need great autonomy without messing too much (or at all) with code. If on the other hand you have a programmer-heavy team they will come to despise this SO pattern as it means there’s a constant back and forth between programming and working with assets and Inspectors.

1 Like

Thank you for the descriptive comment

Yes, I do understand that if the scene isn’t currently loaded, the references won’t exist. That is why I wanted to ensure the MonoBehaviours in the Global Scope are spawned in RuntimeInitializeOnLoad, register themselves on awake and release the containers when destroyed and are marked as DontDestroyOnLoad as a mandatory step. This also eliminates the problem of containers having different lifetime than that of the object it is holding.

I think the system I described is taking the granularity to a level where things will start getting messy with lots of scriptable objects. Keeping it simple is better.

I agree with you and that’s the whole point of me posting here. I am mostly working on library level features in my organisation and want to come up with decoupled systems which can be re-used in multiple games. But, I know that trying to solve a problem going in the details too much will often bring up other problems. I want to avoid making them so decoupled that there are scriptable objects everywhere and the ProjectWindow explodes. I will look into the Components Registry you mentioned. Thanks for sharing!

That sounds like a pretty decent DIY solution to me! It relies on dependency injection, which is the most flexible option that there is when it comes to resolving dependencies. It should quite effectively get rid of all the major negative effects that relying on the Singleton pattern heavily can bring about.

Using a static service locator (like the Components Registry) improves upon the Singleton pattern in a couple of ways (interface support, and the ability to more easily initialize all services in optimal order in a composition root), but it still has all its other flaws - including that big one that you mentioned:

people tend to call them from random places just because they can

With your proposed system, you could use the Reset event to automatically hook up all client components to those default services, but would still also always be able to swap any of those service with different ones for any client component instances just by dragging-and-dropping different containers into them - that’s really powerful!

Your system would also make it possible to unit test all your components while substituting any of those services with fakes, simply by creating new prefabs for those tests, and dragging in your fakes into then. It’s not quite as convenient as being able to do all of that easily purely in code, but it’d still be a dramatic improvement over using Singletons.

I don’t really foresee that the management of those scriptable object assets would become a major pain point. Probably the total number of global “manager” type objects that a project would get during its lifetime would be in the dozens, not hundreds, and creating one new scriptable object asset once per week or so doesn’t sound like it would get very frustrating. There are also assets there that make it possible to visualize many scriptable objects in a single registry-like view, if push should come to shove.

One thing that I could see potentially being a little bit irritating with your system, is that you wouldn’t be able to access members of your services directly though the scriptable object container, but would either always have to add that extra Ref accessor call everywhere, or define a second member in all your clients that provides a direct reference to the actual service that is provided by the container.

[SerializeField] Container<Service> serviceContainer;
Service service => serviceContainer.Ref;

If you used a DI framework instead, then those services could get delivered to the client as arguments of an initialization method, or injected directly into fields, removing the need for the extra container field.

Your system should be able to handle cross-service dependencies very well too (and even support circular dependencies), which is really nice as well.

1 Like

I’ve played with a system of using scriptable object intermediaries to establish references between scenes in a previous project. It definitely worked, but as the scriptable objects piled up it was becoming a bit of a pain to manage.

If I were to tackle it again, I would have a central registry where objects can register themselves (probably just a static class), alongside a key with which can be used to retrieve the object later on. Abstracting the key through an interface which implement IEquatable<T> (and making sure to override GetHashCode on implementors), then pretty much anything could be used as a key. Though it could be done with a struct if you need it to be very lightweight.

Worth noting these cross-scene reference systems also usually allow non-scene objects to get access to scene based objects.

1 Like

Well I do believe that the SOAP pattern is useful. I am thinking of creating a hybrid system where I will use SOAP only when required (Where non-programmers are involved or where it actually makes sense).

For example, a modular system where I think SOAP makes sense is a configuration system for Manual testers of a game. They can access a screen in the game where they can change the debug settings (Let’s say Base URL of an API). Now, I can just create a library which programmers can drag and drop in their game and specify which SOAP variables they want the testers to edit in game. Using this system, they can easily achieve it by dragging and dropping the variables instead of extending the system using code. Also, the system can be reused in other games easily because it does not expect the game to have specific code. So, I think SOAP is useful for changes we need to quickly make for non-programmers to use.

Coming back to the core problem, I think the problem of spaghettification of code lies more in the fact that projects / features are often not planned properly before execution. I think singletons / registries are not evil if you know exactly what you are trying to achieve and know the direction your data is going to flow. The problem occurs when people start abusing them because of their global nature which introduce very tight coupling everywhere. As long as someone who understands the details of these problems oversee the the whole architecture and planning of the game, these issues can be tackled. There are always trade-offs using one pattern over the other so using a hybrid model makes more sense to me.

Yeah, this is definitely it’s killer use case. In a small team with multiple designers/artists that are comfortable with working directly inside Unity, it can save a lot of time, improve the look and feel of the end results, and make the designers/artists really happy.

I think that this can definitely help a lot, but even in this case, the usages of singletons could end up coming back to bite you hard. The reason being, that you will never know before hand exactly what new requirements will turn up in the midst of development.

For example, in one game project I worked on, the need arose during development to implement a headless rendering mode; suddenly we needed to be able to bypass all the main menus, load a serialized game state, visualize everything on screen without executing any logic for any of the various dynamic entities that our game contains, render everything into an image, and upload it to the servers.

The ability to initialize the same clients with completely different services in different contexts, is something that a good dependency injection based architecture can excel at, while Singletons are basically guaranteed to fall flat on their face, and lead to long delays.