Adding dependencies for custom nodes
Inside your custom node you might want access to certain dependencies. Inside the node you can use the Get
method to
get certain dependencies, for example calling Get<Rng>()
to get the random number generator.
Previously, you needed to create your own graph class inheriting from the MapGraphGraph to be able to add your own dependencies. This was rather inconvenient and therefore version 1.27 introduces the ability to add dependencies without needing to create your own graph class.
There are 3 ways to register your own dependencies.
Adding a dependency in the Map Graph Runner inspector
If the dependency you want to add is a MonoBehaviour in the scene, you can add it this way.
You can add a new entry here:
Assigning a GameObject
to this entry, will allow you to pick a MonoBehaviour
on that GameObject to use as a dependency.
You are also able to select the type under which the component is registered. This will be either its own type (by default) or one of the interfaces the class implements.
The type is important, because it's the type used for getting the dependency inside your custom node. In
case above you'd get the dependency by calling Get<YellowProvider>
.
However, here you'd get the component by calling Get<IColorProvider>
. Why this is important will become clear
later, after discussing the other methods for adding dependencies.
Passing dependencies to the Run call
Another way to add dependencies is by simply passing them to the runner component when calling Run
through a
script, like this:
public class RunWithDependencies : MonoBehaviour
{
[SerializeField] private ScriptGraphRunner graphRunner;
public void Start()
{
// Create the dependency.
var redProvider = new RedProvider();
// Create a dependency container.
var dependencyContainer = new DependencyContainer();
// Register the dependency under the interface type IColorProvider
dependencyContainer.Register<IColorProvider>(() => redProvider);
// Pass the dependency container to the Run method.
graphRunner.Run(dependencyContainer);
}
}
Like with the example of adding the dependency through the inspector, registering the dependency under the IColorProvider
interface allows us to get the dependency in the custom node code using a Get<IColorProvider>
call.
Like the Run
method, a dependency container can also be passed to the RunAsync
and RunSync
methods.
Adding dependencies globally
The third method allows for registering dependencies globally, so that they are always available inside your custom
nodes, without having to assign them explicitly in the runner's inspector or passing them to the Run
method.
Here's how:
[InitializeOnLoad] // This makes sure this code gets executed on load.
public static class DependencyInitializer
{
static DependencyInitializer()
{
// Create the dependency.
var blueProvider = () => new BlueProvider();
// Assign the dependency to the global dependency container using this static
// method on the ScriptGraphProcessor class.
ScriptGraphProcessor.RegisterGlobalDependency<IColorProvider>(() => blueProvider);
}
}
Using the dependency inside your custom node
Now that you know how to add your own dependencies, you can use them inside your custom nodes. Simply call the
Get
method in the custom node's code with the type the dependency is registered under as a type argument, like so:
// Get the dependency.
var colorProvider = Get<IColorProvider>();
// Use the dependency!
var color = colorProvider.GetColor();
Which method to use
If you have a dependency that you want to be available everywhere, without having to assign it separately for each runner
component or every Run
call, it's easiest to register it to the global dependency container.
If you have a MonoBehaviour
that you want to register as a dependency, but you don't want to write a custom script for
it, use the inspector method.
If you're calling Run
from a script, you can just pass the dependencies to the method call.
You don't have to pick just one method though, you can use the different methods together, depending on the situation. Being able to override dependencies is a reason to use one method over the other.
Overriding dependencies (order)
So you might have been wondering why you'd ever need to register a dependency under one of its interface types instead of its own type. The reason is, that it allows you to replace dependencies in certain scenarios.
The dependencies are added (and override each other) in the following order:
- Global dependencies
- Dependencies assigned in the inspector
- Dependencies passed to the
Run
method
This means that assigning a dependency in the inspector can override one assigned as a global dependency, when registered
to the same type. And that dependencies passed to the Run
method overrides both of the other methods.
Following the example from earlier, say by default the BlueProvider
is registered as a global dependency under the type
IColorProvider
, but for a specific graph you prefer to use the YellowProvider
as the IColorProvider
. You can override
the default by assigning the YellowProvider
in that graph's runner. Now the YellowProvider
will be returned when
calling Get<IColorProvider>
when running the graph through that particular runner component, but the
BlueProvider
will be used everywhere else.
You can do the same if you want to override a particular dependency for a specific Run
call. For example, by registering
the RedProvider
as the IColorProvider
by passing it as an argument, overrides both other methods, for that
specific Run
call only.
Multithreaded execution compatibility
Whenever you use multithreaded execution, it can occur that multiple nodes try to use the same
dependency at the same time, which might cause strange and hard to diagnose issues, if your dependency is not thread-safe.
An easy way to make your dependency thread-safe is by wrapping it inside a ThreadLocal
instance, like so:
var colorProvider = new ThreadLocal<IColorProvider>(() => new BlueProvider());
container.Register(() => colorProvider.Value);
This will make sure that each thread has their own instance of the dependency.