Dependency Injection (DI)
Starting with version 2.0, RTS Starter Kit moved away from singletons (like the old GameController.Instance) towards Dependency Injection. Instead of a script reaching out to a global instance to get what it needs, its dependencies (other services, settings, etc.) are given to it automatically by the framework — you just declare a field, and it gets filled in for you.
This is why you see code like this everywhere in the API Reference and tutorials:
class SomeSystem : IInitSystem
{
[Inject] GameplayService gameplay;
[Inject] SelectionService selection;
public void Init()
{
selection.SelectionStarted += OnSelectionStarted;
}
void OnSelectionStarted()
{
// gameplay and selection are already filled in here, no manual lookup needed.
}
}
The DI mechanism itself is not part of RTS Starter Kit — it comes from a small, separate open-source library, InsaneOne.Core.Injection, which RTS Starter Kit uses under the hood. You don’t need to reference or install anything extra, it’s already included with the asset — but if you want to see exactly how it works internally, that repository is the source of truth.
How it works
The [Inject] attribute marks a field (or a method) that should receive a dependency:
[Inject] GameplayService gameplay;
At some point during initialization, the framework’s Container goes through all registered objects (called targets) and, for every [Inject]-marked field, finds a registered dependency whose type matches the field type and assigns it. You don’t call anything to make this happen in your own gameplay scripts — as long as your class is picked up by the game’s bootstrap as a system/service, its [Inject] fields will already be set by the time your Init()/Start() runs.
Under the hood, roughly:
var container = new Container(InjectionType.All);
container.AddDependency(gameplayServiceInstance); // registers a dependency by its runtime type
container.AddDependencyAs<ISomeInterface>(someSystemInstance); // ...or registers it under a specific interface/base type instead
container.AddTarget(someSystemInstance); // marks an object as needing injection
container.Resolve(); // fills in [Inject] fields/methods on all added targets
This part is handled by the asset’s own bootstrap code (see the Boot partition) — as an asset user, you normally only write the consumer side (the [Inject] fields), not the registration/bootstrap side.
Named bindings
If you ever need more than one dependency of the same type, you can bind a dependency to a specific id, and request that exact one with [Inject("id")]:
container.AddDependency(firstInstance).Bind("first");
container.AddDependency(secondInstance).Bind("second");
[Inject("first")] SomeType first;
[Inject("second")] SomeType second;
Unity integration
For MonoBehaviour-based objects, there is UnityContainer (derived from Container), which knows how to inject freshly spawned/instantiated objects. A MonoBehaviour opts in to this by implementing the marker interface IRequireInject:
public class SomeUnitComponent : MonoBehaviour, IRequireInject
{
[Inject] GameplayService gameplay;
}
// somewhere in spawning code
var instance = unityContainer.Spawn(prefab, position, rotation); // instantiates the prefab and injects all IRequireInject components on it
If your IRequireInject component was created some other way (not through Spawn), you can inject it directly:
unityContainer.ApplyTo(gameObject); // finds all IRequireInject components on the GameObject and injects them
Note
If a class should not receive field injection at all (for example, it only uses method injection), it can implement INoFieldsInject to opt out of field scanning.