Dependency Injection in Game Dev

thebuzzsaw

Kelly Brown

Posted on August 17, 2020

Dependency Injection in Game Dev

I am a huge fan of dependency injection. It is a central theme in ASP.NET Core, which I use extensively. In recent weeks, I became specifically interested in the dependency injection components themselves and learned how to use them outside of ASP.NET Core. It's remarkably simple to create your own ServiceCollection, populate it with services, and then build a ServiceProvider.

While that's all well and good, web development was the only domain I could think of where dependency injection would help. After all, server applications are long-running programs that need to juggle lots of components and connect everything together. I did not anticipate my interest in dependency injection and my interest in game dev colliding.

Here is an idea of the code powering top level systems in my engine.

using (var sqlite3 = LibraryMapper.Map<ISqlite3>(nativeDll, SqliteExtensions.ResolveName))
using (var stb = LibraryMapper.Map<IStb>(nativeDll, StbExtensions.ResolveName))
using (var al = OpenAlLoader.LoadOpenAl())
using (var audio = new AudioRenderer(al))
using (var sdl = SdlLoader.LoadSdl())
using (var windowManager = new WindowManager(sdl))
{
    // ...
}

So, not only do these systems need to initialize at the beginning, they need to be cleaned up at the end. From there, the systems begin progressively feeding in subsequent systems: AudioRenderer requires IOpenAl.

Originally, I was happy with this approach. I like how explicit and straightforward the code is. The order of events is clear. The object lifetimes are clear. What's not to like?

I started adding new subsystems. The first thing I did was begin replacing all calls to Console.WriteLine with calls to an instance of ILogger. This meant configuring the logging system up front and then injecting the logger into every system that is interested.

using var loggerProvider = new ConsoleLoggerProvider(new LocalConsoleLoggerOptions());
var logger = loggerProvider.CreateLogger("default");

using (var audio = new AudioRenderer(al, logger))
using (var windowManager = new WindowManager(sdl, logger))
{
    // ...
}

This one example wasn't that bad, but I began extracting internal subsystems and injecting them into the necessary main system because I needed access to those subsystems from other main systems. This outer method became somewhat of a nightmare to read and maintain. There were many local variables feeding into many constructors for various systems. I often had to return to the outer method, add stuff, reorder stuff, and come up with a dozen more variable names.

The nice thing about dependency injection frameworks is that they remove the burden of initializing and disposing services in the correct order. So, I decided to look into replacing all this with dependency injection. I was just going to run a little experiment. If I hated it, I could just revert it.

So, I made a service collection...

var services = new ServiceCollection();

...and filled it with stuff...

services
    .AddLogging(builder =>
    {
        builder.SetMinimumLevel(LogLevel.Trace);
        builder.AddConsole(options =>
        {
            options.IncludeScopes = true;
            options.DisableColors = false;
        });
    })
    .AddSqlite3(nativeDll)
    .AddStb(nativeDll)
    .AddOpenAl()
    .AddSdl2()
    .AddSingleton<AudioRenderer>()
    .AddSingleton<WindowManager>()
    .AddSingleton<Renderer>()
    .AddMetricRepository();

...and built a service provider.

// Make sure I didn't mess up.
var options = new ServiceProviderOptions
{
    ValidateScopes = true,
    ValidateOnBuild = true
};

using var serviceProvider = serviceCollection.BuildServiceProvider(options);

Now, I can pull services out, perform additional initialization, and start the game.

var windowManager = serviceProvider.GetRequiredService<WindowManager>();
windowManager.Run();

I gotta say: I'm very happy with the result. I was worried about losing the clarity of performing all these operations manually, but I much prefer the agility I'm afforded with this deferred initialization. Now, if I want to pull a subsystem into another system, it's as simple as adding it to the constructor.

public ImportantSystem(
    ILogger<ImportantSystem> logger,
    ItemSystem itemSystem,
    CraftingSystem craftingSystem)

I do not have to go back to the outer method, call the new constructor, pass the dependencies in, and possibly change the order of initialization. I also gain a new superpower: I can spawn objects and automatically inject all of its dependencies.

var widget = ActivatorUtilities.CreateInstance<Widget>(serviceProvider);

I highly doubt I am the first one to have this idea of using DI in a game engine, but it was satisfying to leverage a new bit of tech and simplify the code. Have you leveraged dependency injection outside of web development before? How did it go?

💖 💪 🙅 🚩
thebuzzsaw
Kelly Brown

Posted on August 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Jawbone Networking Motivation
csharp Jawbone Networking Motivation

June 10, 2023

Dependency Injection in Game Dev
csharp Dependency Injection in Game Dev

August 17, 2020

How to Conditionally Target WinExe
csharp How to Conditionally Target WinExe

November 28, 2019