Services and dependency injection
A game's services live in one dependency injection container, from
Microsoft.Extensions.DependencyInjection. The engine registers its own services in the same
container, so a game gets them the same way it gets its own: by constructor injection, or by asking
the game for them with GetService<T>().
Registering services
Register a game's services in Configure. The method is called once, first thing in Run(), and
it is given the collection the container is built from:
protected override void Configure(IServiceCollection services)
{
services.AddSingleton<SaveGame>();
services.AddTransient<IEnemyFactory, EnemyFactory>();
}
There is no container yet inside Configure, so do not resolve anything there. Resolve from
Initialize onwards.
The registration methods of the other packages, such as AddUI() and AddShaderCompiler(), are
called the same way, from Configure, alongside the game's own.
Resolving services
Inside the game, ask for a service with GetService<T>():
protected override void Initialize()
{
var saves = GetService<SaveGame>();
base.Initialize();
}
GetService<T>() returns null if T is not registered. It works from Initialize onwards.
Any class the container creates gets its dependencies through its constructor. Ask for a service there, rather than resolving it inside the class:
public class EnemySpawner
{
private readonly IContentManager _content;
public EnemySpawner(IContentManager content)
{
_content = content;
}
}
Register EnemySpawner with services.AddSingleton<EnemySpawner>() and it receives the engine's
content manager. A class you create with new gets no injected dependencies; pass them yourself,
or resolve it through the container.
Lifetimes
The container is built with scope validation on, and the game resolves everything from its root provider. Register game services as singletons or transients: a scoped service resolved from the root throws. Most of the engine's services are singletons.
CreateServiceProvider(IServiceCollection) builds the provider from the collection. The default
returns services.BuildServiceProvider(true). Override it to build the provider another way, such
as with a different container. The provider must resolve every registration in the collection,
including the engine's.
What the engine registers
These types are in the container, so a game's classes can take them as constructor parameters or
resolve them with GetService<T>():
| Service | What it is | Covered in |
|---|---|---|
IGame |
The game itself | The game loop |
IGameWindow |
The window | Windows and display |
IDisplayController, IRenderSurface |
VSync, fullscreen and the logical size | Windows and display |
DisplayManager |
The displays and their modes | Windows and display |
ISystemThemeService |
The desktop's light or dark setting | Windows and display |
ILaunchUrls |
Links the game was started or handed | Windows and display |
IGameThread |
Work posted from other threads, run on the game's thread | The game loop |
IGraphics2D, ITextRenderer |
Drawing and text | Drawing |
IOffscreenRenderer, IRenderTargetAllocator |
Render targets | Drawing |
IRenderContextFactory |
Render encoders and render passes | Render passes |
IContentManager |
Textures and fonts | Content |
ShaderManager |
Materials and shader programs | Materials, effects and lighting |
IFullScreenEffectFactory |
Full-screen effects | Materials, effects and lighting |
ILightingManager |
Ambient light and lights | Materials, effects and lighting |
IAudioService |
Sound and music | Audio |
IInputBindingService, IKeyboardManager, IMouseManager |
Input | Input |
IEventAggregator |
Events between parts of the game | Events |
Unless the game registers an ILoggerFactory of its own, the engine registers a null one, and
nothing is logged.
Registering implementations automatically
Some services are registered for you. A class that implements IRunOnInitialization or
IAttachOnGameStart is registered as a singleton and run at startup, as described in
The game loop.
The engine's registration helpers are public, and a game can use them to register every class that implements an interface:
public interface IRule
{
bool Applies(string name);
}
public class LowerCaseRule : IRule
{
public bool Applies(string name) => name == name.ToLowerInvariant();
}
protected override void Configure(IServiceCollection services)
{
services.RegisterImplementationsOf<IRule>();
}
RegisterImplementationsOf<TAbstract>(lifetime) registers each non-abstract class that implements
TAbstract as itself and as TAbstract. A class that is already registered as itself is not
registered again. The default lifetime is transient.
FindAndRegisterImplementations<TAbstract>(lifetime) uses Scrutor to register each class that
implements TAbstract as each interface it implements. It also defaults to transient.
Both search only the assemblies already loaded when they are called, and skip assemblies whose names
begin with System or Microsoft. An assembly that is not yet loaded is not searched.