|
If you haven't heard about dependency injection yet, it's programming style where you
write classes in way that they get any objects they access handed to them instead of
going shopping in your object tree. This promotes decoupling, reusability and allows
you to easily isolate classes for unit testing.
Combined with an Inversion of Control container,
you actually have less work organizing a project and wiring your components up to each
other than if you didn't use dependency injection.
To get started, assume I had written an FpsComponent for displaying my
game's current frames per second with a constructor like this...
public FpsComponent(
IGraphicsDeviceService graphicsDeviceService,
ISharedContentService sharedContentService
) { /* ... */ }
...I could now ask an IoC container for an instance of the FpsComponent
and it would determine that to construct an FpsComponent, it first needs
to construct the component that provides the IGraphicsDeviceService, so
it would look up or create a GraphicsDeviceManager and hand it over to
my constructor.
What this means in practice is that you don't have to pass around global game services
(like that GraphicsDeviceManager) any longer. If you need access to
a service, add it to your constructor and you're done.
And because now all your dependencies are listed neatly in the constructor, it's easy
to write unit tests that mock those services so your components can be
tested in isolation. That's much more test-friendly than letting the
FpsComponent grab the dependencies itself, eg. by handing it your
Game class instance and letting it rummage around for whatever it needs ;-)
For a more in-depth introduction, check out my
Quick
Introduction to Dependency Injection!
Dependency Injection and XNA
Wiring up dependency injection in an XNA game is not so easy. The
GraphicsDeviceManager, for example, wants an instance of the
Game class. But the Game class has to construct
the GraphicsDeviceManager in its constructor, so it can register
its IGraphicsDeviceService before Initialize()
is called and other components try to access it.
The built-in ContentManager and GameServiceContainer
are created by the Game class, so you would have to tell your
dependency injector to create a Game and query its
Services property when a component is dependent upon the
IServiceProvider.
What I've done is take Ninject, wire it up as good as I could and then find
a workaround for the above issues. Which was to explicitely tell Ninject about
the Game instance in the constructor, before the GraphicsDeviceManager
is created (which can then happen cleanly through Ninject).
/// <summary>Binds the provided type to this instance</summary>
/// <param name="kernel">Kernel the binding will be registered to</param>
/// <param name="serviceType">Service to which this instance will be bound</param>
private void bindToThis(IKernel kernel, Type serviceType) {
StandardBinding binding = new StandardBinding(kernel, serviceType);
IBindingTargetSyntax binder = new StandardBinder(binding);
binder.ToConstant(this);
kernel.AddBinding(binding);
}
Next, to get the Game's IServiceProvider and
ContentManager to show up in Ninject, I created two adapters
that depend on the Game class so it looks to Ninject as if the two
adapters were components requiring the Game class, whereas
actually, the adapters delegate their work to the Game class!
private class ServiceProviderAdapter : IServiceProvider {
/// <summary>Initializes a new service provider adapter for the game</summary>
/// <param name="game">Game the service provider will be taken from</param>
public ServiceProviderAdapter(Game game) {
this.gameServices = game.Services;
}
/// <summary>Retrieves a service from the game service container</summary>
/// <param name="serviceType">Type of the service that will be retrieved</param>
/// <returns>The service that has been requested</returns>
public object GetService(Type serviceType) {
return this.gameServices;
}
/// <summary>Game services container of the Game instance</summary>
private GameServiceContainer gameServices;
}
The good thing is that these tweaks are only required because the Game
class didn't anticipate dependency injection. Once in place, Ninject will work in an XNA game
without further complications. So I can just request any GameComponent
from Ninject, and it will happily construct it, without the component even knowing that it's
taking part in an elaborate dependency injection scheme =)
Even better is that Ninject works flawlessly on the XBox 360. It's one of the few
dependency injection frameworks that can cope with the Compact Framework and
selecting the right compilation flags for the XBox 360's XNA Runtime was a breeze.
Example Project
I've put together a small example game that makes use of Ninject to construct a
game component (called FpsComponent):
Ninject XNA Demo Project
This is what you'll find in the example project:
-
Program.cs - Wires up the dependencies and creates the
Game instance using Ninject.
-
MyGame.cs -
Game class which creates the
GraphicsDeviceManager through dependency injection as well as
the FpsComponent.
-
FpsComponent - Example component that displays the
current frame rate. Constructed using dependency injection.
-
Scaffolding/GameModule.cs - Configures the dependencies
(eg. which class provides which services) for Ninject.
-
Scaffolding/NinjectGame.cs - Custom
Game
class from which you need to derive instead to make Ninject work with XNA.
-
Scaffolding/ISharedContentService - An example service
that can be pulled in as a dependency by components that want to access
the game's global
ContentManager.
Ninject builds for PC and XBox 360 are included in the download, as well as an
XBox 360 solution you can use to test things out on your console!
|