This is a potential solution to the dependency injection problem. That is, when an object needs access to some shared resource, how does it get a reference to it? Shared resources are typically handled in games as Singletons, but in truth that is not strictly necessary. Some of those resources could be unloaded and reloaded on an “as needed” basis. Why do we care? Because to test the client object, we could ideally use something completely different for the datasource and the client will be none the wiser.
Why not Editor references, or prefabs? Because a reference is meant for indicating a controller-controlled relationship. The object with the reference is the controlling object, meant to make changes to the controlled object. If we were to add references to dependancies as well, then it would give us dependency injection. The catch is, if we later want to change the object we depend on, perhaps to change the specific subclass, the reference that the child objects have can be invalidated. Then you would have to manually reconnect every single object reference to the new object, manually.
This goes the same for prefabs. There are ways of keeping the prefabs sane, but one change (like deleting an unrelated GameObject from the prefab) will require a new prefab, invalidating every reference to (and from) it.
This solution is designed for game/application code. It is not designed for separate libraries or assemblies. it also is designed as simple as possible. Therefore some performance costs are incurred so that the final code can be simpler.
First we define how a piece of game code will request a dependency. Perhaps it should look something like this:
SharedObject thing = SharedObject.instance;
That sure is pretty. The only gotcha is, when our SharedObject is an interface, we can’t declare an instance with a static property. Okay, then let’s get lower level.
SharedObject T = DependencyRegistry.Find
Not bad. Brief, but effective. We intentionally avoid the DependencyRegistry.instance.Find<>() pattern because it’s more wordy. We will never be passing the DependencyRegistry as a parameter to a method, for example.
What if the caller needs to choose from a list of providers of the same class type? Perhaps a list of database connections. We will consider this a rare case, as most of the time you just need a single class or interface implementation. In any event, this is type-safely solved by creating a wrapper class or interface that offers the list in whatever form makes the most sense for the task at hand. It could be a list object, or even a custom class that has a field for each specific instance as needed.
That SharedObject.instance thing was nice though. And it would work perfectly fine for any concrete class. At first blush, it would seem inconsistent to allow two ways of accessing the same feature. However, the mindset behind an interface is very different from a class, so we can allow it. We just need to remember that a concrete provider that isn’t a singleton has to define an instance getter that resolves to the Registry.
Okay, API defined. How do we implement it? Clearly some searching is needed. Initially I considered using the Transform Hierarchy to search, but I realized that most objects in that tree will not be providers. Additionally, some of the providers may not even be in the same Transform root of the hierarchy. This leads me to creating an object registry. That is, a central list of objects that offer themselves as datasources.
The registry itself is as simple as a list of objects. We chose weak references so that a provider could use the OnDestroy or IDisposable interfaces to know when no consumers are making use of that provider any longer. Also, there should be relatively few providers at any one time, so the ram overhead should be negligible.
As an optimization, we assume that objects added later to the list will be more relevant to the potential consumers than the earlier ones. Therefore when searching we start at the end and work our way backwards.
Getting added to the registry is as simple as asking to be added. To prevent waste, we dedup additions. This can be safely done from within a MonoBehaviour’s OnEnable() method. This will be called before Start(), and work correctly from within prefabs as well as scenes. Likewise, removal is easily done during OnDisable().
It is not required that you add/remove yourself to the registry at these times. You can add or remove yourself at any time. The only things that care are the potential consumers. It is perfectly possible for a consumer to sit in a Start coroutine and wait for the provider object to become available.
The full code will be made available as soon as I can work out permissions with my bosses. That said, it’s a simple enough design that implementing it yourself would be an easy task.