Skip to content

Services

Mika Notarnicola edited this page Jul 20, 2026 · 10 revisions

Writing Services

A service is a plain C# class that implements IService (or one of the other optional service interfaces) and is added to a Service List asset. This section covers what's required to write one and the optional interfaces that add extra behavior.

public interface IService
{
    void InitService(BootstrapContext context);
}

A service must:

  • Be a concrete class (not abstract, not an interface).
  • Not derive from UnityEngine.Object (no MonoBehaviours or ScriptableObjects).
  • Have a parameterless constructor.

InitService is where setup happens, but it should be limited to self setup: work that only touches this service's own state. Anything that depends on another service should be scheduled as cooperative work instead. See Self Setup and Cooperative Work for more details.

Self Setup and Cooperative Work

InitService runs in priority order (see Initialization Order below), which means a lower-priority service's InitService can run before a higher-priority service has had its own InitService called at all. Anything a service does inside InitService should be self setup: work that only touches its own state and doesn't assume any other service is ready yet.

Work that depends on other services is cooperative work, and belongs in context.Scheduler() calls instead of running inline. Scheduled work doesn't run until every service has finished InitService (the AsyncTaskFlush step of the Bootstrap Flow, so by the time it actually runs, every other service is guaranteed to exist and have finished its own self setup.

[Serializable]
public class SaveDataService : IService
{
    private Dictionary<string, SaveSlot> _slots;

    void IService.InitService(BootstrapContext context)
    {
        // Self setup: only touches this service's own state, safe to run immediately.
        _slots = new Dictionary<string, SaveSlot>();

        // Cooperative work: depends on another service, deferred until every
        // service has finished InitService.
        context.Scheduler.Schedule(LoadSlotsFromDiskAsync);
    }

    private async Awaitable LoadSlotsFromDiskAsync()
    {
        // FileIOService is another service, so it's only safe to look up
        // and use here, not from InitService.
        var fileIO = App.Locate<FileIOService>();

        foreach (string path in fileIO.EnumerateFiles("Save/"))
        {
            byte[] bytes = await fileIO.ReadFileAsync(path);
            SaveSlot slot = SaveSlot.Deserialize(bytes);
            _slots[slot.Id] = slot;
        }
    }
}

Scheduler.Schedule also accepts a plain Action for cooperative work that doesn't need to run asynchronously.

Optional Capability Interfaces

A service can opt into additional behavior with these interfaces. These interfaces below already extend IService, so they're implemented instead of IService, not alongside it:

Interface Purpose
IServiceWithInitPriority Controls where this service falls in init order
IServiceWithCustomBindings Exposes the service under additional types (e.g. an interface it implements), instead of just its declared type
IServiceWithUpdateLoop / IServiceWithLateUpdateLoop / IServiceWithFixedUpdateLoop Per-frame callbacks, without needing a MonoBehaviour

Your services can also implement IDisposable, and they will have their Dispose() method invoked automatically when the AppInstance is quitting or resetting.

Referencing Other Services

Services don't take constructor dependencies on each other. See Accessing Services below for the ways to look one up instead.

Looking up another service from inside InitService itself isn't safe, since that other service may not have run its own InitService yet. Do that lookup from cooperative work scheduled via context.Scheduler() instead, where every service is guaranteed to be ready.

Accessing Services

Any code that needs a service, whether it's another service, game code, or editor tooling, looks it up through the app's active service locator, populated during the Bootstrap Flow's Service Binding step. There are three ways to do that lookup.

Access Methods

App.Locate<T>() and App.TryLocate<T>()

The most direct way to access a service. Locate<T>() throws if the service can't be found. TryLocate<T>(out T service) returns false instead.

MatchmakingService matchmaking = App.Locate<MatchmakingService>();

if (App.TryLocate(out MatchmakingService matchmaking))
{
    matchmaking.StartMatch();
}

Source-Generated Singleton Accessor

Applying Source Generation's [GenerateSingleton] to a service class generates a static accessor directly on that class, itself just a thin wrapper around ServiceRef<T>.Instance.

[GenerateSingleton]
public partial class MatchmakingService : IService
{
    // ...
}

// Elsewhere:
MatchmakingService.Instance.StartMatch();

ServiceRef<T>

A cached wrapper around the same lookup: ServiceRef<T>.Instance and ServiceRef<T>.TryGetInstance(out T instance). The first access resolves and caches the instance; later accesses return the cached value directly instead of hitting the locator again. The cache clears itself automatically whenever the App deinitializes, so it never holds on to a stale instance across a reset or play mode sessions.

MatchmakingService matchmaking = ServiceRef<MatchmakingService>.Instance;

if (ServiceRef<MatchmakingService>.TryGetInstance(out MatchmakingService matchmaking))
{
    matchmaking.StartMatch();
}

Custom Bindings

A service is normally located by its own declared type. A service that implements IServiceWithCustomBindings (see Optional Capability Interfaces) can additionally be located under other types, such as an interface it implements.

Timing

None of these lookups succeed until the Service Binding step has completed. Looking one up any earlier, including from inside another service's own InitService, either throws (Locate<T>()) or fails (TryLocate<T>(out T service), TryGetInstance).

Service Lists

A Service List is the asset that actually declares which services exist. Everything else in Bootstrap (environments, Quick Start setup, edit-mode tooling) ultimately points at one of these to know what to construct.

Creating a ServiceList asset

Assets > Create > Bootstrap > ServiceList

A Service List is just an ordered array of services. On its own it does nothing. It needs to be referenced somewhere before it's ever bootstrapped (see Where They're Used below).

Adding a Service

Each element in the list starts as an empty slot with a Create new IService button. Clicking it opens a searchable dropdown of every concrete type in the project that implements IService, grouped by namespace. Picking one creates an instance and shows its serialized fields inline, the same as any other Unity object. No scene objects or prefabs are involved. Everything lives directly in the asset.

Because entries are added by type name rather than by dragging in a component, adding a new service to the project is as simple as writing a class that implements IService. It then appears in the dropdown automatically.

Initialization Order

By default, services are initialized in the order they appear in the list. A service can override this by implementing IServiceWithInitPriority, which assigns it an explicit InitPriority; lower values run first. This is how, for example, a logging service can guarantee it comes online before every other service that might want to log during its own initialization.

There's no dependency graph or attribute-based ordering. If a service depends on another being ready first, that's expressed by giving it a higher InitPriority than its dependency.

Where They're Used

A Service List isn't bootstrapped on its own. Something has to point at it:

  • A Bootstrap Environment Asset references one Service List as its payload, and is what actually gets bootstrapped in play mode or in a build. Different environments can point at different Service Lists (or even share one), which is how a project can boot a different set of services depending on scene, platform, or build profile.
  • The project's Edit Mode Services setting (see Bootstrap Settings) points at a standalone Service List that's bootstrapped continuously whenever the editor is open and not in play mode, independent of any environment.

At runtime and in play mode, a Service List asset is never bootstrapped directly. It's cloned first, so every boot gets fresh service instances instead of mutating the asset on disk.

Clone this wiki locally