Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C#

Simple IoC Container for Unity3d

Rate me:
Please Sign up or sign in to vote.
4.96/5 (20 votes)
21 May 2014CPOL3 min read 28.5K   847   25   3
Implementation of an IoC Container to be able to use Dependency Injection in Unity3d

Introduction

After using Unity3d for some time, I was looking up if there is any IoC-Container-Framework implementation that could be used. I stumbled over the implementation by Sebastiano Mandalà that can be found on his blog here. While this is a very good Framework, I was not totally satisfied by it, because for my small projects, the implementation was too abstract and was split in too many components. Also, coming from the Microsoft world, I often used the IoC-Container-Framework from Prism.Unity for WPF/ASP.NET projects and therefore am accustomed to their syntax.

So I started to implement my own IoC-Container for Unity3d that has a similar syntax as Prism.Unity, based on some ideas of the implementation of Sebastiano Mandalà. If you like a more compact solution and/or are familiar with Prism.Unity, this might be the right framework for you to start with.

About the Implementation

Interfaces

The IoC container implements some interfaces that show the different tasks of the IoC-Container:

  • IIoCContainer - Holds information of registered types
  • IServiceLocator - Can locate a registered implementation of a given type
  • IDependencyInjector - Can inject known dependencies into fields or properties of a given object
C#
/// <summary>
/// Has the task to hold registrations of interfaces and implementations
/// </summary>
public interface IIoCContainer
{
    /// <summary>
    /// Adds the given type to the containers registry.
    /// </summary>
    /// <typeparam name="T">The type to register</typeparam>
    void Register<T>() where T : class;

    /// <summary>
    /// Adds the given interface to the containers registry and links it with an implementation of this interface.
    /// Multiple implementations of the same type are distinguished through a given key.
    /// </summary>
    /// <typeparam name="TInterface">The type of the interface.</typeparam>
    /// <typeparam name="TClass">The type of the class.</typeparam>
    /// <param name="key">Optional.The key to 
    /// distinguish between implementations of the same interface.</param>
    void Register<TInterface, TClass>(string key = null) where TClass : class, TInterface;

    /// <summary>
    /// Adds the given type to the containers registry. The instance of this type will be handled as a singleton.
    /// </summary>
    /// <typeparam name="T">The type to register</typeparam>
    void RegisterSingleton<T>() where T : class;

    /// <summary>
    /// Adds the given instance to the containers registry. This instance will be handled as a singleton.
    /// Multiple implementations of the same type are distinguished through a given key.
    /// </summary>
    /// <typeparam name="T">The type to register</typeparam>
    /// <param name="instance">
    /// The instance that is going to be registered in the container.</param>
    /// <param name="key">Optional.
    /// The key to distinguish between implementations of the same interface.</param>
    void RegisterSingleton<T>(T instance, string key = null) where T : class;

    /// <summary>
    /// Adds the given interface to the containers registry and links it with an implementation of this interface.
    /// The instance of this type will be handled as a singleton.
    /// Multiple implementations of the same type are distinguished through a given key.
    /// </summary>
    /// <typeparam name="TInterface">The type of the interface.</typeparam>
    /// <typeparam name="TClass">
    /// The type of the class.</typeparam>
    /// <param name="key">Optional.
    /// The key to distinguish between implementations of the same interface.</param>
    void RegisterSingleton<TInterface, TClass>(string key = null) where TClass : class, TInterface;
}

/// <summary>
/// Has the task to locate a registered instance of the given type.
/// </summary>
public interface IServiceLocator
{
    /// <summary>
    /// Resolves an instance of the given type.
    /// Multiple implementations of the same type are distinguished through a given key.
    /// </summary>
    /// <typeparam name="T">The type of the wanted object.</typeparam>
    /// <param name="key">Optional.
    /// The key to distinguish between implementations of the same interface.</param>
    /// <returns>An instance of the given type.</returns>
    T Resolve<T>(string key = null) where T : class;

    /// <summary>
    /// Resolves an instance of the given type.
    /// Multiple implementations of the same type are distinguished through a given key.
    /// </summary>
    /// <param name="type">The type of the wanted object.</param>
    /// <param name="key">Optional.
    /// The key to distinguish between implementations of the same interface.</param>
    /// <returns>An instance of the given type.</returns>
    object Resolve(Type type, string key = null);
}

/// <summary>
/// Has the task to provide dependencies of the given object.
/// </summary>
public interface IDependencyInjector
{
    /// <summary>
    /// Injects public properties or fields that are marked with the 
    /// Dependency attribute with the registered implementation.
    /// </summary>
    /// <param name="type">The type of the object.</param>
    /// <param name="obj">The object whose dependencies should be injected.</param>
    /// <returns>The injected object</returns>
    object Inject(Type type, object obj);

    /// <summary>
    /// Injects public properties or fields that are marked with the 
    /// Dependency attribute with the registered implementation.
    /// </summary>
    /// <typeparam name="T">The type of the object</typeparam>
    /// <param name="obj">The object whose dependencies should be injected.</param>
    /// <returns>The injected object</returns>
    T Inject<T>(object obj);
}

The Data Class

The container uses the following class to hold the information of a registered type. The class has a factory method called Create that gathers and stores all public fields and properties with the [Dependency] attribute set.

C#
private class TypeData
{
    /// <summary>
    /// Holds the instance of an already created singleton.
    /// </summary>
    public object Instance { get; set; }

    /// <summary>
    /// Holds all properties that need to be injected
    /// </summary>
    public List<KeyValuePair<DependencyAttribute, PropertyInfo>> Properties { get; private set; }

    /// <summary>
    /// Holds all fields that need to be injected
    /// </summary>
    public List<KeyValuePair<DependencyAttribute, FieldInfo>> Fields { get; private set; }

    /// <summary>
    /// Gets a value indicating whether this type should be handled as a singleton.
    /// </summary>
    public bool IsSingleton { get; private set; }

    private TypeData()
    {
        this.Properties = new List<KeyValuePair<DependencyAttribute, PropertyInfo>>();
        this.Fields = new List<KeyValuePair<DependencyAttribute, FieldInfo>>();
    }

    /// <summary>
    /// Creates a new TypeData an gets all information needed to instantiate the given type.
    /// </summary>
    public static TypeData Create(Type type, bool isSingleton = false, object instance = null)
    {
        var typeData = new TypeData { IsSingleton = isSingleton, Instance = instance };

        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
        {
            var dependency =
                (DependencyAttribute)field.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
            if (dependency == null) continue;

            typeData.Fields.Add(new KeyValuePair<DependencyAttribute, FieldInfo>(dependency, field));
        }

        foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            var dependency =
                (DependencyAttribute)property.GetCustomAttributes(typeof(DependencyAttribute), true).FirstOrDefault();
            if (dependency == null) continue;

            typeData.Properties.Add(new KeyValuePair<DependencyAttribute, PropertyInfo>(dependency, property));
        }

        return typeData;
    }
}

The Container

The container needs two dictionaries. One to connect interfaces to their implementations and the other to connect the implementations with their type data.

C#
/// <summary>
/// Holds all information of the registered types.
/// </summary>
private readonly Dictionary<Type, Dictionary<string, Type>> 
types = new Dictionary<Type, Dictionary<string, Type>>();

/// <summary>
/// The type containers
/// </summary>
private readonly Dictionary<Type, TypeData> typeDatas = new Dictionary<Type, TypeData>();

These dictionaries are used in the private Register method to store the type information.

C#
private void Register(Type interfaceType, Type type, TypeData typeData, string key = null)
{
    try
    {
        if (this.types.ContainsKey(interfaceType ?? type))
        {
            this.types[interfaceType ?? type].Add(key ?? string.Empty, type);
        }
        else
        {
            this.types.Add(interfaceType ?? type, new Dictionary<string, Type> { { key ?? string.Empty, type } });
        }

        this.typeDatas.Add(type, typeData);
    }
    catch (Exception ex)
    {
        throw new IoCContainerException("Register type failed.", ex);
    }
}

After registering your classes, the types can be resolved. If the type to resolve is derived from MonoBehavior, the script is created through calling AddComponent on a GameObject.

C#
/// <summary>
/// Resolves an instance of the given type.
/// Multiple implementations of the same type are distinguished through a given key.
/// </summary>
/// <param name="type">The type of the wanted object.</param>
/// <param name="key">Optional.
/// The key to distinguish between implementations of the same interface.</param>
/// <returns>
/// An instance of the given type.
/// </returns>
public object Resolve(Type type, string key = null)
{
    Guard(!this.types.ContainsKey(type), "The type {0} is not registered.", type.Name);

    Guard(!this.types[type].ContainsKey(key ?? string.Empty),
        "There is no implementation registered with the key {0} for the type {1}.", key, type.Name);

    var foundType = this.types[type][key ?? string.Empty];

    var typeData = this.typeDatas[foundType];

    if (foundType.IsSubclassOf(typeof(MonoBehaviour))) // this is the unity3d specific part
    {
        Guard(this.singletonGameObjectName == null,
            "You have to set a game object name to use for MonoBehaviours with SetSingletonGameObject() first.");

        // places a new empty game object in the game if not found
        var gameObject = GameObject.Find(this.singletonGameObjectName)
            ?? new GameObject(this.singletonGameObjectName);

        // when the game already has the wanted component attached, return it.
        // if that is not the case add the component to the object and inject possible dependencies.
        return gameObject.GetComponent(type.Name) ?? Inject(foundType, gameObject.AddComponent(foundType));
    }

    if (typeData.IsSingleton)
    {
        // if an instance already exists, return it.
        // if that is not the case setup a new instance and inject all dependencies.
        return typeData.Instance ?? (typeData.Instance = this.Setup(foundType));
    }

    return this.Setup(foundType);
}

Using the Code

IoC-Container Setup

To set up the IoC-Container, a few step have to be made. To give an example, I will show you how to setup a Unity3d Project with it.

First, create a new Unity3d-Project and copy the Framework folder into the Assets folder:

Image 1

Next, you have to implement the AbstractBootstrapper.cs. This class will be the entry point for the IoC-Container. You have to override the Configure method, where you put all your services/components you want to register.

C#
public class Bootstrapper : AbstractBootstrapper
{
    public override void Configure(IIoCContainer container)
    {
        // non  singleton
        container.Register<IColorItem, ColorItem>();

        // singletons

        // multiple  implementations
        container.RegisterSingleton<IColorFactory, RedColorFactory>("red");
        container.RegisterSingleton<IColorFactory, GreenColorFactory>("green");
        container.RegisterSingleton<IColorFactory, BlueColorFactory>("blue");

        // monobehaviour
        container.RegisterSingleton<IColorHistory, ColorHistory>();
    }
}

Next, create an empty GameObject and give it a name (e.g., Container) and add the Bootstrapper as Component:

Image 2 Image 3

Now, we need to force Unity to execute the Bootstrapper as the first MonoBehaviour at application start. This is done as the following:

Left-click on any script in your project to bring up the following view in the inspector and click "Execution Order...":

Image 4

The inspector view will change. Click the plus-icon and select the Bootstrapper. Done.

Image 5

Injecting Dependencies into MonoBehaviour Scripts

If you want to inject your dependencies, all you need to do is to mark the corresponding public properties or fields with the [Dependency] attribute and call the method Inject in Start. The Inject method is an extension method that comes with the IoC-Container implementation.

C#
public class ColorDropper : MonoBehaviour
{

    [Dependency("red")]     public IColorFactory RedColorFactory;
    [Dependency("blue")]    public IColorFactory BlueColorFactory;
    [Dependency("green")]   public IColorFactory GreenColorFactory;

    void Start () {
        this.Inject();
        this.StartCoroutine(this.DropColor());
    }

Points of Interest

Example Project

The example project you can download shows only how to use the container in different ways. The implementation of the example itself may not be the way it would be actually done.

My Opinion

What I like the most about using an IoC-Container in Unity is, that it gives you a clearer guide on how you get to components of other GameObjects. You don't need to think about using ways like GameObject.Find (to mention the most inefficient way) to get to them.

It also shows you very clearly what your scripts depend on.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Austria Austria
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionExcellent article. Pin
Member 1286215915-Mar-20 9:12
Member 1286215915-Mar-20 9:12 
QuestionInteresting idea Pin
Sven Bardos16-Jan-17 23:02
Sven Bardos16-Jan-17 23:02 
AnswerRe: Interesting idea Pin
Clemens Pfauser20-Jul-17 8:36
Clemens Pfauser20-Jul-17 8:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.