1. [Service] Attribute

  5. Services No Comments

Defining a Global Service

You can define a global service in Init(args) by simply adding the [Service] attribute to any class:

[Service]
class GameManager { }

This will instruct Init(args) to automatically create and cache a single instance of the the class when the project starts.

Ready-To-Use Before Scene Load

All global services are constructed and initialized before any of the objects in the initial scene become active. As such, clients can acquire and make use of global services during the Awake and OnEnable events without having to worry about execution order issues.

All global services are also automatically initialized in optimal order based on their dependencies, so you don’t have to worry about execution order issues in this case either.

MonoBehaviour<T…> Injection

A global service is automatically delivered to all components in the project that derive from MonoBehaviour<T…>, with one its generic type parameters matching the service type.

class Player : MonoBehaviour<GameManager>
{
     GameManager gameManager;
     protected override void Init(GameManager gameManager) => this.gameManager = gameManager;

     void OnEnable() => gameManager.RegisterPlayer(this);
     void OnDisable() => gameManager.UnregisterPlayer(this);
}

Service-To-Service Injection

A global service is also automatically delivered to all other global services that implement IInitializable<T…>, with one the generic type parameters matching the service type, or contain a constructor with a parameter type matching the service type.

[Service]
class InputManager : IInitializable<GameManager>
{
     public void Init(GameManager gameManager) => Debug.Log($"InputManager received {gameManager}.");
}

[Service]
class AudioManager
{
     public AudioManager(GameManager gameManager) => Debug.Log($"AudioManager received {gameManager}.");
}

Note that this automatic delivery of services to other services only works if all Init method / constructor parameters are global service types.

Also note that if you use constructors for receiving global services, you need to make sure not to have any circular dependencies between your services. If you have a service that requires another services to be created, and that other services also in turn requires the original services to be created, then neither object can be created:

[Service]
class InputManager
{
     public InputManager(GameManager gameManager) => Debug.Log($"InputManager received {gameManager}.");
}

[Service]
class GameManager
{
     public GameManager(InputManager inputManager) => Debug.Log($"GameManager received {inputManager}.");
}

The same thing also applies even if the circular dependency consists of a longer chain of services. E.g. if A’s constructor requires B, B’s constructor requires C, and C’s constructor requires A, then none of the global services can be created.

IInitializable<T…> does not have the same limitation; if A implements IInitializable<B>, and B implements IInitializable<A>, both global services can still be created.

Service Defining Type

When you register a class, let’s say InputManager, as a global service by adding the [Service] attribute on it, all clients that depend on an InputManager will be able to receive it automatically.

Okay, but what if you don’t want to couple all your client components to this one specific concrete class? Maybe you want to use a different implementation in some contexts, such as when creating a build for particular platforms, or when testing a client in a unit test.

A good way to achieve this, is by making your client components depend an interface type, instead of the concrete type of a specific service.

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

class Player : MonoBehaviour<IInputManager>
{
     protected override void Init(IInputManager inputManager) => Debug.Log($"Player received {inputManager}.");
}

You can register a global service with a defining type other than the concrete type of the class, by specifying the type in the constructor of the [Service] attribute.

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

Note that the defining type of a service must satisfy one of the following constraints:

  1. Is the concrete type of the class.
  2. Is an interface that the class implements.
  3. Is a base type from which the concrete type derives.

You can also register a service using more than one defining type, by listing multiple types in the [Service] attribute’s constructor.

[Service(typeof(InputManager), typeof(IInputManager))]
class InputManager : IInputManager

You can make use of preprocessor directives to register a different class as a global service depending on the current platform.

#if UNITY_STANDALONE
[Service(typeof(IInputManager))]
#endif
class KeyboardInputManager : MonoBehaviour, IInputManager
{
    public bool MoveLeft => Keyboard.current[KeyCode.LeftArrow].isPressed;
    public bool MoveRight => Keyboard.current[KeyCode.RightArrow].isPressed;
}
#if !UNITY_STANDALONE
[Service(typeof(IInputManager))]
#endif
class GamepadInputManager : MonoBehaviour : IInputManager
{
    public bool MoveLeft => Gamepad.current[GamepadButton.DpadLeft].isPressed;
    public bool MoveRight => Gamepad.current[GamepadButton.DpadRight].isPressed;
}

Asset Services

Init(args) makes it easy to create global services from prefabs and scriptable object assets in your project.

Two different methods of loading service assets are currently supported: Resources folders and Addressables.

Resources

To load a service from a Resources folder, assign the name of the asset to the ResourcePath property on the [Service] attribute.

[Service(ResourcePath = "Player")]
class Player : MonoBehaviour { }

Addressables

To load a service using the Addressable Asset System, assign the addressable key of the asset to the AddressableKey property on the [Service] attribute.

[Service(AddressableKey = "PlayerSettings")]
class PlayerSettings : ScriptableObject { }

Note that you must have the Addressables package installed in your project in order to use this functionality.

Scene Services

It is also possible to user the [Service] attribute to register a global service from a component that exists in a scene.

FindFromScene

To locate a service from any of the loaded scenes, set FindFromScene to true in the [Service] attribute.

[Service(FindFromScene = true, LazyInit = true)]
class UIManager : MonoBehaviour { }

This can be a viable option for registering services, if your game consists of only a single main scene, or if you always start your game from the same “Preload” or “Boostrap” scene

Note that when you set FindFromScene to true, by default Init(args) will attempt to locate the service from the initial scene immediately when you launch a build or enter Play Mode in the Editor. This might make it difficult to quickly test different scenes in your game, as you could only enter Play Mode with the particular scene containing your scene services open. For this reason it’s almost always a good idea to also set LazyInit to true if you use FindFromScene.

SceneName

To locate a service from a particular scene, assign the name of the scene to the Scene property in the [Service] attribute.

[Service(SceneName = "Services")]
class UIManager : MonoBehaviour { }

If the scene is not already loaded when the service is requested (which happens immediately when the game starts, unless LazyInit is set to true), then Init(args) will automatically load the scene, and acquire the service from it. This can help with testing the game in the Editor, because you can enter Play Mode from any scene, and Init(args) can still be able to initialize all your scene-based global services.

Note that loading a scene in Unity is not instantaneous, but only happens during the next frame. Because of this you might need to add an Initializer to clients that depend on scene based services, and select Wait For Service from the dropdown menu of the scene-based Init arguments. This will delay the initialization of the client and keep it in a disabled state, until after all the services in question have become ready.

Lazy Init

If you set LazyInit to true in the [Service] attribute, then instead of the service being initialized the moment that the game starts, it will only be lazily initialized when the first client requires it.

[Service(LazyInit = true, FindFromScene = true)]
class UIManager : MonoBehaviour { }

If your game contains a large number of global services with heavy computation occurring during their initialization, then setting LazyInit to true on your services could also help reduce the initial loading time of your game, and instead spreading it happen over a longer period of time.

Service Providers

It is also possible to add the [Service] attribute to a value provider that provides a service, instead of the class of the service itself.

This opens up the possibility to implement the value provider in such a way that it returns a different service every time that one is requested, rather than always returning the same globally shared instance:

[Service(typeof(Name))]
class RandomNameProvider : IValueProvider<Name>
{
   public Name Value => Random.Range(0, 10) switch
   {
      0 => "Alice",
      1 => "Bob",
      2 => "Charlie",
      3 => "David",
      4 => "Eve",
      5 => "Frank",
      6 => "Grace",
      7 => "Hank",
      8 => "Ivy",
      _ => "Jack"
   };
}

If you implement the IValueProvider<TValue>.TryGetFor method, you can also make the value provider return different types of services based on the particular clients that are requesting them.

For example, the example value provider below returns instances of different Visuals prefabs based on the id of the client that is requesting it:

[Service(typeof(Visuals), ResourcePath = "VisualsProvider"), CreateAssetMenu]
sealed class VisualsProvider : ScriptableObject, IValueProvider<Visuals>, ISerializationCallbackReceiver
{
   [Serializable]
   sealed class Entry
   {
      public EntityId id = default;
      public Visuals visuals = default;
   }

   [SerializeField] Entry[] entries = default;

   readonly Dictionary<EntityId, Visuals> visualsById = new();

   Visuals IValueProvider<Visuals>.Value => null;

   public bool TryGetFor(Component client, out Visuals visuals)
   {
      if(client.TryGetComponent(out Entity entity) && visualsById.TryGetValue(entity.Id, out var visualsPrefab))
      {
         visuals = Instantiate(visualsPrefab, client.transform);
         return true;
      }

      visuals = null;
      return false;
   }

   public void OnBeforeSerialize() { }

   public void OnAfterDeserialize()
   {
      visualsById.Clear();
      foreach(var entry in entries)
      {
         visualsById[entry.id] = entry.visuals;
      }
   }
}

Service Initializer

Service initializers can be used to register a global service, but instead of Init(args) automatically creating the service for you, you can handle its creation manually in code. This can be useful in situations where your services depend on some objects that are not registered as global services, or if the initialization process is so complicated that it can’t be automated.

To create a service initializer:

  1. define a type that implements a IServiceProvider<TService, …> interface (or derives from the ServiceProvider<TService, …> base class).
  2. Add the [Service] attribute to the type.
  3. Specify the defining types of the service that the service provider initializes as arguments of the [Service] attribute.

The first generic type argument of the IServiceProvider<> interface should be the concrete type of the service that the service initializer handles initializing.

After the first argument you can list zero or more other global services that the initialized service depends on. These will get delivered to the InitTarget method automatically, and can be used to initialize the service.

Manual Initialization

[Service(typeof(IPlayer), ResourcePath = "PlayerInitializer"), CreateAssetMenu]
class PlayerInitializer : ScriptableObject, IServiceInitializer<Player, IInputManager>
{
   [SerializeField] Player prefab;
   [SerializeField] PlayerConfig config = default;
   [SerializeField] Any<string> initialName = default; // Use Any<string> to support localization, randomization etc.

   // This service initializer receives the global service IInputManager from Init(args),
   // and uses it and additional configuration provided via the Inspector to initialize the Player service.
   public Player InitTarget(IInputManager inputManager)
   {
      var player = prefab.Instantiate(config, inputManager);
      prefab.name = initialName;
      return player;
}

class Player : MonoBehaviour<PlayerConfig, IInputManager>, IPlayer
{
   PlayerConfig config;
   IInputManager inputManager;

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

Unlike value providers that have the [Service] attribute, service initializers are only used to create the service once when the game is first loading, and that same instance will be delivered to all clients that request it.

Automatic Initialization

If you derive from ServiceProvider<TService>, and don’t override the InitTarget method with a custom implementation, then Init(args) will automatically create the service instance for you.

[Service(typeof(IPlayer))]
class PlayerInitializer : ServiceInitializer<Player> { }

This will only be possible if the target service is a component, scriptable object, has a parameterless constructor, or has a constructor with every argument being a global service.

This has the same effect as adding the [Service] attribute directly to the service class, but it might be a better option in some situations:

  1. if you can’t add the attribute to the service class directly (for example, because it belongs to a third party package),
  2. if you would prefer to keep the service class decoupled from Init(args),
  3. or if you want to group all service initialization logic together inside a single file.

Lifetime Events

Global services can receive callbacks during select lifetime events, even if they don’t derive from MonoBehaviour, by implementing one of the following interfaces:

  1. IAwake – Awake is executed 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 – OnEnable is executed 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 – Start is executed 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.
  7. IOnDisable – Receive callback when the application is quitting or when exiting play mode in the editor.
  8. IOnDestroy – Receive callback when the application is quitting or when exiting play mode in the editor.

Additionally if your service class implements the IDiposable interface then IDispose will be called for it when the application is quitting or when exiting play mode in the editor.

Registering Services Manually In Code

In addition to registering services automatically using the [Service] attribute, it is possible to register them manually in code as well.

This might be useful for example if you want to dynamically swap your services with different ones based on some conditional logic.
For example you could swap the KeyboardInputManager to GamepadInputManager when the user changes their input method in the settings.

To manually register a global service use the Service.Set method.

To make sure that the service gets registered before the Awake method on any clients components or their initializers is executed, you can add the InitOrder attribute with Category set to ServiceInitializer to the component that does the registering.

[InitOrder(Category.ServiceInitializer)]
public class InputManager : MonoBehaviour, IInputManager
{
    void OnEnable() => Service.Set<IInputManager>(this);
    void OnDisable() => Service.Unset<IInputManager>(this);
}

To manually register a local service limited to only some clients based on their location in the scene hierarchies, use the Service.AddFor method.

[InitOrder(Category.ServiceInitializer)]
public class Player : MonoBehaviour
{
    void Awake() => Service.AddFor(Clients.InChildren, this);
    void OnDestroy() => Service.RemoveFrom(Clients.InChildren, 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.Get<T>() or Service.GetFor<T>(Object).

var inputManager = Service.Get<InputManager>();

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.Get<InputManager>();
var firstPersonCamera = Camera.main;

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

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);

If the client is a scene object, you can generate an Initializer to provide the arguments to the client.
When using Initializers all service arguments are passed to the client automatically, so you only need to assign the other arguments using the Inspector.

Reacting To Changing Services

In cases where your services might change throughout the lifetime of the application, you may want to make sure clients of the services are always using the most recently registered services.

To do this you can subscribe receive callbacks a service instance changes to a different, using the Service.AddInstanceChangedListener method.

When you do this you should also remember to unsubscribe using Service.RemoveInstanceChangedListener when the listener gets destroyed.

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

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

    private void OnEnable()
    {
        Service.AddInstanceChangedListener(OnInputManagerInstanceChanged);
    }

    private void OnDisable()
    {
        Service.RemoveInstanceChangedListener(OnInputManagerInstanceChanged);
    }

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

Leave a Reply

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