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. +[![openupm](https://img.shields.io/npm/v/games.engine-room.generators?label=openupm®istry_uri=https://package.openupm.com&color=brightgreen)](https://openupm.com/packages/games.engine-room.generators/) +[![Unity](https://img.shields.io/badge/Unity-2022.3%2B-black?logo=unity)](#requirements) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + +
+ +

+ + Watch the MGenerators tutorial on YouTube + +
+ ▶ 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)*