Skip to content

Repository files navigation

Dependency Injection & Service Locator for Unity

A lightweight, ScriptableObject-based dependency injection framework for Unity. Register services with attributes, resolve them asynchronously, inject dependencies with one line of code, and let ServiceKit handle the lifecycle automatically.

Unity 2022.3+License: MIT

Support

If you like my work then please consider showing your support for ServiceKit by giving the repo a star or buying me a brew

Buy Me A Coffee

Installation

Add Service Kit to your Unity project via Package Manager:

  1. Open Window > Package Manager
  2. Click + > Add package from git URL
  3. Enter:
https://www.pkglnk.dev/servicekit.git

pkglnk

Features

  • Attribute-Based Registration: Use [Service(typeof(IFoo))] to declare service types - supports multiple interfaces per service.
  • ScriptableObject-Based: Clean, asset-based architecture that integrates seamlessly with Unity's workflow.
  • Multi-Phase Initialization: A robust, automated lifecycle ensures services are registered, injected, and initialized safely.
  • Async Service Resolution: Wait for services to become fully ready with cancellation and timeout support.
  • Atomic 3-State Resolution: Each optional-dependency check is a single lock-guarded atomic operation distinguishing ready, registered-but-not-ready, and absent services (the optional-wait flow re-checks atomically across frames to catch late registrations).
  • UniTask Integration: Automatic performance optimization when UniTask is available - greatly reduced allocations and faster async operations.
  • Fluent Dependency Injection: Elegant builder pattern for configuring service injection.
  • Automatic Scene Management: Services are automatically tracked and cleaned up when scenes unload.
  • Comprehensive Debugging: Built-in editor window with search, filtering, and service inspection.
  • Type-Safe: Full generic support with compile-time type checking.
  • Performance Optimized: Efficient service lookup with minimal overhead, enhanced further with UniTask.
  • Concurrency-Hardened: The registry is lock-guarded and the async awaiter path is race-condition-hardened (atomic 3-state resolution, registration guards). Note that ServiceKit assumes the Unity main thread for registration/lifecycle and integrates with Unity's APIs accordingly — it is not a general-purpose thread-safe container for arbitrary cross-thread use.

What's New in V2

For the narrative version of this release (including why ServiceKit leans into being a service locator on purpose), see the launch post: ServiceKit V2, The Async Service Locator for Unity.

Simpler Service Declarations

V2 replaces the generic ServiceKitBehaviour<T> base class with a non-generic ServiceKitBehaviour plus a [Service] attribute. This eliminates generic type parameter noise from class declarations, inheritance chains, and constraint clauses.

Before (V1):

// Generic parameter threaded through every level of the hierarchypublicabstractclassBaseGameController<TInterface>:ServiceKitBehaviour<TInterface>,IGamewhereTInterface:class,IGame
public class BowlingController :BaseGameController<IBowlingController>,IBowlingController

After (V2):

// Clean inheritance, registration intent is explicitpublicabstractclassBaseGameController:ServiceKitBehaviour,IGame[Service(typeof(IBowlingController))]
public class BowlingController :BaseGameController,IBowlingController

For abstract base classes with multiple generic parameters (e.g., a service type and a data type), only the ServiceKit type parameter is removed — functional generics are preserved:

// Before: two generics, one was just for ServiceKitpublicabstractclassScoreService<TService,TScore>:ServiceKitBehaviour<TService>,IScoreService<TScore>whereTService:class,IScoreService<TScore>whereTScore:struct// After: one generic remains — the one that actually matters
public abstract class ScoreService<TScore>: ServiceKitBehaviour,IScoreService<TScore>whereTScore:struct[Service(typeof(IMyScoreService))]
public class MyScoreService :ScoreService<int>, IMyScoreService

In real-world migrations this reduces hundreds of lines of generic boilerplate while making each class declaration immediately readable.

One-Line Dependency Injection

The new InjectAsync extension method replaces the common 4-method builder chain with a single call:

Before:

await_serviceKitLocator.Inject(this).WithErrorHandling().WithTimeout().ExecuteWithCancellationAsync(destroyCancellationToken);

After:

await_serviceKitLocator.InjectAsync(this,destroyCancellationToken);

This applies default timeout (from ServiceKit Settings), the provided cancellation token, and default error handling — the configuration that 90%+ of injection call sites need. The full builder is still available when you need custom timeout values or error handlers:

// Custom configuration when defaults aren't enoughawait_serviceKitLocator.Inject(this).WithTimeout(10f).WithErrorHandling(ex =>HandleMyError(ex)).ExecuteWithCancellationAsync(destroyCancellationToken);

Atomic Service Resolution

The new TryResolveService method replaces the error-prone two-call pattern of TryGetService followed by IsServiceRegistered with a single atomic check:

varstatus=locator.TryResolveService(typeof(IMyService),outvarservice);switch(status){caseServiceResolutionStatus.Ready:// service is populatedbreak;caseServiceResolutionStatus.RegisteredNotReady:// registered, still initializingbreak;caseServiceResolutionStatus.NotRegistered:// does not existbreak;}

Both checks happen under a single lock, eliminating the race window where a service could register between the two calls.

Race Condition Hardening

  • GetServiceAsync — Task forwarding is now set up inside the lock, preventing a race where the shared TaskCompletionSource could complete before forwarding was established
  • UseLocatorInterlocked.CompareExchange registration guard prevents double-registration when UseLocator is called concurrently with Awake
  • Circular Dependency Detection — Uses Type references instead of string name matching, preventing false matches between types with similar names
  • DontDestroyOnLoad detection — Strengthened to require both scene name and buildIndex == -1

Roslyn Analyzers

RuleSeverityDescription
SK003Error[Service(typeof(IFoo))] on a class that doesn't implement IFoo
SK005ErrorServiceKitBehaviour subclass overrides Awake() without calling base.Awake()

These catch at compile time what would otherwise be silent runtime failures. SK003 restores the type safety that the old generic pattern provided, while SK005 prevents the most common lifecycle mistake.

Quick Start

1. Create a ServiceKit Locator

Right-click in your project window and create a ServiceKit Locator: Create > ServiceKit > ServiceKitLocator

2. Define Your Services

publicinterfaceIPlayerService{voidSavePlayer();voidLoadPlayer();intGetPlayerLevel();}publicclassPlayerService:IPlayerService{privateint_playerLevel=1;publicvoidSavePlayer()=>Debug.Log("Player saved!");publicvoidLoadPlayer()=>Debug.Log("Player loaded!");publicintGetPlayerLevel()=>_playerLevel;}

3. Register Services

Plain C# services — register in a bootstrap:

publicclassGameBootstrap:MonoBehaviour{[SerializeField]privateServiceKitLocator_serviceKit;privatevoidAwake(){// Fluent API - register and ready in one chain_serviceKit.Register(newPlayerService()).As<IPlayerService>().Ready();// Multi-type registration - one instance, multiple interfaces_serviceKit.Register(newAudioManager()).As<IAudioService>().As<IMusicService>().WithTags("audio","core").Ready();}}

MonoBehaviour services — use ServiceKitBehaviour and place them in the scene. Registration, injection, and readiness are all handled automatically:

[Service(typeof(IPlayerController))]publicclassPlayerController:ServiceKitBehaviour,IPlayerController{[InjectService]privateIPlayerService_playerService;protectedoverridevoidInitializeService(){// Called after all dependencies are injected and ready_playerService.LoadPlayer();}}

4. Inject Services

publicclassPlayerUI:MonoBehaviour{[SerializeField]privateServiceKitLocator_serviceKit;// Mark fields for injection[InjectService]privateIPlayerService_playerService;privateasyncvoidAwake(){// One-liner: default timeout, cancellation, and error handlingawait_serviceKit.InjectAsync(this,destroyCancellationToken);// Or configure each option with the builder// await _serviceKit.Inject(this)// .WithTimeout(5f)// .WithCancellation(destroyCancellationToken)// .WithErrorHandling()// .ExecuteAsync();_playerService.LoadPlayer();}}

5. Create MonoBehaviour Services with ServiceKitBehaviour

For services that need to be MonoBehaviours, use the ServiceKitBehaviour base class with the [Service] attribute:

// Single interface registration[Service(typeof(IPlayerController))]publicclassPlayerController:ServiceKitBehaviour,IPlayerController{[InjectService]privateIPlayerService_playerService;protectedoverridevoidInitializeService(){// Called after dependencies are injected_playerService.LoadPlayer();Debug.Log("Player controller ready!");}}// Multiple interface registration - register one instance under multiple types[Service(typeof(IAudioService),typeof(IMusicService))]publicclassAudioManager:ServiceKitBehaviour,IAudioService,IMusicService{publicvoidPlaySound(stringid){/* ... */}publicvoidPlayMusic(stringid){/* ... */}}// No attribute = registers as concrete typepublicclassGameSettings:ServiceKitBehaviour{publicintVolume{get;set;}// Accessible via: serviceKit.GetService<GameSettings>()}

UniTask Integration

ServiceKit provides automatic optimization when UniTask is installed in your project. UniTask is a high-performance, zero-allocation async library specifically designed for Unity.

⚠️ WebGL requires UniTask

On most platforms UniTask is an optional performance upgrade. On WebGL it is required. WebGL has no thread pool, so the default System.Threading.Tasks path cannot resume an awaiter that is waiting for a not-yet-ready service — the injection silently hangs (only injections whose dependencies are already ready complete). UniTask is player-loop based and resumes correctly, so any project targeting WebGL must install UniTask. ServiceKit logs a warning at startup in a WebGL build that does not have it. (Validated on an IL2CPP WebGL player: the await-then-resume path fails on Task and passes on UniTask.)

Automatic Detection

ServiceKit automatically detects when UniTask is available and seamlessly switches to use UniTask APIs for enhanced performance:

// Same code, different performance characteristics:awaitserviceKit.GetServiceAsync<IPlayerService>();// With UniTask installed: → Fewer allocations, faster execution// Without UniTask: → Standard Task performance

Installing UniTask

Add UniTask to your Unity project via Package Manager:

  1. Open Window > Package Manager
  2. Click + > Add package from git URL
  3. Enter:
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#2.5.10

ServiceKit's SERVICEKIT_UNITASK define activates at UniTask 2.5.10 or newer. The #2.5.10 suffix pins that minimum; omit it to track the latest.

Performance Benefits

When UniTask is available, ServiceKit automatically provides:

  • 🚀 Faster Async Operations: Especially for operations that complete immediately
  • 📉 Less Memory Allocation: Reduced GC pressure and frame drops
  • ⚡ Lower-Allocation Async: UniTask itself is allocation-free; ServiceKit's async path allocates far less than on the Task path (not strictly zero — a small per-call buffer remains)
  • 🎯 Unity-Optimized: Better main thread synchronization and PlayerLoop integration

Usage Examples

The same ServiceKit code works with both Task and UniTask - no changes needed:

[Service(typeof(IPlayerController))]publicclassPlayerController:ServiceKitBehaviour,IPlayerController{[InjectService]privateIPlayerService_playerService;[InjectService]privateIInventoryService_inventoryService;// Automatically uses UniTask when available for better performanceprotectedoverrideasyncUniTaskInitializeServiceAsync(){await_playerService.LoadPlayerDataAsync();await_inventoryService.LoadInventoryAsync();}}

Multiple service resolution is also optimized:

// UniTask.WhenAll is more efficient than Task.WhenAllvar(player,inventory,audio)=awaitUniTask.WhenAll(serviceKit.GetServiceAsync<IPlayerService>(),serviceKit.GetServiceAsync<IInventoryService>(),serviceKit.GetServiceAsync<IAudioService>());

Best Practices with UniTask

  • Mobile Games: UniTask's zero-allocation benefits are most noticeable on mobile devices
  • Complex Scenes: Projects with many services see the biggest improvements
  • Frame-Critical Code: Use for smooth 60fps gameplay where every allocation matters
  • Memory-Constrained Platforms: VR, WebGL, and older devices benefit significantly

Roslyn Analyzer Support

ServiceKit includes integrated support for Roslyn Analyzers to help you write better code with real-time analysis and suggestions specifically tailored for ServiceKit development.

Features

The ServiceKit Analyzers provide:

  • Code analysis for common ServiceKit patterns and best practices
  • Real-time suggestions to improve your service implementations
  • Compile-time warnings for potential issues with dependency injection
  • Code fixes to automatically resolve common problems

Installation

ServiceKit includes a built-in tool to download and manage the Roslyn Analyzers:

  1. Open the ServiceKit Settings window: Edit > Project Settings > ServiceKit
  2. Navigate to the Developer Tools section
  3. Click Download Analyzers to automatically fetch the latest version from GitHub
  4. The analyzers will be installed to Assets/Analyzers/ServiceKit/

Manual Installation

You can also manually download the analyzers:

  1. Visit the ServiceKit Analyzers releases page
  2. Download the latest ServiceKit.Analyzers.dll
  3. Place it in Assets/Analyzers/ServiceKit/ in your Unity project
  4. Unity will automatically recognize and apply the analyzers

Managing Analyzers

Through the ServiceKit Settings window, you can:

  • Update: Download the latest version to get new analysis rules and improvements
  • Remove: Uninstall the analyzers if you no longer need them
  • View Details: See the installed version, file size, and last modified date

Contributing to Analyzers

The ServiceKit Analyzers are open source! If you'd like to contribute new analysis rules or improvements:

  • Visit the ServiceKit Analyzers repository
  • Check out the contribution guidelines
  • Submit issues for bugs or feature requests
  • Create pull requests with your improvements

The analyzer repository includes documentation on:

  • How to build custom analyzers for ServiceKit
  • Adding new diagnostic rules
  • Creating code fix providers
  • Testing analyzer implementations

Advanced Usage

Using ServiceKitBehaviour Base Class

For the most robust and seamless experience, inherit from ServiceKitBehaviour and use the [Service] attribute to specify which interface(s) your service implements. This base class automates a sophisticated multi-phase initialization process within a single Awake() call, ensuring that services are registered, injected, and made ready in a safe, deterministic order.

Key Features:

  • Attribute-based registration: Use [Service(typeof(IFoo), typeof(IBar))] to register against multiple types
  • Concrete type fallback: If no [Service] attribute is provided, the service registers against its concrete class type
  • Simplified generics: No more confusing generic inheritance chains

It handles the following lifecycle automatically:

  1. Registration: The service immediately registers itself against all declared types, making it discoverable.
  2. Dependency Injection: It asynchronously waits for all services marked with [InjectService] to become fully ready.
  3. Custom Initialization: It provides InitializeServiceAsync() and InitializeService() for you to override with your own setup logic.
  4. Readiness: After your initialization, it marks the service as ready for all registered types, allowing other services that depend on it to complete their own initialization.
// Single interface registration[Service(typeof(IPlayerController))]publicclassPlayerController:ServiceKitBehaviour,IPlayerController{[InjectService]privateIPlayerService_playerService;[InjectService]privateIInventoryService_inventoryService;// This is the new hook for your initialization logic.// It's called after dependencies are injected, but before this service is marked as "Ready".protectedoverridevoidInitializeService(){// Safe to access injected services here_playerService.LoadPlayer();_inventoryService.LoadInventory();Debug.Log("Player controller initialized with all dependencies!");}// For async setup, you can use the async override:// Note: Returns UniTask when available, Task otherwise - same code works for both!protectedoverrideasyncUniTaskInitializeServiceAsync(){// Example: load data from a web request or fileawait_inventoryService.LoadFromCloudAsync(destroyCancellationToken);}// Optional: Handle injection failures gracefullyprotectedoverridevoidHandleDependencyInjectionFailure(Exceptionexception){Debug.LogError($"Failed to initialize player controller: {exception.Message}");// The exception is typed: a timeout arrives as ServiceInjectionTimeoutException (a// TimeoutException) and an unregistered service as ServiceUnregisteredException (an// OperationCanceledException). Both subclass their base type, so the checks below keep// working; both also carry a .Kind discriminator (ServiceInjectionFailureKind:// Timeout, CallerCanceled, ServiceUnregistered, TargetDestroyed) if you need to branch precisely.if(exceptionisTimeoutException){Debug.Log("Services took too long to become available");}elseif(exceptionisServiceInjectionException){Debug.Log("Required services are not registered or failed to become ready");gameObject.SetActive(false);// Disable this component}}}// Multi-type registration - register under multiple interfaces[Service(typeof(IAudioService),typeof(IMusicService),typeof(ISoundEffects))]publicclassUnifiedAudioManager:ServiceKitBehaviour,IAudioService,IMusicService,ISoundEffects{publicvoidPlaySound(stringid){/* ... */}publicvoidPlayMusic(stringid){/* ... */}publicvoidStopAll(){/* ... */}}// Concrete type registration (no attribute needed)publicclassGameSettings:ServiceKitBehaviour{publicintVolume{get;set;}// Registers as typeof(GameSettings)}

Fluent Registration API

For non-MonoBehaviour services, use the fluent registration API for clean, chainable configuration:

publicclassGameBootstrap:MonoBehaviour{[SerializeField]privateServiceKitLocator_serviceKit;privatevoidAwake(){// Simple registration_serviceKit.Register(newPlayerService()).As<IPlayerService>().Ready();// Multi-type registration - one instance accessible via multiple interfaces_serviceKit.Register(newUnifiedAudioManager()).As<IAudioService>().As<IMusicService>().As<ISoundEffects>().Ready();// With tags for organization and filtering_serviceKit.Register(newAnalyticsService()).As<IAnalyticsService>().WithTags("analytics","third-party").Ready();// Circular dependency exemption_serviceKit.Register(newEventBus()).As<IEventBus>().WithCircularExemption().Ready();// Register without making ready (for deferred initialization)_serviceKit.Register(newNetworkService()).As<INetworkService>().Register();// Not ready yet - call ReadyService<INetworkService>() later}}

Fluent API Methods:

MethodDescription
.As<T>()Register the service under interface type T (can chain multiple)
.WithTags(...)Add string or ServiceTag tags for filtering
.WithCircularExemption()Exempt from circular dependency detection
.Register()Complete registration (not ready, not injectable yet)
.Ready()Complete registration and mark as ready (injectable)

Asynchronous Service Resolution

Wait for services that may not be immediately available (or ready):

publicclassLateInitializer:MonoBehaviour{[SerializeField]privateServiceKitLocator_serviceKit;privateasyncvoidStart(){try{// Wait up to 10 seconds for the service to be registered AND readyvaraudioService=await_serviceKit.GetServiceAsync<IAudioService>(newCancellationTokenSource(TimeSpan.FromSeconds(10)).Token);audioService.PlaySound("welcome");}catch(OperationCanceledException){Debug.LogError("Audio service was not available or ready within the timeout period");}}}

Optional Dependencies

Services can be marked as optional using intelligent 3-state dependency resolution:

publicclassAnalyticsReporter:MonoBehaviour{[SerializeField]privateServiceKitLocator_serviceKit;[InjectService(Required=false)]privateIAnalyticsService_analyticsService;// Optional - stays null if never registered[InjectService]privateIPlayerService_playerService;// Required - injection fails if missingprivateasyncvoidAwake(){// Injection is what populates the fields; a plain MonoBehaviour must trigger it itself// (a ServiceKitBehaviour does this for you). After this, _analyticsService may be null.await_serviceKit.InjectAsync(this,destroyCancellationToken);}}

When Required = false, ServiceKit uses intelligent 3-state resolution via an atomic TryResolveService check:

  • Service is ready → Inject immediately
  • Service is registered but not ready → Wait for it (treat as required temporarily)
  • Service is not registered → Skip injection (field remains null)

All three states are determined in a single lock-guarded operation, eliminating race conditions between the check and the resolution. This means you don't need to predict whether a service will be available — the system automatically waits for registered services that are "coming soon" while skipping services that will "never come."

When Required = true (default):

  • Always wait for the service regardless of registration status
  • Timeout and fail if service is not available within the specified timeout period

Exempting Services from Circular Dependency Checks

In advanced scenarios, you might need to bypass the circular dependency check. This is useful for two main reasons:

  1. Wrapping Third-Party Code: When you "shim" an external library into ServiceKit, that service has no knowledge of your project's classes. A circular dependency check is unnecessary and can be safely bypassed.
  2. Managed Deadlocks: In rare cases, a "manager" service might need a reference to a "subordinate" that also depends back on the manager. If you can guarantee this cycle is not accessed until after full initialization, an exemption can resolve the deadlock.

To handle this, use the CircularDependencyExempt property on the [Service] attribute. This should be used with extreme caution, as it bypasses a critical safety feature.

// Example of a service that needs to be exempted from circular dependency checks[Service(typeof(ISubordinateService),CircularDependencyExempt=true)]publicclassSubordinateService:ServiceKitBehaviour,ISubordinateService{[InjectService]privateIManagerService_manager;// The service will be registered without being considered in the dependency graph analysis// This allows it to participate in circular dependencies safely}

Using ServiceKit with Addressables

ServiceKit fully supports Unity's Addressables system, allowing you to load ServiceKitLocator assets on-demand. However, there are critical considerations regarding how Unity handles ScriptableObjects in Addressable scenes.

Making a ServiceKitLocator Addressable

To use an addressable ServiceKitLocator:

  1. Check the Addressable checkbox on the ServiceKitLocator asset
  2. Add the ServiceKitLocator asset to an addressable group
  3. Load the locator like any other addressable asset
// Example: Loading an addressable ServiceKitLocatorvarhandle=Addressables.LoadAssetAsync<ServiceKitLocator>("MyServiceKitLocator");awaithandle.Task;varserviceKitLocator=handle.Result;

Critical: ScriptableObject Instance Behavior

Understanding this behavior is crucial for proper ServiceKit functionality in addressable setups.

When an addressable scene references a ScriptableObject, Unity's behavior differs based on whether the ScriptableObject itself is addressable:

Non-Addressable ServiceKitLocator (referenced by addressable scene):

  • Unity creates a new instance of the ServiceKitLocator
  • The new instance is embedded in the scene's asset bundle
  • Services registered outside the addressable scene will not be present in this new instance
  • This may be desirable if you want complete isolation between addressable scenes

Addressable ServiceKitLocator (referenced by addressable scene):

  • Unity uses the same instance across all scenes
  • No new instance is created
  • Services registered outside the addressable scene remain registered
  • This maintains a global service registry across all scenes

Recommendations

Use Non-Addressable ServiceKitLocator when:

  • You want complete isolation between addressable scenes
  • Each scene should have its own independent service registry
  • Services should not persist between scene loads

Use Addressable ServiceKitLocator when:

  • You want a global service registry across all scenes
  • Services should persist when loading/unloading addressable scenes
  • You need services registered in the bootstrap scene available in addressable scenes
// Example: Bootstrapping with addressable ServiceKitLocatorpublicclassAddressableBootstrap:MonoBehaviour{privateasyncvoidStart(){// Load the addressable ServiceKitLocatorvarlocatorHandle=Addressables.LoadAssetAsync<ServiceKitLocator>("GlobalServiceKitLocator");awaitlocatorHandle.Task;varserviceKitLocator=locatorHandle.Result;// Register global servicesvaraudioService=newAudioService();serviceKitLocator.RegisterService<IAudioService>(audioService);serviceKitLocator.ReadyService<IAudioService>();// Now load addressable scenes - they will reference the same ServiceKitLocator instance// and have access to the IAudioServiceawaitAddressables.LoadSceneAsync("GameplayScene").Task;}}

Service Lifecycle in Addressable Scenes

Important: ServiceKitBehaviours registered in a scene are automatically unregistered and destroyed when that scene is unloaded. This applies to both regular and addressable scenes.

To preserve a ServiceKitBehaviour beyond the lifetime of its scene:

Option 1: Use DontDestroyOnLoad

[Service(typeof(IPersistentService))]publicclassPersistentService:ServiceKitBehaviour,IPersistentService{protectedoverridevoidInitializeService(){// Prevent this service from being destroyed when the scene unloadsDontDestroyOnLoad(gameObject);}}

Option 2: Load scenes additively

// Load scenes additively to keep previous scene services activeawaitAddressables.LoadSceneAsync("AdditiveScene",LoadSceneMode.Additive).Task;

Best Practices:

  • Use DontDestroyOnLoad for global services that should persist across scene transitions (e.g., audio, save system, analytics)
  • Use additive scene loading when you need services from multiple scenes active simultaneously
  • Be mindful of memory usage when keeping services alive - unload scenes explicitly when no longer needed
  • For addressable scenes, consider whether the service should be tied to the scene's lifetime or persist globally

ServiceKit Debug Window

Access the debugging interface via Tools > ServiceKit > ServiceKit Window:

Features:

  • Real-time Service Monitoring: View all registered services across all ServiceKit locators.
  • Readiness Status: See at a glance whether a service is just registered or fully ready.
  • Scene-based Grouping: Services organized by the scene that registered them, with DontDestroyOnLoad services shown separately.
  • Tag Visualization: Service tags displayed inline for quick identification.
  • Search & Filtering: Find services quickly by name or tag.
  • Script Navigation: Click to open service implementation files.
  • GameObject Pinging: Click MonoBehaviour services to highlight them in the scene.

API Reference

IServiceKitLocator Interface

// Fluent Registration (recommended)IServiceRegistrationBuilderRegister<T>(Tservice)whereT:class;IServiceRegistrationBuilderRegister(objectservice);// Direct Registration// (registration methods also take a trailing optional [CallerMemberName] string registeredBy,// auto-filled for debug attribution - you normally omit it)voidRegisterService<T>(Tservice)whereT:class;voidRegisterService(TypeserviceType,objectservice);voidRegisterAndReadyService<T>(Tservice)whereT:class;voidReadyService<T>()whereT:class;voidUnregisterService<T>()whereT:class;// Synchronous AccessTGetService<T>()whereT:class;boolTryGetService<T>(outTservice)whereT:class;// Atomic 3-State ResolutionServiceResolutionStatusTryResolveService(TypeserviceType,outobjectservice);// Returns Ready, RegisteredNotReady, or NotRegistered — single lock, no race conditions// Asynchronous Access (automatically uses UniTask when available)Task<T>GetServiceAsync<T>(CancellationTokencancellationToken=default)whereT:class;// Returns UniTask<T> when UniTask package is installed// Dependency InjectionIServiceInjectionBuilderInject(objecttarget);// Tag QueriesIReadOnlyList<ServiceInfo>GetServicesWithTag(stringtag);IReadOnlyList<ServiceInfo>GetServicesWithAnyTag(paramsstring[]tags);IReadOnlyList<ServiceInfo>GetServicesWithAllTags(paramsstring[]tags);// ManagementIReadOnlyList<ServiceInfo>GetAllServices();// Curated subset. The interface also exposes status checks (IsServiceRegistered<T>,// IsServiceReady<T>, GetServiceStatus<T>), tag mutators (AddTagsToService, RemoveTagsFromService,// GetServiceTags), and scene tooling (GetServicesInScene, UnregisterServicesFromScene,// CleanupDestroyedServices). See IServiceKitLocator.cs for the full surface.

IServiceRegistrationBuilder Interface

IServiceRegistrationBuilderAs<T>()whereT:class;// Register as interface typeIServiceRegistrationBuilderAs(TypeserviceType);// Register as runtime typeIServiceRegistrationBuilderWithTags(paramsstring[]tags);// Add tagsIServiceRegistrationBuilderWithTags(paramsServiceTag[]tags);IServiceRegistrationBuilderWithCircularExemption();// Exempt from circular dependency checkvoidRegister();// Complete registration (not ready yet)voidReady();// Complete registration and mark as ready

IServiceInjectionBuilder Interface

IServiceInjectionBuilderWithCancellation(CancellationTokencancellationToken);IServiceInjectionBuilderWithTimeout();// Use the default timeout (30s, from ServiceKit Settings)IServiceInjectionBuilderWithTimeout(floattimeoutSeconds);IServiceInjectionBuilderWithErrorHandling();// Use the default handler (logs against the target)IServiceInjectionBuilderWithErrorHandling(Action<Exception>errorHandler);TaskExecuteAsync();// Awaitable (UniTask when available)TaskExecuteWithCancellationAsync(CancellationTokentoken);// Awaitable, applies the cancellation token// Fire-and-forget Execute() / ExecuteWithCancellation() also exist on the concrete// ServiceInjectionBuilder for advanced use; prefer the awaitable ExecuteAsync above.

Convenience Extensions

ServiceKitExtensions adds ergonomic helpers on top of IServiceKitLocator:

// One-line injection (default timeout + cancellation + error handling)awaitserviceKit.InjectAsync(this,destroyCancellationToken);// Check whether a ready service existsif(serviceKit.HasService<IPlayerService>()){/* ... */}// Run an action only if the service is available (no-op otherwise)serviceKit.WithService<IAudioService>(audio =>audio.Play(clip));// ...or return a value, with a fallback when the service is absentvarvolume=serviceKit.WithService<IAudioService,float>(audio =>audio.Volume,1f);// Register from a factory, or from an async source (Task<T> / UniTask<T>)serviceKit.RegisterServiceFactory<IPlayerService>(()=>newPlayerService());awaitserviceKit.RegisterServiceAsync(LoadPlayerServiceAsync());

Locator Interface Facets

IServiceKitLocator is composed from four smaller interfaces, so code (and alternative locator implementations) can depend on only the slice it needs:

InterfaceResponsibility
IServiceLocatorCore: register, ready, resolve, inject
IServiceTagRegistryTag assignment and tag-based queries
IServiceSceneManagerScene-scoped enumeration and cleanup
IServiceDiagnosticsInspection, status, circular-dependency state

IServiceKitLocator inherits all four, so existing code is unaffected. To point a ServiceKitBehaviour at your own locator, override ResolveLocator():

protectedoverrideIServiceKitLocatorResolveLocator()=>MyCustomLocator.Instance;

ServiceKit Settings

Create a settings asset via Assets > Create > ServiceKit > Settings (loaded from a Resources folder at runtime, auto-discovered in the editor):

SettingDefaultEffect
DefaultTimeout30Seconds WithTimeout() waits before failing
AutoCleanupOnSceneUnloadtrueUnregister scene MonoBehaviour services when their scene unloads
WarnOnDestroyedRegistrationtrueLog a warning when a destroyed object is registered
DebugLoggingfalseVerbose registration/ready logging (editor)
DefaultServiceKitLocatorLocator used for auto-assignment; takes highest priority

Best Practices

Service Design

  • Use interfaces for service contracts to maintain loose coupling.
  • Keep services stateless when possible for better testability.
  • Prefer composition over inheritance for complex service dependencies.

Registration Strategy

  • Register early in the application lifecycle. ServiceKitBehaviour automates this in Awake.
  • Initialize wisely. Place dependency-related logic in InitializeService or InitializeServiceAsync when using ServiceKitBehaviour.
  • Global services should be registered in persistent scenes or DontDestroyOnLoad objects.

Dependency Management

  • Mark dependencies as optional when they're not critical for functionality.
  • Use timeouts for service resolution to avoid indefinite waits.
  • Handle injection failures gracefully with proper error handling.
  • Avoid circular dependency exemptions unless absolutely necessary and the lifecycle is fully understood.
  • Use TryResolveService when you need to atomically distinguish ready, registered-not-ready, and absent services without race conditions.

Performance Optimization

  • Install UniTask for automatic performance improvements in async operations.
  • Use async initialization in InitializeServiceAsync() for I/O operations to avoid blocking the main thread.
  • Batch service resolution when possible using UniTask.WhenAll() or Task.WhenAll().
  • Profile on target platforms - UniTask benefits are most noticeable on mobile and lower-end devices.

Indicative Performance

The tables below are indicative timings from a single Unity Editor session (development build) on one machine — a rough guide, not a bundled or reproducible benchmark. Release builds are faster, and absolute numbers vary widely by hardware, so treat these as ballpark figures and profile your own target platform.

Service Resolution

OperationAverage TimeThroughput
TryGetService0.004ms245,700 ops/sec
IsServiceRegistered0.005ms220,614 ops/sec
IsServiceReady0.007ms147,477 ops/sec
GetService (sync)0.010ms103,000 ops/sec
GetServiceAsync0.018ms54,789 ops/sec
GetServicesWithTag0.026ms38,493 ops/sec

Service Registration

OperationAverage TimeThroughput
RegisterService0.594ms1,686 ops/sec
RegisterAndReadyService1.196ms837 ops/sec
Complete lifecycle (register + inject + ready)1.722ms581 ops/sec
Register 10 services17.152ms58 ops/sec
Register 50 services91.096ms11 ops/sec

Stress Tests

OperationAverage Time
1000x sync resolution2.763ms
100x async resolution16.413ms
50 concurrent accessors x 20 services36.818ms
1000x register/unregister cycle1867.780ms

All core operations are well within frame budget for 60fps+ applications. ServiceKit does not bundle a benchmark suite — use the Unity Profiler on your target platform to measure your own setup.

Performance Tips

// Fastest: TryGetService for hot-path accessif(serviceKit.TryGetService<IPlayerService>(outvarservice)){service.Update();}// Prefer sync GetService when you know the service is readyvarplayer=serviceKit.GetService<IPlayerService>();// Use async only when the service may not be ready yetvarplayer=awaitserviceKit.GetServiceAsync<IPlayerService>();// Install UniTask for zero-allocation async and better Unity thread integration

Migration Guide

Migrating from v1.x to v2.0

V2.0 is a major release that replaces the generic ServiceKitBehaviour<T> pattern with attribute-based registration, adds a fluent API, and introduces one-liner dependency injection.

1. Replace ServiceKitBehaviour<T> with ServiceKitBehaviour + [Service]

Before:

publicclassAudioManager:ServiceKitBehaviour<IAudioService>,IAudioService{[InjectService]privateIConfigService_config;publicvoidPlaySound(stringid){/* ... */}}

After:

[Service(typeof(IAudioService))]publicclassAudioManager:ServiceKitBehaviour,IAudioService{[InjectService]privateIConfigService_config;publicvoidPlaySound(stringid){/* ... */}}

For abstract base classes with multiple generic parameters, remove only the ServiceKit type parameter:

// Before: two generics — TService was just for ServiceKitpublicabstractclassScoreService<TService,TScore>:ServiceKitBehaviour<TService>whereTService:classwhereTScore:struct// After: keep the functional generic, drop the ServiceKit one
public abstract class ScoreService<TScore>: ServiceKitBehaviour
whereTScore:struct// Concrete class adds [Service][Service(typeof(IMyScoreService))]
public class MyScoreService :ScoreService<int>,IMyScoreService{}

2. Simplify Dependency Injection Calls

Before:

await_serviceKitLocator.InjectServicesAsync(this).WithErrorHandling().WithTimeout().ExecuteWithCancellationAsync(destroyCancellationToken);

After (one-liner):

await_serviceKitLocator.InjectAsync(this,destroyCancellationToken);

The builder is still available for custom configuration:

await_serviceKitLocator.Inject(this).WithTimeout(10f).WithErrorHandling(ex =>HandleMyError(ex)).ExecuteWithCancellationAsync(destroyCancellationToken);

3. Use Fluent Registration API

Before:

_serviceKit.RegisterService<IAudioService>(audioService);_serviceKit.ReadyService<IAudioService>();

After:

_serviceKit.Register(audioService).As<IAudioService>().Ready();

Quick Migration Checklist

  1. Find-and-replace : ServiceKitBehaviour< — remove the generic, keep the base class
  2. Add [Service(typeof(T))] attribute above each concrete class with the interface type
  3. Find-and-replace .InjectServicesAsync( with .InjectAsync( for one-liner calls, or .Inject( for builder calls
  4. Replace RegisterService<T>() / ReadyService<T>() pairs with .Register().As<T>().Ready()
  5. Ensure each class still implements its declared interface

What You Gain

  • Cleaner declarations — No generic type parameter noise in class signatures or inheritance chains
  • Multi-type registration[Service(typeof(IFoo), typeof(IBar))] on a single class
  • One-liner injectionInjectAsync(this, token) replaces 4-line builder chains
  • Fluent registration — Chainable API with tags, circular exemption, and deferred readiness
  • Compile-time safety — Roslyn analyzer SK003 catches [Service] type mismatches; SK005 catches missing base.Awake() calls

Unit Testing

ServiceKit provides first-class support for unit testing through the UseLocator() method, which allows you to inject mock or test instances of IServiceKitLocator without requiring Unity's serialized field assignment.

Important: When using AddComponent<T>() to create a ServiceKitBehaviour, Unity calls Awake() immediately—before you can assign a locator. UseLocator() handles this automatically by triggering registration if it was skipped during Awake().

Testing with Mocks

Use NSubstitute or any mocking framework to create isolated unit tests:

[TestFixture]publicclassMyServiceTests{privateIServiceKitLocator_mockLocator;privateIServiceInjectionBuilder_mockBuilder;[SetUp]publicvoidSetup(){_mockLocator=Substitute.For<IServiceKitLocator>();_mockBuilder=Substitute.For<IServiceInjectionBuilder>();// Setup fluent API chain_mockBuilder.WithCancellation(Arg.Any<CancellationToken>()).Returns(_mockBuilder);_mockBuilder.WithTimeout(Arg.Any<float>()).Returns(_mockBuilder);_mockBuilder.WithTimeout().Returns(_mockBuilder);_mockBuilder.WithErrorHandling(Arg.Any<Action<Exception>>()).Returns(_mockBuilder);_mockBuilder.ExecuteAsync().Returns(Task.CompletedTask);_mockLocator.Inject(Arg.Any<object>()).Returns(_mockBuilder);}[Test]publicasyncTaskMyBehaviour_RegistersService_OnAwake(){// Arrangevargo=newGameObject();varbehaviour=go.AddComponent<MyServiceKitBehaviour>();behaviour.UseLocator(_mockLocator);// Actawaitbehaviour.TestAwake(CancellationToken.None);// Assert - ServiceKitBehaviour uses non-generic RegisterService_mockLocator.Received(1).RegisterService(typeof(IMyService),Arg.Any<object>(),Arg.Any<string>());}}

Testing with Real ServiceKitLocator

For integration tests, use a real ServiceKitLocator instance:

[TestFixture]publicclassMyServiceIntegrationTests{privateServiceKitLocator_locator;[SetUp]publicvoidSetup(){_locator=ScriptableObject.CreateInstance<ServiceKitLocator>();}[TearDown]publicvoidTearDown(){_locator?.ClearServices();if(_locator!=null)Object.DestroyImmediate(_locator);}[Test]publicvoidMyBehaviour_RegistersAutomatically_WhenUseLocatorCalled(){// Arrange - AddComponent triggers Awake, but registration is skipped (no locator yet)vargo=newGameObject();varbehaviour=go.AddComponent<MyServiceKitBehaviour>();// Act - UseLocator triggers registration automaticallybehaviour.UseLocator(_locator);// Assert - Service is now registeredAssert.IsTrue(_locator.IsServiceRegistered<IMyService>());}[Test]publicasyncTaskMyBehaviour_InjectsDependencies_WhenServicesReady(){// ArrangevarplayerService=newPlayerService();_locator.RegisterAndReadyService<IPlayerService>(playerService);vargo=newGameObject();varbehaviour=go.AddComponent<MyServiceKitBehaviour>();behaviour.UseLocator(_locator);// Act - Complete injection manuallyawait_locator.Inject(behaviour).WithCancellation(CancellationToken.None).WithTimeout().ExecuteAsync();_locator.ReadyService<IMyService>();// AssertAssert.IsNotNull(behaviour.PlayerService);Assert.AreSame(playerService,behaviour.PlayerService);}}

Creating Testable ServiceKitBehaviours

Expose a TestAwake method to manually trigger the initialization sequence in tests:

[Service(typeof(IMyService))]publicclassMyServiceKitBehaviour:ServiceKitBehaviour,IMyService{[InjectService]privateIPlayerService_playerService;publicIPlayerServicePlayerService=>_playerService;publicasyncTaskTestAwake(CancellationTokencancellationToken){RegisterServiceWithLocator();awaitLocator.Inject(this).WithCancellation(cancellationToken).WithTimeout().WithErrorHandling(HandleDependencyInjectionFailure).ExecuteAsync();awaitInitializeServiceAsync();InitializeService();MarkServiceAsReady();}}

Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

License

This project is licensed under the MIT License - see the LICENSE file for details.


Built with ❤️ for the Unity community

About

Lightweight dependency injection & service locator for Unity. Attribute-based registration, async resolution, fluent API, optional dependencies, service tags, UniTask support.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages