Third-Party Services

  10. Advanced No Comments

Global Services

Typically you can register a global service simply by adding the [Service] attribute directly to type of the service:
[Service]
public class SomeService { }
Sometimes, however, you might want to create services from types defined inside third-party assets, in which case making any modifications to their source code could be out of the question.
You can get around this problem using a Service Initializer.

Service Initializer

To register a global service without modifying the source code of the service, you can define a class which derives from a ServiceInitializer<TService...> base class and add the [Service] attribute to it instead:
[Service(typeof(SomeService))]
class SomeServiceInitializer : ServiceInitializer<SomeService> { }
As usual, you can use all the properties that the [Service] attribute has to customize how Init(args) should create or locate the service during initialization.
[Service(typeof(Cinemachine), SceneName = "Services")]
class CinemachineInitializer : ServiceInitializer<Cinemachine> { }
You can also override the InitTarget method to provide custom code for resolving the instance.
For example, if a third-party asset was using the Singleton pattern to lazily instantiate the service when needed, you could implement InitTarget to acquire the instance via the singleton’s static accessor:
[Service(typeof(SomeSingleton), LazyInit = true)]
class SomeSingletonInitializer : ServiceInitializer<SomeSingleton>
{
    public override SomeSingleton InitTarget() => SomeSingleton.Instance;
}
If you need to access some other global services in order to initialize this global service, you can list the defining types of those other services as generic type argument of the base class, and then you will automatically receive them as arguments of the InitTarget method:
[Service(typeof(SomeSingleton), LazyInit = true)]
class SomeSingletonInitializer : ServiceInitializer<SomeSingleton, SomeSingletonSettings>
{
    public override SomeSingleton InitTarget(SomeSingletonSettings settings)
    {
        var instance = SomeSingleton.Instance;
        instance.Settings = settings;
        return instance;
    }
}

If you need to access some other objects which are not global services in order to initialize the global service, you can also make the service initializer derive from ScriptableObject or MonoBehaviour, and make it implement a IServiceInitializer<TService...> interface. You could then use serialized fields (including Any<TValue> fields for extended functionality) to assign any dependencies you need using the Inspector window, and use the properties of the [Service] attribute to tell Init(args) how it should locate the service initializer:

[CreateAssetMenu]
[Service(typeof(SomeSingleton), ResourcePath = "SomeSingletonInitializer")]
class SomeSingletonInitializer : ScriptableObject, IServiceInitializer<SomeSingleton>
{
    [SerializeField] SomeSingletonSettings settings;
    public override SomeSingleton InitTarget()
    {
        var instance = SomeSingleton.Instance;
        instance.Settings = settings;
        return instance;
    }
}

Service Initializer Async

If acquiring the third-party service might not complete immediately, you can also derive from ServiceInitializerAsync<TService...>:

[Service(typeof(SomeService), LoadAsync = true)]
class SomeServiceInitializer : ServiceInitializerAsync<SomeService>
{
    public override async Task<SomeService> InitTargetAsync()
    {
        var loadService = Resources.LoadAsync<GameObject>("SomeService");
        await loadService;
        var service = ((GameObject)loadService.asset).GetComponent<SomeService>();

        var loadDependency = Resources.LoadAsync<GameObject>("SomeDependency");
        await loadDependency;
        service.Dependency = ((GameObject)loadDependency.asset).GetComponent<SomeDependency>();
        return service;
    }
}

Local Services

If you have a third-party object that you want to automatically pass to some clients, but they do not exist for the entirely lifetime of the application, then you can register them as local services instead.

Registering local services from third-party assets works exactly the same way as it does with types that you own: using a Service Tag or a Services component.

If you need to register a plain old C# object as a local service, you can use a Wrapper, and then attach the Service Tag to the Wrapper, or drag-and-drop the Wrapper into a Services component.

If you need to register a local service from a third-party object that only becomes available at runtime, you can create a Value Provider that locates the object, and drag-and-drop the Value Provider into a Services component.

Manual

If all else fails, you can also register third-party objects as global or local services manually in code using Service.Set or Service.AddFor respectively.

One downside with this approach, though, is that then Init(args) won’t know anything about those services existing in Edit Mode. This means that:

  1. The Null Argument Guard can warn you about those services being missing, even if they are going to be available at runtime.
  2. You won’t be able to use click-to-ping in the Inspector to easily locate these services via their clients.
  3. These services won’t have a (Service) label in their header in the Inspector in Edit Mode.

To resolve the first problem, you can attach an Initializer to clients and select Wait For Service from the dropdown of those services.

Leave a Reply

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