⚡ Inspired by QFramework – RicKit.RFramework’s command system and service locator pattern borrow heavily from QFramework, while delivering a lightweight, Unity-friendly implementation.
- Introduction
- Features
- Architecture Overview
- Installation
- Getting Started
- Service Locator Lifecycle & Initialization
- Dependency Injection & Service Registration
- Dependency Injection in Commands & MonoBehaviours
- Event System
- Command System
- Usage Examples
- Best Practices
- Advanced Topics
- Contributing
- License
RicKit.RFramework is a lightweight service locator and messaging framework for Unity and C# projects. It supports:
- Dependency Injection via a global ServiceLocator
- Event Bus for publish/subscribe patterns
- Command Dispatch (CQRS-style Request-Handler)
By relying on a single central locator, you avoid fragile static references while still enjoying fast lookup and automatic lifecycle management.
- Zero-configuration: register services and commands at runtime.
- Automatic initialization, startup, and deinitialization of services.
- Event System: strongly-typed events with register/unregister/send.
- Command System: support for:
- No-arg no-return commands
- No-arg with-return commands
- Arg-only, no-return commands
- Arg-and-return commands
- Full Dependency Injection support via
TryGetService<T>(). - Fully extension-method-based API: dispatch commands or events from any object implementing locator interfaces.
- Inspired by QFramework’s patterns but implemented independently and streamlined for performance.
- ServiceLocator (
ServiceLocator<T>)- Generic singleton locator
- Manages a Cache of
IServiceimplementations - Calls
Init(),Start(),DeInit()in proper order
- IService / AbstractService
- Base interface for all services
- Lifecycle:
Init(),Start(),DeInit()
- ICanGetLocator / ICanSetLocator
- Enables any class to retrieve the locator instance via
this.GetLocator()
- Enables any class to retrieve the locator instance via
- EventExtension
RegisterEvent<T>(Action<T>)UnRegisterEvent<T>(Action<T>)SendEvent<T>(T arg)
- CommandExtension
SendCommand<TCommand>()SendCommand<TCommand, TResult>()SendCommand<TCommand, TArgs, TResult>(TArgs args)SendCommand<TCommand, TArgs>(TArgs args)
Install via OpenUPM:
npm install com.rickit.rframework --registry https://package.openupm.comor via Unity Package Manager (UPM) UI:
- Open
Window → Package Manager. - Click “+” → “Add package by name…”.
- Enter
com.rickit.rframework.
-
Initialize your custom locator in bootstrap code (e.g., on game launch):
MyGameLocator.Initialize();
-
Create a subclass of
ServiceLocator<T>and register services in itsInit()override:public class MyGameLocator : ServiceLocator<MyGameLocator> { protected override void Init() { // Register services by interface RegisterService<IAnalyticsService>(new AnalyticsService()); RegisterService<IDataService>(new DataService()); } }
-
Retrieve services in any object by implementing
ICanGetLocator<MyGameLocator>:public class PlayerController : MonoBehaviour, ICanGetLocator<MyGameLocator> { private IAnalyticsService analytics; void Awake() { // Inject via TryGetService in Awake this.TryGetService(out analytics); analytics.TrackEvent("game_start"); } void Start() { // You can also get services directly var data = this.GetService<IDataService>(); data.SaveGame(); } }
-
Static Initialize
- Creates locator instance
- Calls your
Init()override to register services - Calls each registered service’s
Init() - Calls each service’s
Start(), setsIsInitialized = true - Sets locator’s
IsInitialized = true
-
DeInit
- Calls
DeInit()on all initialized services - Clears the static locator reference
- Calls
Register all services in your locator’s Init():
public interface IAnalyticsService : IService
{
void TrackEvent(string name);
}
public class AnalyticsService : AbstractService, IAnalyticsService
{
public override void Init() { /* attach SDK */ }
public void TrackEvent(string name) { /* ... */ }
}
// In MyGameLocator.Init():
RegisterService<IAnalyticsService>(new AnalyticsService());- Services must implement
IService. - Locator will inject itself into each service before initialization.
- Use
this.GetService<Interface>()orthis.TryGetService(out T)to obtain dependencies.
You can perform injection in both commands and MonoBehaviours using TryGetService<T>():
// Command Example
public class KillPlayerCommand
: AbstractCommand<int, int>
{
private IPlayerService playerService;
public override void Init()
{
// Inject dependency in Init
this.TryGetService(out playerService);
}
public override int Execute(int playerId)
{
playerService.Kill(playerId);
return playerId;
}
}
// MonoBehaviour Example
public class GameStarter : MonoBehaviour,
ICanGetLocator<MyGameLocator>
{
private IDataService dataService;
void Awake()
{
// Inject in Awake
this.TryGetService(out dataService);
dataService.LoadGame();
}
}// Any class implementing ICanGetLocator<MyGameLocator>:
this.RegisterEvent<PlayerDamagedEvent>(evt => { /* handle damage */ });
this.UnRegisterEvent<PlayerDamagedEvent>(handler);
this.SendEvent(new PlayerDamagedEvent { Damage = 10 });- Handlers stored in a
Dictionary<Type, Delegate>on the locator. - Supports multiple subscribers per event type.
Do not instantiate command classes manually. Always dispatch via extension methods on
ICanGetLocator<T>.
// No-arg, no-return
this.SendCommand<ResetGameCommand>();
// No-arg, return
int count = this.SendCommand<GetPlayerCountCommand, int>();
// Arg-only, no-return
this.SendCommand<LogEventCommand, string>("Player died.");
// Arg-and-return
int killedId = this.SendCommand<KillPlayerCommand, int, int>(playerId);- Commands are cached in the locator and initialized once before first use.
-
Service Example
public class DataService : AbstractService, IDataService { public override void Init() { /* load DB */ } public void SaveGame() { /* ... */ } } // Registered in MyGameLocator.Init() // Retrieved anywhere in ICanGetLocator: var data = this.GetService<IDataService>(); data.SaveGame();
-
Command with Custom Struct
public struct PlayerInfo { public int Id; public string Name; } public class LogPlayerInfoCommand : AbstractCommandOnlyArgs<PlayerInfo> { public override void Init() { this.TryGetService(out var logger); } public override void Execute(PlayerInfo info) { Debug.Log($"Player {info.Id}: {info.Name}"); } } this.SendCommand<LogPlayerInfoCommand, PlayerInfo>( new PlayerInfo { Id = 1, Name = "Alice" });
-
Command with Tuple
public class ProcessScoresCommand : AbstractCommand<(int Level, int Score), bool> { public override void Init() { } public override bool Execute((int Level, int Score) args) { return args.Score > 1000; } } bool passed = this.SendCommand< ProcessScoresCommand, (int Level, int Score), bool>((2, 1200));
- Always implement
ICanGetLocator<MyGameLocator>and usethis.GetService<T>()orthis.TryGetService(out T)for dependencies. - Perform dependency injection in
Init()for commands orAwake()for MonoBehaviours. - Register all services in your locator’s
Init(). - Use
SendCommand/SendEventextension methods only. - Call
MyGameLocator.Initialize()once on startup andMyGameLocator.I.DeInit()on shutdown.
- Scoped locators for scene-specific modules.
- Integration with Zenject, VContainer, or other DI frameworks.
- Fork the repo.
- Create a feature branch.
- Open a PR against
main. - Add tests or example coverage.