Awesome Unity: Dependency Injection through ScriptableObjects

Daniel Tan
GlassBlade
Published in
5 min readMay 17, 2020

Introduction

Dependency Injection

Dependency Injection(DI), also known as Inversion of Control, is something most people have heard of. It is basically where the parent classes passes everything the child classes needs to instantiate itself. This goes hand-in-hand with the “Composition over Inheritance” principle, so classes are loosely coupled with each other.

Simple example would be a Car class, which has a reference to the RedWheels class. Now what if there’s a need to use BlueWheels instead of RedWheels? Changing the wheels can be quite a debugging nightmare. You do not want to waste your time in this self-made hell. Using dependency injection allows us to have a buffer layer in-between that injects the required wheels for us.

public interface ICar {}public interface IWheels {}public class RedWheels : IWheels {}public class BlueWheels : IWheels {}// Example of self-made hell
public class RedCar : ICar {
public IWheels Wheels;
public RedCar() {
// changing this won't be nice to other
// classes expecting red wheels.
Wheels = new RedWheels();
}
}
public class RedCar: ICat {
public IWheels Wheels;
public RedCar(IWheels wheels) {
// simple change, major difference.
// red car now only need any wheels instead
// of red wheels. Same for its dependencies
Wheels = wheels;
}
}

Unity was actually built with visual DI built-in from the ground up and we can build a simple, effective DI “framework” using nothing but Unity-provided objects. This is especially important to us since games often have very complex, asynchronous state, so we should make debugging as painless as possible.

ScriptableObjects

ScriptableObjects are Unity Objects that can be serialized as a file. That means your classes can be saved, with pre-injected data, as a file in your project. This is a far cry from code-only DI frameworks with complicated configuration code that requires advanced IDE support before it becomes excessively messy to navigate through.

With ScriptableObjects , to pre-inject data all you need to do is drag and drop your data. This is called "Referencing" the data.

Example of a simple easy way to configure movement directions for different unit classes in my board game.

ScriptableObjects are also known as “Assets”, which is basically everything that’s saved on disk in your project folder. In-game “Scene Objects” on the other hand, are not saved on disk except in the scene file, and only exist in game. So, “Assets” cannot reference other “Scene Objects” but it can reference other “Assets”. “Scene Objects” does not have this limitation.

Actual Implementation

We’ll create three different components.

  1. Services: This contains actual game state logic.
  2. View/Controllers: This contains input and output logic.
  3. Data: This contains in-game shared configuration data.

Services

This is an on-disk file. First, you can create a base class like so:

public class BaseService : ScriptableObject {
public virtual void Initialize() {}
}

Then in your game logic processor, you need to do two things. Inherit the class, and use the CreateAssetMenu attribute.

[CreateAssetMenu(fileName = "board_service", menuName = "Services/BoardService")]
public class BoardService : BaseService
{
// logic
}

CreateAssetMenu allows you to have a custom menu in the Create option.

In our Service class, we will have some sort of event emitter using UnityEvents or Events or UniRx. This will be covered in a future post.

You can compose services as well, which really shows how DI shines in allowing easier debugging.

Example: My board game has two players. One would be a local player, and the other is a networked player. But during development, obviously I want both players to be local. How do I do that?

Just drag and drop the local input service to the other player’s input.

It’s almost too easy to be true.

View/Controller

In my Controller MonoBehaviour , I have a reference to the class I've created, and in my code I listen to the events emitted by the Service.

Special mention: If you’re using a state machine like what I’ve shown in a previous article I wrote, then you can do the following.

Prepare an initialization step in your state machine.

Reference the service in your state behaviour.

Data

This will be a typical ScriptableObject as well.

[CreateAssetMenu(fileName = "direction_", menuName = "Config/DirectionData", order = 0)]
public class DirectionData : ScriptableObject
{
public Vector2Int Offset;
}

What this also allows is hot-switching between different functionality and data in Play Mode, which is great for dynamically testing something.

I try to update weekly. This is part two of the Awesome Unity Series.

Part one here.

--

--