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.