07. Services

  03. Features No Comments

What Are Services?

Services are objects that provide services to one or more clients that depend on them. Inity automatically caches a single instance of each service and makes it simple to share that across all clients.

Registering Services

To define a service class, simply add the Service attribute to it.

[Service]
public class InputManager
{
     public bool MoveLeft => Keyboard.current[KeyCode.LeftArrow].isPressed;
     public bool MoveRight => Keyboard.current[KeyCode.RightArrow].isPressed;
}

This results in a single instance of the InputManager class being automatically created and cached behind the scenes.

Services can be plain old class objects or derive from MonoBehaviour or ScriptableObject.

Dependencies Between Services

Services registered using the Service attribute should always have a parameterless default constructor, so that it is possible for the framework to create an instance of it automatically.

If your services need to make use of other services to function, you can implement an IInitializable<T…> interface with its generic types matching the types of other services. The other services will get injected to the client service through its Init method as part of the service initialization process.

[Service]
public class PlayerManager : IInitializable<InputManager>
{
     private InputManager inputManager;
     public void Init(InputManager inputManager) = this.inputManager = inputManager;
}

Unity Events

Your Services, even if they don’t derive from MonoBehaviour, can still receive callbacks during select Unity events by implementing one of the following interfaces:

  1. IAwake – Receive a callback to an Awake function immediately following the initialization of the service instance. The Init functions of all services that implement an IInitializable<T> interface are always executed before any Awake functions are.
  2. IOnEnable – Receive a callback to an OnEnable function following the initialization of the service instance. The Awake functions of all services that implement IAwake are always executed before any OnEnable functions are.
  3. IStart – Receive a callback to a Start function following the initialization of the service instance. The OnEnable functions of all services that implement IOnEnable are always executed before any Start functions are.
  4. IUpdate – Receive callback during the Update event.
  5. IFixedUpdate – Receive callback during the FixedUpdate event.
  6. ILateUpdate – Receive callback during the LateUpdate event.

Registering Services Manually

In addition to registering services automatically using the Service attribute, it is possible to register them manually.

This might be useful for example if you only want to register a service lazily when a certain scene is loaded, or if you need to dynamically swap your services with different ones based on some factors.

To manually register a service use the Service<T>.SetInstance method.

public class InputManager : MonoBehaviour
{
    private void Awake()
    {
        Service<InputManager>.SetInstance(this);
    }
}

Using Services

To automatically receive the InputManager service during initialization you can have your class derive from MonoBehaviour<InputManager>. It will then receive the InputManager object in its Init function, making it possible to assign it to a member variable.

public class Player : MonoBehaviour<InputManager>
{
    private InputManager inputManager;

    protected override void Init(InputManager inputManager)
    {
        this.inputManager = inputManager;
    }
}

This means that the Player component could exist as part of a scene that is loaded or a prefab that is instantiated without any arguments being provided manually, and Init would still get called with the InputManager service.

Note that the Init function will only get automatically called during initialization if all the Init arguments the client expects are services.

If one or more arguments are not services, then you will need to manually provide all the arguments when initializing the client.

For example, consider this client that requires one service object and one object that is not a service:

public class Player : MonoBehaviour<InputManager, Camera>
{
    private InputManager inputManager;
    private Camera firstPersonCamera;

    protected override void Init(InputManager inputManager, Camera firstPersonCamera)
    {
        this.inputManager = inputManager;
        this.firstPersonCamera = firstPersonCamera;
    }
}

You can retrieve the cached instance of any service manually using Service<T>.Instance.

var inputManager = Service<InputManager>.Instance;

Then you need to pass that instance to the client when it is being created.

For example if you are creating the client by instantiating it from a prefab, you can use the Prefab.Instantiate function.

var inputManager = Service<InputManager>.Instance;
var firstPersonCamera = Camera.main;

var playerPrefab = Resources.Load<Player>("Player");
playerPrefab.Instantiate(inputManager, firstPersonCamera);

If the client is a scene object, you can use an Initializer component to provide the arguments to the client.

Or if the client is being built from scratch at runtime, you can use the GameObject.AddComponent function that accepts initialization arguments.

var playerGameObject = new GameObject("Player");
playerGameObject.AddComponent(inputManager, firstPersonCamera);

Services and Interfaces

One big benefit with the service system is that it supports caching and receiving services using an interface as the defining type.

This makes it very easy to decouple your classes from relying on specific concrete classes. This has numerous benefits such as making it easier to create unit tests for your classes and making it much easier to swap your services with other ones midway through development.

To achieve this, first we need to define the interface that all clients objects can use to communicate with the service object.

public interface IInputManager
{
    bool MoveLeft { get; }
    bool MoveRight { get; }
}

Next let’s return to our InputManager class and make it implement the interface, as well as specify in the Service attribute that this service should be accessed via the interface type.

[Service(typeof(IInputManager))]
public class InputManager : IInputManager
{
    public bool MoveLeft => Keyboard.current[KeyCode.LeftArrow].isPressed;
    public bool MoveRight => Keyboard.current[KeyCode.RightArrow].isPressed;
}

After this everything can work exactly the same on the client side, except now instead of referring to the InputManager directly, we use the IInputManager interface instead.

public class Player : MonoBehaviour<IInputManager>
{
    private IInputManager inputManager;

    protected override void Init(IInputManager inputManager)
    {
        this.inputManager = inputManager;
    }
}
[Service]
public class KeyboardInputManager : IInputManager
{
    public bool MoveLeft => Keyboard.current[KeyCode.LeftArrow].isPressed;
    public bool MoveRight => Keyboard.current[KeyCode.RightArrow].isPressed;
}
[Service]
public class GamepadInputManager : IInputManager
{
    public bool MoveLeft => Gamepad.current[GamepadButton.DpadLeft].isPressed;
    public bool MoveRight => Gamepad.current[GamepadButton.DpadRight].isPressed;
}

Reacting To Changing Services

In cases where your services might change throughout the lifetime of the application, and you want to make sure clients of the services are always using the most recently registered services, you can subscribe to the Service<T>.InstanceChanged event and get notified whenever a service instance is changed to a different one.

public class Player : MonoBehaviour<IInputManager>
{
    private IInputManager inputManager;

    protected override void Init(IInputManager inputManager)
    {
        this.inputManager = inputManager;
    }

    private void OnEnable()
    {
        Service<IInputManager>.InstanceChanged += OnInputManagerInstanceChanged;
    }

    private void OnDisable()
    {
        Service<IInputManager>.InstanceChanged -= OnInputManagerInstanceChanged;
    }

    private void OnInputManagerInstanceChanged(IInputManager newInstance)
    {
        inputManager = newInstance;
    }
}

Services and Thread Safety

Instances of services class that have the Service attribute are created during initialization of the game, before the first scene is loaded. After this process has finished the service framework will not make any changes to these service instances. Because the service instances will remain unchanged throughout the applications lifetime by default, accessing services through Service<T>.Instance is a thread safe operation – provided that this only occurs after the initialization process has finished and that you don’t manually change services instance at runtime in your code.

If however you manually change Service instances at runtime using the Service<T>.SetInstance method, accessing the service from other threads using Service<T>.Instance is no longer a safe operation.

Leave a Reply

Your email address will not be published. Required fields are marked *