A flexible & efficient way to manage and access services in Unity
ServiceLocator is based on a ScriptableObject implementation, offering various methods for registering, unregistering, and retrieving services.
To install ServiceLocator in your Unity project, add the package from the git URL: https://github.com/PaulNonatomic/ServiceLocator.git using the Unity package manager.
ScriptableObject-based implementation for seamless Unity integration- Synchronous, asynchronous, and promise-based service retrieval
- Coroutine support for service access
- Multi-service retrieval in a single call
- Cancellation support with
CancellationToken, includingMonoBehaviour.destroyCancellationToken - Robust error handling and service rejection mechanisms
- Automatic scene tracking for services with a fallback cleanup method when scenes are unloaded
- Service validation for checking if MonoBehaviour services are valid or destroyed
- Configurable feature toggles via the Service Locator Window settings tab
- Fluent API options
A DefaultServiceLocator asset is included in the package for immediate use. The ServiceLocator property drawer automatically populates with the first ServiceLocator asset found in the project. To create a custom instance:
- Right-click in the Unity Editor's Project panel.
- Navigate to
Create -> ServiceLocatorto create a newServiceLocatorasset.
Register services using the Register<T> method:
public interface IMyService
{
void DoSomething();
}
public class MyService : IMyService
{
public void DoSomething()
{
Debug.Log("Doing something!");
}
}
// Obtain your ServiceLocator instance
ServiceLocator locator; // ... reference to your ServiceLocator asset
locator.Register<IMyService>(new MyService());ServiceLocator offers multiple retrieval methods:
- Async/Await - Single Service:
IMyService myService = await _serviceLocator.GetServiceAsync<IMyService>();
myService.DoSomething();- Promise-based - Single Service:
_serviceLocator.GetService<IMyService>()
.Then(service => service.DoSomething())
.Catch(ex => Debug.LogError($"Failed to get service: {ex.Message}"));- Coroutine-based - Single Service:
StartCoroutine(_serviceLocator.GetServiceCoroutine<IMyService>(service =>
{
service?.DoSomething();
}));- Immediate (Try-Get) - Single Service
if (_serviceLocator.TryGetService<IMyService>(out var myService))
{
myService.DoSomething();
}Retrieve multiple services in one call (up to 6 supported):
- Async/Await - Two Services
var (myService, anotherService) = await _serviceLocator.GetServicesAsync<IMyService, IAnotherService>();
myService.DoSomething();
anotherService.DoAnotherThing();_serviceLocator.GetService<IMyService, IAnotherService, IThirdService>()
.Then(services =>
{
services.Item1.DoSomething();
services.Item2.DoAnotherThing();
services.Item3.DoThirdThing();
})
.Catch(ex => Debug.LogError($"Failed to get services: {ex.Message}"));ServiceLocator now includes a fluent API for cleaner, more readable service retrieval, particularly when working with multiple services.
// Single service
var myService = await _serviceLocator
.GetAsync<IMyService>()
.WithCancellation(destroyCancellationToken);
// Multiple services
var (myService, anotherService, thirdService) = await _serviceLocator
.GetAsync<IMyService>()
.AndAsync<IAnotherService>()
.AndAsync<IThirdService>()
.WithCancellation(destroyCancellationToken);// Single service
_serviceLocator
.Get<IMyService>()
.WithCancellation(destroyCancellationToken)
.Then(service => service.DoSomething())
.Catch(ex => Debug.LogError($"Failed to get service: {ex.Message}"));
// Multiple services
_serviceLocator
.Get<IMyService>()
.And<IAnotherService>()
.And<IThirdService>()
.WithCancellation(destroyCancellationToken)
.Then(services => {
var (service1, service2, service3) = services;
service1.DoSomething();
service2.DoAnotherThing();
service3.DoThirdThing();
})
.Catch(ex => Debug.LogError($"Failed to get services: {ex.Message}"));// Single service
StartCoroutine(
_serviceLocator
.GetCoroutine<IMyService>()
.WithCallback(service => service?.DoSomething())
);
// Multiple services
StartCoroutine(
_serviceLocator
.GetCoroutine<IMyService>()
.And<IAnotherService>()
.And<IThirdService>()
.WithCallback((service1, service2, service3) => {
service1?.DoSomething();
service2?.DoAnotherThing();
service3?.DoThirdThing();
})
);This fluent approach improves code readability significantly over the traditional approach, especially when retrieving multiple services. It maintains all the same functionality, including cancellation support, while providing a more intuitive and chainable API.
The fluent API supports up to 6 services in a single chain, just like the traditional API methods.
ServiceLocator now includes extension methods that provide standardized error handling for async operations, reducing the need for repetitive try/catch blocks.
// Without extension - requires try/catch block
private async void Start()
{
try
{
var service = await _serviceLocator.GetServiceAsync<IMyService>();
service.DoSomething();
}
catch (OperationCanceledException)
{
// Handle cancellation
}
catch (Exception ex)
{
Debug.LogError($"Failed to retrieve service: {ex.Message}");
}
}
// With extension - cleaner code with default error logging
private async void Start()
{
var service = await _serviceLocator
.GetServiceAsync<IMyService>()
.WithErrorHandling(defaultValue: null);
service?.DoSomething();
}The error handling extensions provide several features:
var service = await _serviceLocator
.GetServiceAsync<IMyService>()
.WithErrorHandling(
defaultValue: null, // Value to return if the operation fails
errorHandler: ex => { // Custom error handler
NotifyUser("Service unavailable");
AnalyticsManager.LogError(ex);
},
rethrowException: false // Whether to rethrow the exception
);Error handling extensions work seamlessly with the fluent API:
// Single service with fluent API and error handling
var service = await _serviceLocator
.GetAsync<IMyService>()
.WithCancellation(destroyCancellationToken)
.WithErrorHandling(defaultValue: null);
// Multiple services with fluent API and error handling
var (service1, service2) = await _serviceLocator
.GetAsync<IMyService>()
.AndAsync<IAnotherService>()
.WithCancellation(destroyCancellationToken)
.WithErrorHandling(
defaultValue: (null, null),
errorHandler: ex => LogManager.LogError("Failed to load services", ex)
);
// Process services safely with null checks
if (service1 != null)
{
service1.DoSomething();
}
if (service2 != null)
{
service2.DoAnotherThing();
}These error handling extensions provide:
- Automatic contextual logging: Each error is logged with file, method, and line number
- Default value support: Specify fallback values when services aren't available
- Customizable error handling: Provide custom handlers for specific error scenarios
- Simplified code: Remove boilerplate try/catch blocks for cleaner service access
- Cancellation handling: OperationCanceledException is handled gracefully
Error handling works for any Task-based operation, not just service retrieval, making it useful throughout your codebase.
Unregister a service when no longer needed:
_serviceLocator.Unregister<IMyService>();Use MonoService for automatic registration/unregistration::
public interface IMyService
{
void DoSomething();
}
public class MyMonoService : MonoService<IMyService>, IMyService
{
public void DoSomething()
{
Debug.Log("Doing something from MonoService!");
}
protected override void Awake()
{
base.Awake();
// Additional initialization if needed
ServiceReady(); // Registers the service
}
}You can create abstract base classes that inherit from MonoService to establish service patterns:
// Base abstract class for all mini-game score services
public abstract class MiniGameScoreService<T> : MonoService<T>, IMiniGameScoreService
where T: class, IMiniGameScoreService
{
// Common score tracking functionality
protected int _score;
public virtual int GetScore() => _score;
public virtual void AddScore(int points)
{
_score += points;
OnScoreChanged?.Invoke(_score);
}
public event Action<int> OnScoreChanged;
// Other shared functionality...
}
// Concrete implementation for a specific game
public class MyGameScoreService : MiniGameScoreService<IMyGameScoreService>, IMyGameScoreService
{
// Game-specific scoring logic
public void AddComboBonus(int comboCount)
{
AddScore(comboCount * 10);
}
protected override void Awake()
{
base.Awake();
_score = 0; // Initialize score
ServiceReady(); // Register with ServiceLocator
}
}Usage:
public class GameController : MonoBehaviour
{
[SerializeField] private ServiceLocator _serviceLocator;
protected virtual async void Awake()
{
// Get the specific mini-game score service
// This works because MyGameScoreService registers as IMyGameScoreService
var scoreService = await _serviceLocator.GetServiceAsync<IMyGameScoreService>();
scoreService.AddComboBonus(5);
// This would get the specific implementation, not a generic IMiniGameScoreService
// var genericScoreService = await _serviceLocator.GetServiceAsync<IMiniGameScoreService>(); // Won't work!
// You can still access the common interface methods
scoreService.AddScore(100);
}
}The ServiceLocator package includes a powerful debugging tool to help you monitor and manage your services during development.
To open the Service Locator Window:
- In the Unity Editor, navigate to Tools > Service Locator > Service Locator Window
- The window displays all ServiceLocator assets in your project and their registered services
The Service Locator Window now includes two tabs:
Services Tab: View and manage all registered services Settings Tab: Configure which ServiceLocator features are enabled
- Real-time monitoring: Automatically updates to show all registered services
- Scene-based organization: Services are grouped by the scenes they belong to
- Quick navigation: Click on a service to select its GameObject in the hierarchy
- Script access: Open the script file associated with a service directly from the window
- Unloaded scene tracking: Identifies services from unloaded scenes
- Play mode awareness: Updates when entering/exiting play mode
- Feature toggles: Enable/disable specific ServiceLocator features
- Preprocessor directives: Automatically adds or removes the corresponding preprocessor directives
- Reset to defaults: Option to reset all settings to their default values (all enabled)
- Sync from project: Sync settings with the current project's scripting define symbols
- Toggle Async Services
- Toggle Promise Services
- Toggle Coroutine Services
- Toggle Scene Tracking
- Toggle Logging
- Quick debugging: Quickly identify which services are registered and in which scenes
- Missing services: If a service isn't registered when expected, check the window to confirm
- Scene management: Track which services persist across scene loads
- Development: Monitor service registration/unregistration during gameplay
Using Cancellation Token
ServiceLocator supports CancellationToken for canceling service retrieval, particularly useful with MonoBehaviour.destroyCancellationToken to handle cleanup when objects are destroyed.
Example: Canceling Async Service Retrieval
public class ServiceUser : MonoBehaviour
{
[SerializeField] private ServiceLocator _serviceLocator;
private async void Start()
{
try
{
var service = await _serviceLocator.GetServiceAsync<IMyService>(destroyCancellationToken);
service.DoSomething();
}
catch (TaskCanceledException)
{
Debug.Log("Service retrieval canceled due to object destruction.");
}
}
}Behavior: If the MonoBehaviour is destroyed before the service is retrieved, the task cancels automatically, preventing memory leaks or invalid operations.
Example: Canceling Promise-based Retrieval
public class PromiseUser : MonoBehaviour
{
[SerializeField] private ServiceLocator locator;
private void Start()
{
locator.GetService<IMyService>(destroyCancellationToken)
.Then(service => service.DoSomething())
.Catch(ex => Debug.Log($"Retrieval failed or canceled: {ex.Message}"));
}
}Behavior: The promise rejects with a TaskCanceledException if the object is destroyed, ensuring safe cleanup.
The IsServiceValid() method provides a convenient way to check if a service is registered and valid, particularly important for MonoBehaviour-based services that might be destroyed:
public class ServiceValidator : MonoBehaviour
{
[SerializeField] private ServiceLocator _serviceLocator;
private void Update()
{
// Check if the service is still valid before using it
if (_serviceLocator.IsServiceValid<IMyService>())
{
_serviceLocator.TryGetService<IMyService>(out var service);
service.DoSomething();
}
else
{
// Service is not registered or has been destroyed
Debug.Log("Service is not valid, waiting for a new one to register...");
}
}
}Behavior: This method not only checks if the service is registered but also verifies that Unity MonoBehaviour-based services haven't been destroyed, providing safer service access.
The ServiceLocator uses CancellationTokenSource.CreateLinkedTokenSource internally to provide robust cancellation behavior for multi-service operations. This offers several benefits:
- All-or-nothing cancellation: If any service in a multi-service request fails or cancels, all other pending requests are automatically cancelled
- Single control point: One cancellation token affects all dependent operations
- Reduced resource leakage: Proper cleanup of all related operations when cancellation occurs
- Consistent application state: Prevents partial results where some parts of a tuple are filled while others aren't
Example: Cancelling Multi-Service Operations
Utilise MonoBehaviour.destroyCancellationToken to cancel multi-service operations when the object is destroyed with ease.
public class MultiServiceUser : MonoBehaviour
{
[SerializeField] private ServiceLocator _serviceLocator;
private async void Start()
{
try
{
// Request multiple services with the same token
var (service1, service2, service3) = await _serviceLocator.GetServicesAsync<
IAuthService,
IUserService,
IDataService
>(destroyCancellationToken);
// All services successfully retrieved
ProcessServices(service1, service2, service3);
}
catch (OperationCanceledException)
{
Debug.Log("Multi-service operation was cancelled");
}
}
}ServiceLocator provides mechanisms to handle errors and explicitly reject service promises.
Example: Handling Errors with Async/Await
public class ErrorHandler : MonoBehaviour
{
[SerializeField] private ServiceLocator locator;
private async void Start()
{
try
{
var service = await locator.GetServiceAsync<IMyService>();
service.DoSomething();
}
catch (Exception ex)
{
Debug.LogError($"Failed to retrieve service: {ex.Message}");
}
}
}Behavior: Catches any exceptions, such as cancellation or rejection, thrown during retrieval.
Example: Rejecting a Service with a Custom Exception
public class ServiceRejector : MonoBehaviour
{
[SerializeField] private ServiceLocator locator;
private void Start()
{
locator.GetService<IMyService>()
.Then(service => service.DoSomething())
.Catch(ex => Debug.LogError($"Service rejected: {ex.Message}"));
// Simulate a failure to initialize the service
locator.RejectService<IMyService>(new InvalidOperationException("Service initialization failed"));
}
}Behavior: RejectService triggers the Catch handler with the custom exception, allowing you to handle initialization failures gracefully.
Example: Combining Cancellation and Rejection
public class CombinedHandler : MonoBehaviour
{
[SerializeField] private ServiceLocator _serviceLocator;
private void Start()
{
_serviceLocator.GetService<IMyService>(destroyCancellationToken)
.Then(service => service.DoSomething())
.Catch(ex =>
{
if (ex is TaskCanceledException)
{
Debug.Log("Retrieval canceled due to destruction.");
}
else
{
Debug.LogError($"Service retrieval failed: {ex.Message}");
}
});
// Simulate rejection if initialization fails
if (someCondition)
{
_serviceLocator.RejectService<IMyService>(new InvalidOperationException("Initialization failed"));
}
}
}Behavior: Handles both cancellation (e.g., object destruction) and explicit rejection (e.g., initialization failure) in a single Catch block.
Manually clean up services and promises:
_serviceLocator.Cleanup(); // Clears services and cancels pending promisesWhen a service is unregistered (either manually or through scene unloading), any pending promises for that service will be automatically rejected with an ObjectDisposedException, allowing consumers to handle the unavailability gracefully.
ServiceLocator automatically tracks which scene each MonoBehaviour-based service belongs to. When a scene is unloaded, any services from that scene are automatically unregistered to prevent memory leaks. If service clean is handled correctly this fallback should not be needed, but it is a good safety net and a warning will be logged for each service that is not cleaned up via the fallback.
Example: Automatic Scene Cleanup
// When TestScene is unloaded, all services registered from that scene
// will be automatically unregistered, and any pending promises will be rejected
Behavior: This prevents "zombie" services from persisting after their scene has been unloaded, which could lead to memory leaks or unexpected behavior.
### Initialization and De-initialization
ServiceLocator initializes/de-initializes automatically, but you can control it manually:
```csharp
_serviceLocator.Initialize(); // Manual initialization
_serviceLocator.DeInitialize(); // Manual cleanupContributions are welcome! Please refer to CONTRIBUTING.md for guidelines on how to contribute.
ServiceLocator is licensed under the MIT license. See LICENSE for more details.
For more complex dependency management, consider Dependency Injection frameworks like Zenject. ServiceLocator is ideal for simpler service management needs in Unity.


