A Unity package of Roslyn source generators that take care of the boilerplate around common gameplay patterns. Every generator ships with a companion analyzer that catches common misuse at edit time, so the pitfalls of each pattern surface as compiler diagnostics instead of runtime surprises.

▶ Watch the tutorial on YouTube
Via OpenUPM CLI — recommended
The fastest path. Requires the OpenUPM CLI (npm install -g openupm-cli):
openupm add games.engine-room.generators
This adds the scoped registry, pins the latest version in Packages/manifest.json, and triggers Unity to import it.
Via Git URL
Open Window → Package Manager, click the + button, choose Install package from git URL…, and paste:
https://github.com/Engine-Room-Games/MGenerators.git?path=src/generators-unity/Packages/games.engine-room.generators
Tip — Pin a version by appending
#v<version>to the URL. Without a version, Unity locks to whatever the default branch points at the time of install and never updates on its own; pinning makes that choice explicit.
# pin to a release
https://github.com/Engine-Room-Games/MGenerators.git?path=src/generators-unity/Packages/games.engine-room.generators#v1.0.0
# track the main branch (rolling)
https://github.com/Engine-Room-Games/MGenerators.git?path=src/generators-unity/Packages/games.engine-room.generators#main
Available versions are listed on the releases page.
Via OpenUPM (manual / scoped registry)
Use this if you don't want to install the OpenUPM CLI. You'll add OpenUPM as a scoped registry once, then install the package from the Package Manager UI.
1. Open Edit → Project Settings → Package Manager and add a new Scoped Registry:
| Field | Value |
|---|---|
| Name | package.openupm.com |
| URL | https://package.openupm.com |
| Scope(s) | games.engine-room.generators |
2. Open Window → Package Manager, switch the top-left dropdown to My Registries, find Engine Room Generators, and click Install.
Warning
Singletons are bad and I do not recommend using them. With that said, I know I can't change the world — there is a big following of the pattern, and people will reach for it whether I like it or not. I strongly believe that any code should be ready for change. If I can make singletons ready to be swapped out for dependency injection while keeping the ergonomics that make people use them in the first place — I'm jumping on the opportunity.
[Singleton] — turn a MonoBehaviour into a singleton
[Singleton] turns a partial class : MonoBehaviour into a singleton. Consumers reach it through the generated I<ClassName>.Instance.
using EngineRoom.Runtime.Singleton;
using UnityEngine;
[Singleton]
public partial class SoundManager : MonoBehaviour
{
[SerializeField] private AudioClip _tapClip;
private AudioSource _audioSource;
public void PlayTap() => _audioSource.PlayOneShot(_tapClip);
partial void OnAwake()
{
_audioSource = GetComponent<AudioSource>();
}
}This will generate the following code:
public interface ISoundManager : ISingleton<ISoundManager>
{
void PlayTap();
}
public partial class SoundManager : ISoundManager
{
public static ISoundManager Create()
{
var obj = new GameObject(nameof(SoundManager));
return obj.AddComponent<SoundManager>();
}
// Returns false when this instance is a duplicate (gameObject has already
// been Destroy'd) so the lifecycle dispatcher can short-circuit.
private bool SingletonAwake()
{
var existing = ISoundManager.Instance as Object;
if (existing != null && existing != this)
{
Object.Destroy(gameObject);
return false;
}
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
ISoundManager.Instance = this;
return true;
}
}
// Emitted by the Lifecycle generator (see below). Calls SingletonAwake first,
// aborts on duplicate, then runs any user [Awake] methods, then OnAwake().
public partial class SoundManager
{
private void Awake()
{
if (!SingletonAwake())
{
return;
}
OnAwake();
}
partial void OnAwake();
}Access it from anywhere via the generated Instance:
ISoundManager.Instance.PlayTap();The attribute accepts two optional arguments — a custom interface type and a destroyOnLoad flag:
[Singleton(destroyOnLoad: false)] // default — survives scene loads
[Singleton(destroyOnLoad: true)] // per-scene singleton
[Singleton(typeof(IDataStoreManager))] // bring your own interface
[Singleton(typeof(IDataStoreManager), destroyOnLoad: true)]destroyOnLoad (default: false)
By default the generated SingletonAwake helper calls DontDestroyOnLoad(gameObject) and re-parents the host to the scene root, so the singleton outlives scene transitions. Set destroyOnLoad: true when you want the singleton scoped to its scene — useful for per-level managers that should reset on reload. With the flag on, the DontDestroyOnLoad and reparenting calls are omitted from the helper.
Bring-your-own interface
If you'd rather curate the public surface yourself, pass an interface to the attribute and the generator will wire the class up to it instead of synthesising one. Your interface must extend ISingleton<TSelf>:
public partial interface IDataStoreManager : ISingleton<IDataStoreManager>
{
int GetScore();
void SetScore(int value);
}
[Singleton(typeof(IDataStoreManager))]
public partial class DataStoreManager : MonoBehaviour, IDataStoreManager
{
public int GetScore() => PlayerPrefs.GetInt("Score", 0);
public void SetScore(int value) => PlayerPrefs.SetInt("Score", value);
}| Member | What it does |
|---|---|
I<ClassName> interface |
Auto-generated when no interface is supplied. Lists the public-and-non-[SingletonIgnore] members of the class (or only [SingletonInclude]-tagged members in explicit mode). |
static Create() |
Factory that spawns a fresh GameObject named after the class and adds the component. |
private bool SingletonAwake() |
Helper: publishes the instance, enforces the singleton invariant, optionally calls DontDestroyOnLoad. Returns false on a duplicate (after calling Destroy(gameObject)). |
private void Awake() |
Lifecycle dispatcher — emitted by the Lifecycle generator. Calls SingletonAwake(), short-circuits on duplicate, then user [Awake] methods, then OnAwake(). |
partial void OnAwake() |
Back-compat hook — define it for post-awake setup; called after the instance is published. (For new code, prefer marking a method with [Awake].) |
[SingletonInclude] & [SingletonIgnore] — curate the generated interface
When the generator synthesises the interface, it has to decide which members of your class show up on it. By default that's every public method, property, and event. [SingletonInclude] and [SingletonIgnore] give you fine-grained control. They are mutually exclusive per class — using [SingletonInclude] anywhere on the class flips the generator into explicit mode.
Note — Neither attribute applies when you supply your own interface via
[Singleton(typeof(IFoo))]; in that case the interface contract is whatever you typed.
Auto mode (default — no [SingletonInclude] on the class)
Every public instance method, property, and event is on the interface. Use [SingletonIgnore] to hide individual members:
[Singleton]
public partial class SoundManager : MonoBehaviour
{
public void PlayTap() { /* ... */ } // → exposed on ISoundManager
public void PlayWin() { /* ... */ } // → exposed on ISoundManager
[SingletonIgnore]
public void DebugDumpMixerState() { /* ... */ } // → omitted from ISoundManager
}Explicit mode (any [SingletonInclude] present on the class)
Only members tagged [SingletonInclude] appear on the interface. [SingletonIgnore] becomes redundant (and the analyzer will warn you):
[Singleton]
public partial class SoundManager : MonoBehaviour
{
[SingletonInclude]
public void PlayTap() { /* ... */ } // → exposed on ISoundManager
public void PlayWin() { /* ... */ } // → NOT on ISoundManager (no [SingletonInclude])
public void DebugDumpMixerState() { } // → NOT on ISoundManager
}Constraints
- Both attributes target methods, properties, and events only.
[SingletonInclude]members must be public and non-static — the analyzer raisesER0xxxdiagnostics otherwise.
[Dependency] — inject a singleton into a field
[Dependency] resolves a private field from the matching singleton's Instance in a generated Start(). The field's type must be the singleton's interface.
using EngineRoom.Runtime.Singleton;
using UnityEngine;
public partial class Egg : MonoBehaviour
{
[Dependency] private ISoundManager _soundManager;
public void Tap()
{
_soundManager.PlayTap();
}
}This will generate the following code:
public partial class Egg
{
private void DependencyStart()
{
_soundManager = ISoundManager.Instance;
}
}
// Emitted by the Lifecycle generator (see below). Calls DependencyStart first,
// then any user [Start] methods, then OnStart().
public partial class Egg
{
private void Start()
{
DependencyStart();
OnStart();
}
partial void OnStart();
}Multiple [Dependency] fields on the same class are all assigned in the same generated DependencyStart() before any user [Start] methods or the back-compat OnStart() partial run.
Swapping in tests
Because consumers see only the generated interface, mocking is a one-liner:
public class MockSoundManager : ISoundManager
{
public int TapPlayCount { get; private set; }
public static MockSoundManager Install()
{
var mock = new MockSoundManager();
ISoundManager.Instance = mock;
return mock;
}
public void PlayTap() => TapPlayCount++;
}
[Test]
public void Tapping_plays_the_sound()
{
var sound = MockSoundManager.Install();
var egg = new GameObject().AddComponent<Egg>();
egg.Tap();
Assert.AreEqual(1, sound.TapPlayCount);
}This doesn't solve the fundamental issue with singletons — Instance is still global state and every test has to install its mocks up front — but it's a meaningful step up from a classic singleton where there's no seam to mock against at all.
Lazy instantiation
The generators deliberately don't support lazy instantiation. Auto-spawning a singleton the first time Instance is read leads to hard-to-trace initialization order bugs once the objects do real work — so the package leaves the instantiation moment in your hands.
The preferred approach is to place each singleton on a scene (or instantiate it from a prefab) so Unity wires it up like any other component. If you'd rather create them from code, a tiny bootstrap MonoBehaviour on the first scene does the job:
using UnityEngine;
[DefaultExecutionOrder(-1000)]
public class Bootstrap : MonoBehaviour
{
private void Awake()
{
SoundManager.Create();
UiManager.Create();
DataStoreManager.Create();
GameManager.Create();
}
}The [DefaultExecutionOrder] attribute makes Bootstrap.Awake run before any other script, so every Create() (the factory emitted by [Singleton]) registers its instance before anything else touches it.
The Lifecycle generator turns Unity's lifecycle hooks (Awake, Start, OnEnable, OnDisable, Update, FixedUpdate, LateUpdate, OnDestroy) into method-level attributes. Mark as many methods as you like with [Awake]/[Start]/etc.; the generator emits a single dispatcher per hook that calls them all in order. This lets multiple features on the same MonoBehaviour cooperate on a lifecycle method without anyone owning Awake() outright — which is also how [Singleton] and [Dependency] integrate cleanly.
Lifecycle attributes — hook into Unity's lifecycle without owning the method
One attribute per Unity message:
| Attribute | Generated dispatcher |
|---|---|
[Awake] |
private void Awake() |
[Start] |
private void Start() |
[OnEnable] |
private void OnEnable() |
[OnDisable] |
private void OnDisable() |
[Update] |
private void Update() |
[FixedUpdate] |
private void FixedUpdate() |
[LateUpdate] |
private void LateUpdate() |
[OnDestroy] |
private void OnDestroy() |
using EngineRoom.Runtime.Lifecycle;
using UnityEngine;
public partial class Player : MonoBehaviour
{
[Awake]
private void WireInput() { /* … */ }
[Awake]
private void CacheComponents() { /* … */ }
[Update(order: -10)]
private void PollInput() { /* runs before any unordered [Update] */ }
[Update]
private void Step() { /* … */ }
[OnDestroy]
private void Unsubscribe() { /* … */ }
}The generator emits a single partial:
public partial class Player
{
private void Awake()
{
WireInput();
CacheComponents();
OnAwake(); // back-compat hook (always declared for Awake/Start)
}
partial void OnAwake();
private void Update()
{
PollInput(); // order = -10, runs first
Step(); // unordered, source position
}
private void OnDestroy()
{
Unsubscribe();
}
}Each attribute takes an optional int order (defaults to 0). Methods are sorted by ascending Order, then by their declaration position in source for stable ties. Methods without an explicit order intermix with order: 0 and follow source order among themselves. When two methods on the same lifecycle share the same explicit order, the analyzer warns (ERG0205) and the tiebreak falls back to source position.
[Singleton] always runs its SingletonAwake() helper before any user [Awake] methods on the same class, and short-circuits the rest of the dispatcher on a duplicate (the gameObject has been Destroy'd). [Dependency] always resolves fields via DependencyStart() before any user [Start] methods. These are special-cased — they don't participate in Order sorting.
For backwards compatibility, the Awake and Start dispatchers still call (and declare) the OnAwake() and OnStart() partial hooks at the end, so existing code that implemented those keeps working. New code is encouraged to use [Awake]/[Start]-marked methods instead.
- The host class must be
partialand inherit fromMonoBehaviour(ERG0201,ERG0202). - A lifecycle-attributed method must be an instance method, take no parameters, and return
void(ERG0204). - The class must not define the lifecycle entry-point method directly (e.g.
void Awake()when the class also has[Awake]methods or[Singleton]) — the generator emits it (ERG0203).
Unity 2022.3 or newer. Tested on Unity 2022.3.62 and Unity 6000.4.0f1.