1. [Service] Attribute

  05. 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 arguments 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 arguments 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 service in its constructor, and that other services also in turn requires the original services in its constructor, then neither object can be constructed:

[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 use the [Service] attribute to register a global service from a component that is loaded as part of a scene.

FindFromScene

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

[Service(FindFromScene = 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 scene.

When FindFromScene is enabled, the LazyInit option is also always enabled by default. This can help make it easier to enter Play Mode with different scenes open in the Editor, without necessarily encountering a service initialization exception if the scene doesn’t contain your scene-based service, if no clients are requesting the service at the time either.

LoadScene

To locate a service from a particular scene, and automatically load said scene if needed, assign the name or build index of the scene to the LoadScene property in the [Service] attribute.

[Service(LoadScene = "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 it will be automatically loaded additively, and then the service will be acquired 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 automatically initialize your scene-based global services.

Note that loading a scene at runtime in Unity is not instantaneous, but only completes at the end of the frame.

However, if you assign to LoadScene the scene that is the first scene in the Scene List in your Build Settings, and don’t set LazyInit nor LoadAsync to true, then Init(args) will actually load said scene synchronously when you start entering Play Mode, and makes sure that objects in that scene are initialized before objects in any other scenes. This can help ensure consistency between builds and the editor, and be very useful if you want to use the Preload Scene architectural pattern.

Additionally, all components whose class derives from MonoBehaviour<T…> support asynchronous initialization, which means that they can automatically wait until scenes containing services that they depend on finish loading.

Reloading Scenes

If you reload a scene containing a service registered using the [Service] attribute, the old instance will not get replaced with the new one in Init(args)’s service cache automatically.

If you need your scene-based global service to support its scene being reloaded, you can either use a Service Tag instead, or manually reregister the service using a component attached to the reloaded scene:

[InitOrder(Category.ServiceInitializer, Order.VeryEarly)]
class PlayerServiceRegisterer : MonoBehaviour
{
   [SerializeField] Player player;

   void Awake() => Service.Set<IPlayer>(player);
}

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(FindFromScene = true, LazyInit = 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.

Load Async

If LoadAsync is set to true in the [Service] attribute, then the service will be initialized using asynchronous methods when that is supported.

[Service(AddressableKey = "UI", LoadAsync = true)]
class UI : MonoBehaviour { }

Using asynchronous initialization can help keep the main thread running more smoothly, with services being loaded over a long period of time in the background.

Here are all the asynchronous initialization methods that Init(args) supports:

  • When LoadAsync is combined with AddressableKey, then Addressables.LoadAssetAsync is used to load the asset.
  • When LoadAsync is combined with ResourcePath, then Resources.LoadAsync is used to load the asset.
  • When LoadAsync is combined with a prefab asset loaded using AddressableKey or ResourcePath, Object.InstantiateAsync is used to instantiate the service.
  • When LoadAsync is combined with a non-prefab asset loaded using AddressableKey or ResourcePath, and Instantiate is set to true, Object.InstantiateAsync is used to instantiate the service.

Instantiate

When you register an asset-based service using AddressableKey or ResourcePath, Init(args) will instantiate a clone of the asset if it is a prefab, and register the asset directly if it is not a prefab (e.g. a scriptable object asset).

If you want to deliver a prefab asset to clients directly, instead of a clone, you can set Instantiate to false.

Similarly, if you want to deliver a clone of a non-prefab asset to clients, instead of the asset itself, you can set Instantiate to true.

[Service(ResourcePath = nameof(PlayerSettings), Instantiate = true)]
class PlayerSettings : ScriptableObject { }

[Service(ResourcePath = "PlayerPrefab", Instantiate = false)]
class Player : MonoBehaviour { }

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 derives from a ServiceInitializer<TService, …> base class or implements a IServiceInitializer<TService, …> interface.
  2. Add the [Service] attribute to the type.
  3. Specify the defining types of the global service as arguments of the [Service] attribute.

The first generic type argument of the IServiceInitializer<> 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 ServiceInitializer<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;
    }
}

Best Practices

  • Consider using the Preload Scene pattern and [Service(LoadScene = name)] for scene-based global services, if you want make sure they are always initialized before any clients in other scenes.
  • If a scene-based service does not exist for the entire lifetime of the application, use a Service Tag or Services component to register it instead of a [Service] attribute.
  • If you want to instantiate prefab-based services synchronously, [Service(ResourcePath = path)] can be used.
  • Consider avoiding using DontDestroyOnLoad to convert scene objects into persistent global services – if the objects should hold any references to other objects in its original scene, or vice versa, those could easily break if the original scene is unloaded or reloaded at some point. For this reason it can be safer to simply never unload the scene(s) that contain global services as a whole, so that all objects in the scene are guaranteed to have the same lifetime.

Leave a Reply

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