From 9b6dd788489703a1d34df18778a56dc6c6f63526 Mon Sep 17 00:00:00 2001
From: Yuri Sokolov <7556847+migus88@users.noreply.github.com>
Date: Wed, 27 May 2026 13:45:46 +0300
Subject: [PATCH 1/2] Restructure README: video, collapsible sections, expanded
docs
- Embed the YouTube tutorial as a clickable thumbnail under the header
- Collapsible install methods (OpenUPM CLI, Git URL with version pinning, manual scoped registry)
- Collapsible attribute docs with new section for [SingletonInclude] / [SingletonIgnore]
- Document [Singleton]'s destroyOnLoad flag and generated members
- Move "Swapping in tests" and "Lazy instantiation" under a Guidance subsection
- Add shields.io badges, GitHub callouts, and section separators for readability
---
README.md | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 189 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index b935e6e..1409c88 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,110 @@
+
+
# MGenerators
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.
+[](https://openupm.com/packages/games.engine-room.generators/)
+[](#requirements)
+[](LICENSE)
+
+
+
+
+
+
+
+
+ ▶ Watch the tutorial on YouTube
+
+
+---
+
## Table of contents
- [Installation](#installation)
- [Singletons](#singletons)
- - [Singleton Attribute](#singleton)
- - [Dependency Attribute](#dependency)
- - [Swapping in tests](#swapping-in-tests)
- - [Lazy instantiation](#lazy-instantiation)
+ - [Attributes](#attributes)
+ - [Guidance](#guidance)
- [Requirements](#requirements)
+---
+
## Installation
-Available on [OpenUPM](https://openupm.com/):
+
+Via OpenUPM CLI — recommended
+
+
+
+The fastest path. Requires the [OpenUPM CLI](https://openupm.com/docs/getting-started.html#installing-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` 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](https://github.com/Engine-Room-Games/MGenerators/releases).
+
+
+
+
+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**.
+
+
+
+---
+
## Singletons
> [!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.
+> 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.
+
+### Attributes
+
+
+[Singleton] — turn a MonoBehaviour into a singleton
-### `[Singleton]`
+
`[Singleton]` turns a `partial class : MonoBehaviour` into a singleton. Consumers reach it through the generated `I.Instance`.
@@ -60,7 +139,7 @@ public partial class SoundManager : ISoundManager
{
public static ISoundManager Create()
{
- var obj = new GameObject();
+ var obj = new GameObject(nameof(SoundManager));
return obj.AddComponent();
}
@@ -89,10 +168,27 @@ Access it from anywhere via the generated `Instance`:
ISoundManager.Instance.PlayTap();
```
-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:
+#### Options
+
+The attribute accepts two optional arguments — a custom interface type and a `destroyOnLoad` flag:
```csharp
-public partial interface IDataStoreManager
+[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 `Awake()` 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 generated `Awake()`.
+
+**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`:
+
+```csharp
+public partial interface IDataStoreManager : ISingleton
{
int GetScore();
void SetScore(int value);
@@ -106,9 +202,70 @@ public partial class DataStoreManager : MonoBehaviour, IDataStoreManager
}
```
-`[SingletonInclude]` / `[SingletonIgnore]` are also available on individual members for finer control over the auto-generated interface.
+#### Generated members
+
+| Member | What it does |
+| ------------------------- | --------------------------------------------------------------------------------------- |
+| `I` 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 void Awake()` | Publishes the instance, enforces the singleton invariant, optionally calls `DontDestroyOnLoad`. |
+| `partial void OnAwake()` | Your hook — define it for post-awake setup; called after the instance is published. |
+
+
+
+
+[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 and property. `[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 and property is on the interface. Use `[SingletonIgnore]` to hide individual members:
+
+```csharp
+[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)*
-### `[Dependency]`
+Only members tagged `[SingletonInclude]` appear on the interface. `[SingletonIgnore]` becomes redundant (and the analyzer will warn you):
+
+```csharp
+[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 and properties only.
+- `[SingletonInclude]` members must be **public** and **non-static** — the analyzer raises `ER0xxx` diagnostics 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.
@@ -142,7 +299,16 @@ public partial class Egg
}
```
-### Swapping in tests
+Multiple `[Dependency]` fields on the same class are all assigned in the same generated `Start()` before `OnStart()` runs.
+
+
+
+### Guidance
+
+
+Swapping in tests
+
+
Because consumers see only the generated interface, mocking is a one-liner:
@@ -173,7 +339,12 @@ public void Tapping_plays_the_sound()
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
+
+
+
+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.
@@ -197,6 +368,10 @@ public class Bootstrap : MonoBehaviour
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.
+
+
+---
+
## Requirements
Unity **2022.3** or newer. Tested on **Unity 2022.3.62** and **Unity 6000.4.0f1**.
From f5d91409d8e14d90b324ecf1174a3c6920da9a88 Mon Sep 17 00:00:00 2001
From: Yuri Sokolov <7556847+migus88@users.noreply.github.com>
Date: Wed, 27 May 2026 13:58:25 +0300
Subject: [PATCH 2/2] Replace alert syntax inside with bold-label
blockquotes
GitHub renders [!NOTE]/[!TIP]/[!WARNING] callouts only at the markdown's
top level, not inside blocks, so the two inside collapsibles
were falling through as plain quotes.
---
README.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 1409c88..44f91a0 100644
--- a/README.md
+++ b/README.md
@@ -58,8 +58,7 @@ Open **Window → Package Manager**, click the **+** button, choose **Install pa
https://github.com/Engine-Room-Games/MGenerators.git?path=src/generators-unity/Packages/games.engine-room.generators
```
-> [!TIP]
-> Pin a version by appending `#v` 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.
+> **Tip** — Pin a version by appending `#v` 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
@@ -220,8 +219,7 @@ public partial class DataStoreManager : MonoBehaviour, IDataStoreManager
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 and property. `[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.
+> **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)*