Tuesday, March 1, 2011

XNA and an IoC Container

In the previous post, we discussed the topic of XNA and Dependency Injection.
Now, let's build on this information to discuss an IoC Container with XNA.

Note: this post includes complete code sample on CodePlex.
Download code sample here.

IoC Container
An IoC Container is a framework component that automatically resolves all dependent references for an object: when an object is constructed, the container will instantiate all dependent objects automatically and injects them into the source object accordingly.

There are many IoC containers available to .NET developers:
However, Ninject is currently the only IoC container that is compatible with the .NET Compact Framework and will work on Windows Phone 7 and Xbox 360.

Example
As an example, let's revise the Going Beyond tutorial to demonstrate XNA and an IoC Container.

Sample
Download the code sample from the previous post; the IoC Container will be added to this code.
All logic to resolve dependent component references can be encapulated into a single object:

IOC CONTAINER
public static class IoCContainer
{
 private static IKernel kernel;

 public static T Resolve<T>()
 {
  if (null == kernel)
  {
   INinjectModule staticModule = new StaticModule();
   INinjectModule[] modules = new[] { staticModule };

   kernel = new StandardKernel(modules);
  }

  return kernel.Get<T>();
 }

 public static void Release()
 {
  if (null == kernel)
  {
   return;
  }

  kernel.Dispose();
  kernel = null;
 }
}
Next, build a module to configure the bindings and manage the lifetime of all component references:
STATIC MODULE
public class StaticModule : NinjectModule
{
 public override void Load()
 {
  Bind<IGameManager>().To<GameManager>().InSingletonScope();
  Bind<ICameraManager>().To<CameraManager>().InSingletonScope();
  Bind<IContentManager>().To<ContentManager>().InSingletonScope();
  Bind<IGameObjectManager>().To<GameObjectManager>().InSingletonScope();
  Bind<IGraphicsManager>().To<GraphicsManager>().InSingletonScope();
  Bind<IScreenManager>().To<ScreenManager>().InSingletonScope();
 }
}
Next, invoke the IoC Container to construct a single instance of the GameManager component:
GAME FACTORY
public static class GameFactory
{
 private static IGameManager gameManager;

 public static IGameManager GetGameManager()
 {
  return gameManager ?? (gameManager = IoCContainer.Resolve<IGameManager>());
 }

 public static void Release()
 {
  IoCContainer.Release();
 }
}
Finally, dispose of the IoC Container when the game exits:
GAME MANAGER
public class GameManager : IGameManager
{
 // Same code as previous post.

 // Exit game.
 public void Exit()
 {
  GameFactory.Release();
 }
}
GAME
public class MyGame : Game
{
 // Same code as previous post.

 protected override void OnExiting(object sender, EventArgs args)
 {
  gameManager.Exit();
  base.OnExiting(sender, args);
 }
}
Execute the updated game code: the output should be identical to the previous post.

Device Factory
In an older post, we discussed the topic of a Device Factory. Essentially, the Device Factory is an abstract base class that contains all game code common to every device, but allows device specific game code to be overridden in the concrete implementation class through polymorphism.

Let's complete the sample by extending the current code base to target all devices currently available in
XNA 4.0: Windows PC, Windows Phone 7 and Xbox 360.

First, build the Device Factory abstract base class and all concrete implementation classes:
DEVICE FACTORY
public abstract class ADeviceFactory
{
 // GraphicsManager.
 public Int32 PreferredBackBufferWidth { get; protected set; }
 public Int32 PreferredBackBufferHeight { get; protected set; }
 public Boolean IsFullScreen { get; protected set; }

 // ContentManager.
 public String RootDirectory { get; protected set; }
}

public class PhoneDeviceFactory : ADeviceFactory
{
 public PhoneDeviceFactory()
 {
  PreferredBackBufferWidth = 800;
  PreferredBackBufferHeight = 480;
  IsFullScreen = true;
  RootDirectory = "Content";
 }
}

public class WorkDeviceFactory : ADeviceFactory
{
 public WorkDeviceFactory()
 {
  PreferredBackBufferWidth = 1280;
  PreferredBackBufferHeight = 720;
  IsFullScreen = false;
  RootDirectory = "Content";
 }
}

public class XboxDeviceFactory : ADeviceFactory
{
 public XboxDeviceFactory()
 {
  PreferredBackBufferWidth = 1280;
  PreferredBackBufferHeight = 720;
  IsFullScreen = false;
  RootDirectory = "Content";
 }
}
Next, build a Device Manager to delegate all work to the Device Factory:
DEVICE MANAGER
public class DeviceManager : IDeviceManager
{
 // DeviceManager has dependency on DeviceFactory.
 private readonly ADeviceFactory deviceFactory;

 public DeviceManager(ADeviceFactory deviceFactory)
 {
  this.deviceFactory = deviceFactory;
 }

 public ADeviceFactory DeviceFactory
 {
  get { return deviceFactory; }
 }
}
Note: build an Input Factory and Input Manager; these will be placeholders available for future posts:
INPUT FACTORY
public abstract class AInputFactory
{
}
public class PhoneInputFactory : AInputFactory
{
}
public class WorkInputFactory : AInputFactory
{
}
public class XboxInputFactory : AInputFactory
{
}
INPUT MANAGER
public class InputManager : IInputManager
{
}
Next, build a module to configure the bindings and manage the lifetime of all device specific components:
DYNAMIC MODULE
 public class DynamicModule : NinjectModule
 {
  public override void Load()
  {
#if WINDOWS_PHONE
   Bind<ADeviceFactory>().To<PhoneDeviceFactory>().InSingletonScope();
   Bind<AInputFactory>().To<PhoneInputFactory>().InSingletonScope();
#elif WINDOWS
   Bind<ADeviceFactory>().To<WorkDeviceFactory>().InSingletonScope();
   Bind<AInputFactory>().To<WorkInputFactory>().InSingletonScope();
#elif XBOX
   Bind<ADeviceFactory>().To<XboxDeviceFactory>().InSingletonScope();
   Bind<AInputFactory>().To<XboxInputFactory>().InSingletonScope();
#else
   throw new ArgumentOutOfRangeException("DynamicModule");
#endif
  }
 }
Next, update the IoC Container:
IOC CONTAINER
public static class IoCContainer
{
 private static IKernel kernel;

 public static T Resolve()
 {
  if (null == kernel)
  {
   INinjectModule staticModule = new StaticModule();
   INinjectModule dynamicModule = new DynamicModule();

   INinjectModule[] modules = new[] { staticModule, dynamicModule };
   kernel = new StandardKernel(modules);
  }

  return kernel.Get();
 }

 public static void Release()
 {
  if (null == kernel)
  {
   return;
  }

  kernel.Dispose();
  kernel = null;
 }
}
Finally, inject all device specific components using constructor injection technique:
CONTENT MANAGER
public class ContentManager : IContentManager
{
 // ContentManager has dependency on DeviceManager.
 private readonly IDeviceManager deviceManager;
 private XnaContentManager content;

 public ContentManager(IDeviceManager deviceManager)
 {
  this.deviceManager = deviceManager;
 }

 // Load all content.
 public void LoadContent(XnaContentManager xnaContent)
 {
  if (null != content)
  {
   return;
  }

  content = xnaContent;
  content.RootDirectory = deviceManager.DeviceFactory.RootDirectory;
  SpaceShipModel = content.Load<Model>("Models/p1_wedge");
 }

 // Unload all content.
 public void UnloadContent()
 {
  if (null == content)
  {
   return;
  }

  content.Unload();
 }

 public Model SpaceShipModel { get; private set; }
}
GRAPHICS MANAGER
public class GraphicsManager : IGraphicsManager
{
 // GraphicsManager has dependency on DeviceManager.
 private readonly IDeviceManager deviceManager;
 private XnaGraphicsDeviceManager graphics;

 public GraphicsManager(IDeviceManager deviceManager)
 {
  this.deviceManager = deviceManager;
 }

 // Initialize all graphics properties.
 public void Initialize(XnaGraphicsDeviceManager xnaGraphics)
 {
  if (null != graphics)
  {
   return;
  }

  graphics = xnaGraphics;
  graphics.PreferredBackBufferWidth = deviceManager.DeviceFactory.PreferredBackBufferWidth;
  graphics.PreferredBackBufferHeight = deviceManager.DeviceFactory.PreferredBackBufferHeight;
  graphics.IsFullScreen = deviceManager.DeviceFactory.IsFullScreen;
  graphics.ApplyChanges();

  GraphicsDevice = graphics.GraphicsDevice;
  SpriteBatch = new SpriteBatch(GraphicsDevice);
 }

 public GraphicsDevice GraphicsDevice { get; private set; }
 public Single AspectRatio { get { return GraphicsDevice.Viewport.AspectRatio; } }
 public SpriteBatch SpriteBatch { get; private set; }
}
Download code sample here.

Summary
The revised Going Beyond example demonstrates how to add an IoC Container to an existing code base. Once the bindings for each game component have been configured, the IoC Container will automatically resolve all dependent references for an object.

Therefore, the code base is now in a testable state: each game component is now able to be tested in isolation: Unit Testing. This will be the topic in the next post.

No comments:

Post a Comment