AutoInject
Reflection-free node-based dependency injection for C# Godot scripts, including utilities for automatic node-binding, additional lifecycle hooks, and .net-inspired notification callbacks.
Install / Use
/learn @chickensoft-games/AutoInjectREADME
💉 AutoInject
[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] ![line coverage][line-coverage] ![branch coverage][branch-coverage]
Reflection-free node-based dependency injection for C# Godot scripts, including utilities for automatic node-binding, additional lifecycle hooks, and .net-inspired notification callbacks.
<p align="center"> <img alt="Chickensoft.AutoInject" src="Chickensoft.AutoInject/icon.png" width="200"> </p>
📘 Background
Game scripts quickly become difficult to maintain when strongly coupled to each other. Various approaches to dependency injection are often used to facilitate weak coupling. For C# scripts in Godot games, AutoInject is provided to allow nodes higher in the scene tree to provide dependencies to their descendant nodes lower in the tree.
AutoInject borrows the concept of a Provider and a Dependent from [other tree-based dependency provisioning systems][provider]. A Provider node provides values to its descendant nodes. A Dependent node requests values from its ancestor nodes.
Because _Ready/OnReady is called on node scripts further down the tree first in Godot (see [Understanding Tree Order][tree-order] for more), nodes lower in the tree often cannot access the values they need since they do not exist until their ancestors have a chance to create them in their own _Ready/OnReady methods. AutoInject solves this problem by temporarily subscribing to each Provider it finds that is still initializing from each Dependent until it knows the dependencies have been resolved.
Providing nodes "top-down" over sections of the game's scene tree has a few advantages:
- ✅ Dependent nodes can find the nearest ancestor that provides the value they need, allowing provided values to be overridden easily (when desired).
- ✅ Nodes can be moved around the scene tree without needing to update their dependencies.
- ✅ Nodes that end up under a different provider will automatically use that new provider's value.
- ✅ Scripts don't have to know about each other.
- ✅ The natural flow-of-data mimics the other patterns used throughout the Godot engine.
- ✅ Dependent scripts can still be run in isolated scenes by providing default fallback values.
- ✅ Scoping dependencies to the scene tree prevents the existence of values that are invalid above the provider node.
- ✅ Resolution occurs in O(n), where
nis the height of the tree above the requesting dependent node (usually only a handful of nodes to search). For deep trees, "reflecting" dependencies by re-providing them further down the tree speeds things up further. - ✅ Dependencies are resolved when the node enters the scene tree, allowing for O(1) access afterwards. Exiting and re-entering the scene tree triggers the dependency resolution process again.
- ✅ Scripts can be both dependents and providers.
📼 About Mixins
The [Introspection] generator that AutoInject uses allows you to add [mixins] to an existing C# class. Mixins are similar to interfaces, but they allow a node to gain additional instance state, as well as allow the node to know which mixins are applied to it and invoke mixin handler methods — all without reflection.
In addition, AutoInject provides a few extra utilities to make working with node scripts even easier:
- 🎮
IAutoOn: allow node scripts to implement .NET-style handler methods for Godot notifications: i.e.,OnReady,OnProcess, etc. - 🪢
IAutoConnect: automatically bind properties marked with[Node]to a node in the scene tree — also provides access to nodes via their interfaces using [GodotNodeInterfaces]. - 🛠
IAutoInit: adds an additional lifecycle method that is called before_Readyif (and only if) the node'sIsTestingproperty is set to false. The additional lifecycle method for production code enables you to more easily unit test code by separating initialization logic from the engine lifecycle. - 🎁
IProvider: a node that provides one or more dependencies to its descendants. Providers must implementIProvide<T>for each type of value they provide, or optionally implementIProvideAnyto handle the generic on the method level withIProvideAny.Value<T>. - 🔗
IDependent: a node that depends on one or more dependencies from its ancestors. Dependent nodes must mark their dependencies with the[Dependency]attribute and callthis.DependOn<T>()to retrieve the value. - 🐤
IAutoNode: a mixin that applies all of the above mixins to a node script at once.
Want all the functionality that AutoInject provides? Simply add this to your Godot node:
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
// Apply all of the AutoInject mixins at once:
[Meta(typeof(IAutoNode))]
public partial class MyNode : Node
{
public override void _Notification(int what) => this.Notify(what);
}
Alternatively, you can use just the mixins you need from this project.
[Meta(
typeof(IAutoOn),
typeof(IAutoConnect),
typeof(IAutoInit),
typeof(IProvider),
typeof(IDependent)
)]
public partial class MyNode : Node
{
public override void _Notification(int what) => this.Notify(what);
}
[!IMPORTANT] For the mixins to work, you must override
_Notificationin your node script and callthis.Notify(what)from it. This is necessary for the mixins to know when to invoke their handler methods. Unfortunately, there is no way around this since Godot must see the_Notificationmethod in your script to generate handlers for it.public override void _Notification(int what) => this.Notify(what);
📦 Installation
AutoInject is a source-only package that uses the [Introspection] source generator. AutoInject provides two mixins: IDependent and IProvider that must be applied with the Introspection generator's [Meta].
You'll need to include Chickensoft.GodotNodeInterfaces, Chickensoft.Introspection, Chickensoft.Introspection.Generator, and Chickensoft.AutoInject in your project. All of the packages are extremely lightweight.
Simply add the following to your project's .csproj file. Be sure to specify the appropriate versions for each package by checking on Nuget.
<ItemGroup>
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="..." />
<PackageReference Include="Chickensoft.Introspection" Version="..." />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="..." PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.AutoInject" Version="..." PrivateAssets="all" />
</ItemGroup>
You can also add Chickensoft.AutoInject.Analyzers to your project to get additional checks and code fixes for AutoInject, such as ensuring that you override _Notification and call this.Provide() from your provider nodes.
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject.Analyzers" Version="..." PrivateAssets="all" OutputItemType="analyzer" />
</ItemGroup>
[!WARNING] We strongly recommend treating warning
CS9057as an error to catch possible compiler-mismatch issues with the Introspection generator. (See the [Introspection] README for more details.) To do so, add aWarningsAsErrorsline to your.csprojfile'sPropertyGroup:<PropertyGroup> <TargetFramework>net8.0</TargetFramework> ... <!-- Catch compiler-mismatch issues with the Introspection generator --> <WarningsAsErrors>CS9057</WarningsAsErrors> ... </PropertyGroup>
[!TIP] Want to see AutoInject in action? Check out the Chickensoft [Game Demo].
🎁 Providers
To provide values to descendant nodes, add the IProvider mixin to your node script and implement IProvide<T> for each value you'd like to make available, or IProvideAny if you want to handle the generic on the method level.
Once providers have initialized the values they provide, they must call the this.Provide() extension method to inform AutoInject that the provided values are now available.
The example below shows a node script that provides a string value to its descendants. Values are always provided by their type.
[!NOTE] The
IProvideAnyinterface will stop signals from propagating to the node's parent. This could at best mute any errors from unresolved dependencies when implemented on the root node, and at worst stop a dependency from being resolved if it was supposed to be fetched from an ancestor.
namespace MyGameProject;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
[Meta(typeof(IAutoNode))]
public partial class MyProvider : Node, IProvide<string>
{
public override void _Notification(int what) => this.Notify(what);
string IProvide<string>.Value() => "Value"
// Call the this.Provide() method once your dependencies have been initialized.
public void OnReady() => this.Provide();
public void OnProvided()
{
// You can optionally implement this method. It gets called once you call
// this.Provide() to inform AutoInject that the provided values are now
// available.
}
}
🐣 Dependents
To use a provided value in a descendant node somewhere, add the IDependent mixin to your descendent node script and mark each dependency with the [Dependency] attribute. The notification method override is used to automatically tell the mixins when your node is ready and begin the dependency resolution process.
Once all of the dependencies in your dependent node are resolved, the OnResolved() method of your dependent node will be called (if overridden).
namespace MyGameProject;
using Chickensoft.Introspection;
using Godot;
[Meta(typeof(IAutoNode))]
public partial class StringDependent : Node
{
public override void _Notification(int what) => this.Notify(what);
[Dependency]
public string MyDependency => this.DependOn<string>
