-
Notifications
You must be signed in to change notification settings - Fork 0
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(noMonoBehaviours orScriptableObjects). - 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.
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;
public void 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.
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.
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.
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.
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();
}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();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();
}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.
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).
A Service List is the asset that actually declares which services exist. Everything else in Bootstrap (environments, Bootstrap Wizard setup, edit-mode tooling) ultimately points at one of these to know what to construct.
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).
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.
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.
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 Wizard) 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.