Color Book — Architecture Guide
A toddler-targeted (ages 2–6) coloring book game built on the same Strict Modular Monolith pattern as the Bus Game. Powered by VContainer for DI, UniTask for async, Addressables for shipped content, and a hybrid Sprites + Canvas render strategy.
This document is the canonical reference for the Color Book game's structure. The Bus Game's Darkmatter Architecture Guide is the parent contract; this doc only adds game-specific structure.
1. Game Flow
App launch
└─ Boot scene (RootLifetimeScope)
└─ MainMenu scene
├─ Press "Play" → ColorBook scene
│ ├─ Drawing catalog (grid of templates)
│ ├─ Select drawing
│ ├─ Shape Builder panel (drag pieces → snap to slots)
│ ├─ ↓ on assembly complete
│ ├─ Color panel (tap color → tap region)
│ ├─ Undo / Redo any time
│ ├─ "Capture" → save to Gallery with paper background
│ └─ "Next" → auto-save + load next drawing
└─ Press "Art Book" → ArtBook scene (gallery viewer)
├─ Grid of saved artworks
├─ View / share / delete
└─ Save to device camera roll
2. Philosophy
Identical to Bus Game:
- Vertical slices — code grouped by Feature, not by type.
- Strict layering — dependencies flow downward only.
- Composition over inheritance — wired by DI.
- Code vs Content —
Code/for logic,Contents/for assets.
Game-specific additions:
- Toddler-first UX — large hitboxes, forgiving snap radii, no fail states, no timers.
- Stateless replay — every action (paint, snap) is an
ICommandso undo/redo and auto-save are trivial. - Capture-as-truth — the gallery is a folder of PNGs on disk, not a serialized scene graph. What the child sees is what gets saved.
3. Dependency Graph
App ──────────┐
▼
Features ──► Core ◄── Services
▲
└── Libs
Forbidden actions
- Services cannot reference Features.
- Features cannot reference Service implementations — only Core contracts via DI.
- Features cannot reference other Features — use
IEventBussignals or Core contracts. - Core may not contain logic. Interfaces, enums, DTOs, signal records only.
- Core may reference
UniTaskfor async contract signatures. Nothing else.
4. Folder Structure
This section reflects the actual project on disk today. Empty folders that have been reserved for upcoming work are marked (planned); everything else has at least one file in it. Aspirational additions for the rest of the game are listed in §4c at the bottom.
4a. Actual layout on disk
Assets/Darkmatter/
├── Scenes/
│ └── Boot.unity ← only scene wired so far
│
├── Content/ ← singular ("Content", not "Contents")
│ └── Gameplay/
│ └── PaperRig/ ← (planned — paper rig prefabs)
│
├── Data/
│ ├── Inputs/ (Input System .inputactions)
│ └── Settings/
│ ├── Persistance/Resources/ (ProtectedPlayerPrefs settings)
│ ├── Rendering/ (URP renderer + asset)
│ └── Scenes/URP2DSceneTemplate.unity
│
└── Code/
├── App/
│ └── LifetimeScopes/
│ └── RootLifetimeScope.cs ← scope loads serialized IServiceModule list
│ Darkmatter.App.asmdef
│
├── Core/ (asmdef name: `Core`, namespace root `Darkmatter.Core.*`)
│ ├── Compatibility/
│ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
│ ├── Contracts/
│ │ ├── Paper/ ← misplaced empty folder — should be Contracts/Features/Paper/ (delete or move)
│ │ └── Services/
│ │ ├── Assets/IAssetProviderService.cs
│ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
│ │ ├── Camera/ICameraService.cs
│ │ ├── Capture/ ← (planned — ICaptureService)
│ │ ├── Inputs/IInputReader.cs
│ │ └── Scenes/ISceneService.cs
│ ├── Data/
│ │ ├── Dynamic/Services/Audio/ (AudioHandle, AudioRequest)
│ │ └── Static/Services/Audio/ (SfxCatalogSO)
│ └── Enums/
│ └── Services/
│ ├── Audio/ (AudioChannel, AudioPlayMode, SfxId)
│ ├── Camera/CameraType.cs (MainCamera, UICamera — ArtCamera not added yet)
│ └── Scenes/GameScene.cs
│
├── Features/ ← (planned — empty folder today)
│
├── Libs/
│ ├── FSM/ (IState, State, StateMachine + Docs)
│ │ Libs.FSM.asmdef
│ ├── Installers/ (IServiceModule — Register(IContainerBuilder))
│ │ Libs.Installers.asmdef
│ ├── Observer/ (IEventBus, EventBus — note: not named "EventBus")
│ │ Libs.Observer.asmdef
│ ├── PlayerPrefs/ (ProtectedPlayerPrefs — used in place of a Persistence service)
│ │ ├── Editor/ Libs.PlayerPrefs.Editor.asmdef
│ │ └── Runtime/ Libs.PlayerPrefs.asmdef
│ └── UI/ (ToggleButton, ToggleButtonGroup)
│ Libs.UI.asmdef
│
└── Services/
├── Analytics/
│ ├── Installers/AnalyticsServiceModule.cs
│ └── Systems/FirebaseAnalyticsSystem.cs
│ Services.Analytics.asmdef
├── Assets/
│ ├── AddressableAssetProviderService.cs
│ └── AddressableLoadHandleTracker.cs
│ Services.Assets.asmdef
├── Audio/
│ ├── AudioService.cs
│ └── SfxPlayer.cs
│ Services.Audio.asmdef
├── Camera/
│ ├── Installers/CameraServiceModule.cs
│ └── Service/CameraService.cs
│ Services.Camera.asmdef
├── Inputs/
│ ├── Generated/GameInputs.cs (Input System codegen)
│ ├── Installers/InputServiceModule.cs
│ └── Readers/InputReaderSO.cs
│ Services.Inputs.asmdef
└── Scenes/
└── SceneService.cs
Services.Scenes.asmdef
4b. Conventions visible in current code
- Asmdef per Service / Lib / App / Core. No Feature asmdefs yet (folder is empty).
- Core sub-tree shape:
Core/{Compatibility, Contracts, Data, Enums}— deeply nested by Service rather than by topic. Game-specific Core types (Drawing, Coloring, Paper, Gallery, History, Progression, Signals) will be added under eitherCore/Contracts/<Area>/or new top-levelCore/<Area>/— pick one convention before adding the first one. - Service sub-shape varies — some are flat (
Services/Audio/*.cs), some split (Services/Camera/Installers/,Services/Camera/Service/). The "official" per-feature shape (§4d) hasn't been validated against real code yet. - Installer pattern:
MonoBehaviour, IServiceModulewithRegister(IContainerBuilder builder). NotIInstaller.Install(...)as older sections of this doc imply. New installers must followIServiceModule. - No
AppBootentry point exists yet.RootLifetimeScopeonly iterates a serializedMonoBehaviour[] serviceModulesand callsRegisteron eachIServiceModule. Boot sequence in §29 is aspirational. - No Persistence service.
Libs/PlayerPrefs(theProtectedPlayerPrefslibrary) is in place for small-key state. JSON persistence for gallery sidecars will need a separate service when Gallery lands. - Camera service is a registry, not a fitter.
ICameraService.Register/Getonly — no aspect-fit logic.
4c. Planned additions (not on disk yet)
All new game code follows the same nesting pattern as existing Services — Contracts/Features/<Name>/, Data/{Dynamic,Static}/Features/<Name>/, Features/<Name>/ — and asmdefs drop the Darkmatter. prefix to match Core, Services.Audio, Libs.Observer.
Rough landing order for ColorBook scene to be playable:
| Path | Role |
|---|---|
Core/Contracts/Features/Paper/IPaperRig.cs, IArtInputBridge.cs |
Paper rig contracts |
Core/Contracts/Services/Capture/ICaptureService.cs |
Capture service contract |
Core/Contracts/Services/Gallery/IGalleryService.cs |
Gallery service contract |
Core/Contracts/Features/Drawing/IDrawingTemplate.cs, IDrawingTemplateCatalog.cs |
Drawing template contracts |
Core/Contracts/Features/Coloring/IColorPalette.cs |
Palette contract |
Core/Contracts/Features/History/ICommand.cs, IUndoStack.cs |
Undo/redo contracts |
Core/Contracts/Features/Progression/IProgressionService.cs |
Progression contract (despite the name, it's a feature contract since it's game-specific) |
Core/Data/Static/Features/Drawing/ (DrawingTemplateSO) |
Authored drawing data |
Core/Data/Static/Features/Coloring/ (ColorPaletteSO) |
Authored palette data |
Core/Data/Dynamic/Features/Drawing/ShapePieceDTO.cs, ColorRegionDTO.cs |
Runtime drawing structs |
Core/Data/Dynamic/Features/Coloring/PaintCommandDTO.cs |
Runtime coloring struct |
Core/Data/Dynamic/Features/Gallery/SavedArtworkDTO.cs |
Runtime gallery struct |
Core/Data/Dynamic/Features/Signals/ (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, ArtworkCapturedSignal, ArtworkSavedSignal) |
Cross-feature signal structs |
Core/Enums/Services/Camera/CameraType.cs |
Add ArtCamera enum value to existing file |
Libs/CommandStack/ (+ Libs.CommandStack.asmdef) |
Bounded undo/redo |
Services/Capture/ (+ Services.Capture.asmdef) |
RenderTextureCaptureService reads IPaperRig.Surface |
Services/Gallery/ (+ Services.Gallery.asmdef) |
FileGalleryService — PNG + sidecar JSON IO |
Features/Paper/ (+ Features.Paper.asmdef) |
Scene-bound RT rig |
Features/{MainMenu,DrawingCatalog,ShapeBuilder,Coloring,History,Capture,Progression,ColorBookFlow,ArtBook}/ (+ asmdefs each) |
Game features |
App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs |
Per-scene scopes |
App/Boot/AppBoot.cs |
Bootstrap entry point |
Assets/Darkmatter/Scenes/{MainMenu,ColorBook,ArtBook}.unity |
Scenes |
Content/Gameplay/Drawings/<theme>/<id>/{Template.asset, Pieces/, Regions/, PaperBackground.png} |
Authored drawings (under existing Content/Gameplay/ root) |
Content/Gameplay/Palettes/*.asset |
Color palettes |
Content/Audio/{UI,Coloring}/ |
SFX banks |
4d. Per-feature folder layout (matches existing Services pattern)
Look at Services/Camera/ (Installers/ + Service/) and Services/Analytics/ (Installers/ + Systems/) — that's the convention. Features adopt the same shape, adding UI/ or Views/ only when there's something to put in them.
Features/[Name]/
├── Installers/ IServiceModule — VContainer registration
├── Systems/ (or Service/) Controllers, repositories, factories (pure C#)
├── UI/ (only if the feature has Canvas UI)
│ ├── *Presenter.cs Pure C#, listens to model, drives view
│ └── *View.cs MonoBehaviour, setters only
├── Views/ (only if the feature has world-space MonoBehaviours)
└── Features.<Name>.asmdef
Rules of thumb pulled from current Services:
- Use
Service/(singular) when the feature has exactly one main implementation class (likeServices/Camera/Service/CameraService.cs). - Use
Systems/(plural) when there are multiple pure-C# coordinators (likeServices/Analytics/Systems/). - Skip nesting entirely when the feature has only 1–2 files at root (like
Services/Audio/AudioService.cs+SfxPlayer.csflat). Docs/is per-folder in current code — drop aDocs/inside any sub-folder that needs notes, don't make a global feature-level Docs.
5. Namespaces & Asmdef naming
C# namespace pattern is Darkmatter.<Layer>.<Module>[.<SubArea>] — the Darkmatter. prefix stays on namespaces. Examples already in code:
Darkmatter.Core.Contracts.Services.Camera(ICameraService.cs)Darkmatter.Services.Camera(CameraService.cs)Darkmatter.Services.Camera.Installers(CameraServiceModule.cs)Darkmatter.Libs.Installers(IServiceModule.cs)
Asmdef names drop the Darkmatter. prefix. Existing pattern:
| Namespace | Asmdef |
|---|---|
Darkmatter.Core.* |
Core |
Darkmatter.App |
Darkmatter.App (one exception — keep as-is, don't churn) |
Darkmatter.Libs.Observer |
Libs.Observer |
Darkmatter.Libs.FSM |
Libs.FSM |
Darkmatter.Libs.Installers |
Libs.Installers |
Darkmatter.Libs.PlayerPrefs |
Libs.PlayerPrefs (+ Libs.PlayerPrefs.Editor) |
Darkmatter.Libs.UI |
Libs.UI |
Darkmatter.Services.Audio |
Services.Audio |
Darkmatter.Services.Assets |
Services.Assets |
Darkmatter.Services.Camera |
Services.Camera |
Darkmatter.Services.Inputs |
Services.Inputs |
Darkmatter.Services.Scenes |
Services.Scenes |
Darkmatter.Services.Analytics |
Services.Analytics |
New asmdefs follow the same convention: Services.Capture, Services.Gallery, Libs.CommandStack, Features.Paper, Features.Coloring, etc.
6. Scenes & Lifetime Scopes
| Scene | Scope | Status | Contents |
|---|---|---|---|
Boot.unity |
RootLifetimeScope |
✅ exists | All Services + Libs. Persists forever. |
MainMenu.unity |
MainMenuLifetimeScope |
⚠️ planned | Menu presenter, art book entry. |
ColorBook.unity |
ColorBookLifetimeScope |
⚠️ planned | PaperRig, DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. |
ArtBook.unity |
ArtBookLifetimeScope |
⚠️ planned | Gallery presenter, viewer, share. |
Only Boot.unity exists today; the three scene scope classes haven't been written yet either (only RootLifetimeScope exists in App/LifetimeScopes/).
Scopes nest: Root → (MainMenu | ColorBook | ArtBook). Services resolved from the root parent. Scene scopes only register their own features.
Boot chain (planned)
No AppBoot class exists yet — today RootLifetimeScope only registers services and stops there. When AppBoot is added (as an IAsyncStartable registered via builder.RegisterEntryPoint<AppBoot>()), it should run once, in order:
- Initialize
IAssetProviderService(Addressables init). - Preload essential bundles (palettes, UI sounds).
- Load
IProgressionServicefrom disk. - Load
MainMenuscene.
Failures show a child-friendly retry screen; never crash.
7. Rendering Strategy
RT-as-paper. ArtCamera renders the drawing world to an offscreen RenderTexture. A Canvas RawImage displays that RT. HUD lives on the same Canvas, above the RawImage. The RT is the paper — same fixed coordinate system on every device.
┌──────────────────────────────────────────────────────┐
│ UICanvas (Screen-Space - Camera, UICamera) │
│ │
│ ┌────────────────────────────────────┐ │
│ │ RawImage (AspectRatioFitter 1:1) │ [HUD] │
│ │ └─ texture = PaperRig.Surface │ palette │
│ │ │ undo etc │
│ │ ArtCamera renders → here │ │
│ └────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
▲
│ rendered offscreen
│
ArtCamera (orthographicSize fixed, aspect = 1f)
culling mask: Artwork, PaperBackground, Effects
target texture: PaperRig.Surface (2048×2048 ARGB32)
Cameras
| Camera | Type | Culling Mask | Render Target | Purpose |
|---|---|---|---|---|
ArtCamera |
Orthographic, fixed ortho size, aspect = 1 | Artwork, PaperBackground, Effects |
PaperRig.Surface (offscreen RT) |
Renders the drawing world. Never sees the screen. |
UICamera |
Camera (Screen-Space – Camera) | UI |
Screen | Displays the paper RawImage + HUD. |
Layers
| Layer | Used by |
|---|---|
Artwork |
Drawing region sprites, shape pieces, paper bg, all in ArtCamera world |
Effects |
Particle bursts, sparkles — also in ArtCamera world (so they're captured into the PNG) |
UI |
All Canvas elements (RawImage paper + HUD) |
Why RT-as-paper
| Need | Choice | Why |
|---|---|---|
| Per-region tap-to-fill | Sprites + PolygonCollider2D in ArtCamera world; tapped via IArtInputBridge |
Coordinate system is fixed (RT space). One Physics2D.OverlapPoint call after screen→art-world conversion. |
| Drag/drop shape pieces | Sprites + Physics2D in art world | Same fixed bounds on every device — no per-aspect tray layout. |
| Capture to PNG | RT → Texture2D → PNG |
The RT is the saved image. No camera state override, no compositing pass, no determinism worries. |
| Multi-resolution support | AspectRatioFitter (1:1, FitInParent) on the RawImage |
The "fit camera" problem reduces to a single Canvas property. Letterbox/pillarbox = whatever the Canvas around the RawImage looks like. |
| Color palette, buttons | Canvas above the RawImage | Anchors handle aspect ratios. Buttons + ScrollRect free. |
| Drawing catalog grid | Canvas | GridLayoutGroup + ScrollRect, async thumbnail loader. |
Multi-resolution rule
The artwork world is screen-size-independent by construction. Author every drawing in a fixed 2048×2048 design rect (or 20×20 world units at PPU=100). Pieces, regions, snap radii, slot positions — all expressed in this space and never scaled at runtime. Different screen sizes only change how the RawImage is laid out on the Canvas; the contents of the RT stay identical.
If you need a backdrop (wood/cloth behind the paper), it's a sibling Canvas Image outside the RawImage, sized to fill the screen. The RT itself has a transparent or paper-colored background.
8. Core Contracts
All Core types are pure data or interfaces.
Drawing
Contracts live in
Darkmatter.Core.Contracts.Features.Drawing; DTOs inDarkmatter.Core.Data.Dynamic.Features.Drawing.
namespace Darkmatter.Core.Contracts.Features.Drawing;
public interface IDrawingTemplate {
string Id { get; }
string DisplayName { get; }
Sprite Thumbnail { get; }
Sprite PaperBackground { get; }
IReadOnlyList<ShapePieceDTO> Pieces { get; }
IReadOnlyList<ColorRegionDTO> Regions { get; }
}
public readonly struct ShapePieceDTO {
public string PieceId { get; }
public Sprite Sprite { get; }
public Vector2 SlotPosition { get; }
public float SlotRotation { get; }
public float SnapRadius { get; } // generous for toddlers
}
public readonly struct ColorRegionDTO {
public string RegionId { get; }
public Sprite Sprite { get; } // sprite renderer source
public Vector2[] ColliderPath { get; } // polygon collider points
public Color InitialColor { get; } // usually white
}
Coloring
Contracts in
Darkmatter.Core.Contracts.Features.Coloring; DTOs inDarkmatter.Core.Data.Dynamic.Features.Coloring.
namespace Darkmatter.Core.Contracts.Features.Coloring;
public interface IColorPalette {
string Id { get; }
IReadOnlyList<Color> Colors { get; }
}
public readonly struct PaintCommandDTO {
public string RegionId { get; }
public Color FromColor { get; }
public Color ToColor { get; }
}
Paper (RT rig + input bridge)
Contracts live in
Darkmatter.Core.Contracts.Features.Paper. Files atCore/Contracts/Features/Paper/.
namespace Darkmatter.Core.Contracts.Features.Paper;
public interface IPaperRig {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface
RenderTexture Surface { get; } // 2048×2048 ARGB32; the paper itself
Transform PaperRoot { get; } // parent of regions/pieces/paper bg
Vector2 DesignSize { get; } // world units, e.g. (20, 20)
Rect DesignRect { get; } // centered on origin, DesignSize wide
}
public interface IArtInputBridge {
// Converts a screen-space pointer (Input System) to art-world coords
// inside the RT. Returns false if the pointer is outside the RawImage.
bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos);
}
IPaperRigis implemented byPaperRig : MonoBehaviourin the ColorBook scene.IArtInputBridgedoes the screen → RawImage local → UV →ArtCamera.ViewportToWorldPointchain.- All consumers (Coloring, ShapeBuilder, Capture, particle effects) read these from DI; they never touch
Screen.width/heightdirectly.
History
Contracts in
Darkmatter.Core.Contracts.Features.History.
namespace Darkmatter.Core.Contracts.Features.History;
public interface ICommand {
void Execute();
void Undo();
}
public interface IUndoStack {
bool CanUndo { get; }
bool CanRedo { get; }
void Push(ICommand cmd); // executes + appends
void Undo();
void Redo();
void Clear();
}
Gallery & Capture
IGalleryServiceis a Service contract →Darkmatter.Core.Contracts.Services.Gallery.SavedArtworkDTOis a runtime data struct →Darkmatter.Core.Data.Dynamic.Features.Gallery.ICaptureService→Darkmatter.Core.Contracts.Services.Capture.
namespace Darkmatter.Core.Data.Dynamic.Features.Gallery;
public readonly struct SavedArtworkDTO {
public string Id { get; }
public string TemplateId { get; }
public DateTime CreatedUtc { get; }
public string ImagePath { get; } // persistentDataPath PNG
public string ThumbnailPath { get; }
}
namespace Darkmatter.Core.Contracts.Services.Gallery;
public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync();
UniTask<Texture2D> LoadFullAsync(string artworkId);
UniTask DeleteAsync(string artworkId);
}
namespace Darkmatter.Core.Contracts.Services.Capture;
public interface ICaptureService {
// No camera or paperBg args — capture reads directly from IPaperRig.Surface.
// Dimensions inherited from the RT; no resize, no compositing.
UniTask<byte[]> CaptureAsync();
}
ICaptureService resolves IPaperRig via DI and reads Surface directly. The paper background is already baked into the RT because it sits in PaperRoot under the ArtCamera. No special compositing pass is ever needed.
Signals
Signal structs live in
Darkmatter.Core.Data.Dynamic.Features.Signals(runtime data, cross-feature).
namespace Darkmatter.Core.Data.Dynamic.Features.Signals;
public readonly struct DrawingSelectedSignal {
public string TemplateId { get; }
}
public readonly struct ShapeAssembledSignal {
public string TemplateId { get; }
}
public readonly struct ColorAppliedSignal {
public string RegionId { get; }
public Color Color { get; }
}
public readonly struct ArtworkCapturedSignal {
public string ArtworkId { get; }
}
public readonly struct ArtworkSavedSignal {
public SavedArtworkDTO Artwork { get; }
}
9. Feature Responsibilities
DrawingCatalog
- Loads the catalog manifest (list of available template IDs + thumbnail addresses).
- Presents a scrollable grid of thumbnails (Canvas).
- On select → fires
DrawingSelectedSignal(templateId)and unloads the catalog UI.
Paper
- Scene-scoped infrastructure. Lives in
ColorBook.unityonly. - Owns
PaperRig(MonoBehaviour) — exposesArtCamera, theRenderTexture Surface,PaperRoottransform, and the design rect. - Owns
ArtInputBridge— converts pointer screen positions to art-world coords inside the RT. - Registered in
ColorBookLifetimeScopeviaPaperRigModule. All other features in the scene resolveIPaperRig/IArtInputBridgefrom DI. - Lifetime is scene-scoped: created on scene load, destroyed on scene unload. RT is allocated in
Awake, released inOnDestroy.
ShapeBuilder
- Listens to
DrawingSelectedSignal. - Loads template via
IDrawingTemplateLoader, parents shape pieces underIPaperRig.PaperRootat off-slot positions inside the design rect. - Per piece: drag with
ShapePieceView(sprite + collider). Pointer events go throughIArtInputBridge.TryScreenToArtWorld. On drop, check distance toSlotPositionagainstSnapRadius; if within, snap and lock. - Fires
ShapeAssembledSignalwhen all pieces locked.
Coloring
- Listens to
ShapeAssembledSignal. - Spawns one
ColorRegionViewperColorRegionDTOunderIPaperRig.PaperRoot(sprite + polygon collider onArtworklayer). - Listens to palette selection (current color held in
ColoringStateRepository). - On pointer down:
IArtInputBridge.TryScreenToArtWorld(screenPos, out var artPos)→Physics2D.OverlapPoint(artPos, artworkMask)→ if hit, buildPaintRegionCommand(regionId, oldColor, newColor), push toIUndoStack. - Command sets
SpriteRenderer.coloron undo/redo. - Fires
ColorAppliedSignalfor SFX / sparkle effects.
History
- Owns the singleton
IUndoStackfor the current ColorBook session. - Cleared on
DrawingSelectedSignal(new drawing = fresh history). - Capped at ~20 entries (memory + cognitive simplicity).
- UI: two big arrow buttons; disabled state when
CanUndo / CanRedois false.
Capture
- Bound to the "Capture" button.
- Calls
ICaptureService.CaptureAsync()→ PNG bytes. Capture readsIPaperRig.Surfacedirectly; no camera or paper-bg args needed. - Hands bytes to
IGalleryService.SaveAsync(...). - Fires
ArtworkCapturedSignalthenArtworkSavedSignal. - Shows a quick "saved!" toast with a thumbnail of the new entry.
Progression
- Tracks completed template IDs and the in-progress draft.
- On "Next" button: silently runs Capture pipeline (auto-save), marks current as completed, calls
IDrawingTemplateCatalog.NextUnseen(). - Persists JSON via
IPersistenceService.
ColorBookFlow
- The only orchestrator inside ColorBook scope.
- Subscribes to flow-relevant signals and toggles UI panels (catalog → builder → coloring).
- Coordinates "Next" sequence:
IProgressionService.MarkCompleted→ICaptureServiceautosave →IDrawingTemplateLoader.Release(currentId)→ load next. - Built as a small FSM (
Catalog → Building → Coloring → Done).
ArtBook
- Separate scene.
GalleryPresentercallsIGalleryService.ListAsync()→ grid of thumbnails.- Tap → fullscreen view, share-sheet button, delete.
- Saved-to-device-camera-roll uses an optional platform plugin behind
IExternalShareService(Core contract).
10. Addressables Strategy
Mirror the Bus Game pattern via IAssetProviderService.
What ships through Addressables
| Asset | Why |
|---|---|
DrawingTemplate ScriptableObject (per drawing) |
Many; load on demand. |
| Shape piece sprites | Only needed when active. |
| Region sprites + polygon paths | Heavy; loaded per drawing. |
| Paper backgrounds | Per template, sometimes shared. |
| Color palette SOs | Swap per theme. |
| Audio clips (tap, snap, complete, sparkle) | Shared SFX bank. |
What does NOT use Addressables
- HUD prefabs (palette button, undo icon) — always loaded with scene.
- Core UI canvases.
- Boot scene assets.
- User-saved gallery PNGs — those live in
Application.persistentDataPath.
Group layout
Drawings_Animals (label: drawing, animals)
Drawings_Vehicles (label: drawing, vehicles)
Drawings_Shapes (label: drawing, shapes)
Palettes (label: palette)
Audio_UI (label: sfx, ui)
Audio_Coloring (label: sfx, coloring)
Lifecycle
- Catalog loads thumbnail handles only (cheap).
- On select → full template loads (pieces + regions + paper).
- On "Next" or scene exit → previous template
Released before next loads. - This bound matters on toddler tablets with limited RAM.
Remote groups (future)
Drawing packs ship as remote bundles. New theme packs (Christmas, Dinosaurs) update without an app store release.
11. Persistence
Two distinct stores, each behind its own Core contract.
IPersistenceService (JSON / PlayerPrefs)
Holds:
- Completed template IDs.
- Last opened drawing.
- Audio volume, simple settings.
Path: Application.persistentDataPath/save.json.
IGalleryService (file IO)
Holds user artworks:
persistentDataPath/Gallery/
├── {guid}.png full-res render (~2048×2048)
├── {guid}.thumb.png 256×256 for grid
└── {guid}.json SavedArtworkDTO sidecar
- Writes are atomic (
.tmp→ rename). ListAsyncenumerates sidecar JSONs sorted byCreatedUtc desc.- Thumbnail generation happens once at save time on a worker thread.
12. Capture Pipeline
With the RT-paper-rig, capture has no setup phase. The RT is already the final image at all times.
[Capture button or Next button]
│
▼
ICaptureService.CaptureAsync()
│
├─ rt = _paperRig.Surface (already populated each frame)
├─ prev = RenderTexture.active
├─ RenderTexture.active = rt
├─ tex = new Texture2D(rt.width, rt.height, RGBA32, false)
├─ tex.ReadPixels(full rect, 0, 0); tex.Apply()
├─ RenderTexture.active = prev
├─ bytes = tex.EncodeToPNG() (on worker via UniTask.RunOnThreadPool)
├─ Object.Destroy(tex)
└─ return bytes
▼
IGalleryService.SaveAsync(bytes, templateId)
│
├─ Write .png atomically
├─ Generate + write thumbnail
├─ Write sidecar JSON
└─ return SavedArtworkDTO
▼
EventBus.Publish(new ArtworkSavedSignal(dto))
Notes:
- HUD never appears in capture because the HUD is on
UICamera/ Canvas — it is physically in a different render path. The RT only ever seesArtCamera's output. - Paper background is a sprite parented under
IPaperRig.PaperRootand is rendered into the RT every frame — already baked in. - Saved PNGs are byte-comparable across devices because the RT dimensions and ArtCamera matrix never depend on screen size.
CaptureAsyncis safe to call repeatedly — no camera state is ever mutated.
13. Communication Rules
| Use case | Mechanism |
|---|---|
| Load template, return result | Direct DI call (IDrawingTemplateLoader.LoadAsync). |
| Capture → save chain | Direct DI calls, sequenced. |
| Notify HUD that a region was painted | IEventBus signal. |
| Notify Progression that a drawing was completed | IEventBus signal. |
| Tell ColorBookFlow that pieces are assembled | IEventBus signal. |
| Tell Coloring which color is currently selected | Direct DI on ColoringStateRepository. |
Never use signals for request/response. If you need a return value or guaranteed single handler, define a Core interface.
14. UI (MVP — Passive View)
Identical to Bus Game.
- Model — controller / repository, fires C# events.
- View —
MonoBehaviour, only setters (SetColors(IReadOnlyList<Color>)). - Presenter — pure C#, subscribes to model events, calls view setters.
Inspector bridge
For palette icons, undo buttons, region prefabs:
[SerializeField, RequireInterface(typeof(IColorButtonView))]
private MonoBehaviour[] _colorButtons;
15. Toddler UX Constraints
These shape several design decisions and are non-negotiable:
- No fail states. Drawings cannot be "wrong".
- No timers. Nothing decays or runs out.
- No tiny hitboxes. Drag tolerance ≥ 40 px; snap radius ≥ 60 px for shape pieces.
- Auto-snap on near-miss. If a piece is dropped within
1.5 × SnapRadius, snap anyway and play a happy sound. - No text-heavy UI. Icons everywhere. Single-word labels max.
- Loud, immediate feedback. Every tap plays a sound; every fill bursts a small particle effect.
- Undo cap = 20. Toddlers will mash undo. Bound the memory.
- Long-press = quick menu off. Avoid surprise modals.
16. Testing
| Layer | Test type | Location |
|---|---|---|
Libs/CommandStack |
EditMode unit tests | Libs/CommandStack/Tests/ |
Core DTOs |
EditMode | rarely needed, but for SavedArtworkDTO serialization, yes. |
Services/Gallery |
EditMode w/ temp directory | mocks Application.persistentDataPath. |
Services/Capture |
PlayMode | requires a Camera in the test scene. |
Features/*/Systems |
EditMode w/ DI test container | inject fakes for IUndoStack, signals captured by a fake IEventBus. |
| Full flow | PlayMode smoke test | one drawing → assemble → color → capture → assert gallery has 1 file. |
17. "Where do I put this?" Checklist
- Is it a cross-assembly interface / enum / DTO? →
Core/ - Is it a generic, sellable utility? →
Libs/ - Is it infrastructure (input, audio, file IO, addressables, capture)? →
Services/ - Is it gameplay logic specific to coloring books? →
Features/ - Is it composition / scene wiring? →
App/
When in doubt, ask: would deleting this feature break Core? If yes, the dependency is wrong.
18. Open Questions / Future Work
- Pencil/brush mode — currently the design is tap-to-fill regions. A free-draw brush mode would need a
BrushStrokeCommandand a dynamic texture per region; out of scope for v1. - Multi-child profiles — single-profile for v1; multi-profile would slot in behind
IProgressionServiceandIGalleryServicekeyed byprofileId. - Cloud sync — gallery sync would happen behind
IGalleryService(decorator pattern); local-first stays the source of truth. - Sticker / decoration layer — additive sprite layer above coloring, also
ICommand-driven so it integrates with undo/redo cleanly.
19. Quick Reference — Feature ↔ Signal Map
| Feature | Subscribes to | Publishes |
|---|---|---|
DrawingCatalog |
— | DrawingSelectedSignal |
ShapeBuilder |
DrawingSelectedSignal |
ShapeAssembledSignal |
Coloring |
ShapeAssembledSignal |
ColorAppliedSignal |
History |
DrawingSelectedSignal (to clear) |
— |
Capture |
— (button-driven) | ArtworkCapturedSignal, ArtworkSavedSignal |
Progression |
ArtworkSavedSignal |
— |
ColorBookFlow |
ShapeAssembledSignal, ArtworkSavedSignal |
— |
ArtBook (Gallery) |
ArtworkSavedSignal (if open) |
— |
Maintained alongside the Darkmatter Architecture Guide. Do not break the dependency arrows.
20. Assembly Definition Map
Every Lib / Service / Feature is its own .asmdef. The Darkmatter. prefix is only on the App asmdef; everything else uses bare <Layer>.<Module> names. References follow the layer rules.
On disk today
| Asmdef | Path | References |
|---|---|---|
Darkmatter.App |
App/ |
All Services, Libs, Core (Features when they land) |
Core |
Core/ |
(none — UniTask allowed in async signatures) |
Libs.FSM |
Libs/FSM/ |
Core |
Libs.Installers |
Libs/Installers/ |
(VContainer only) |
Libs.Observer |
Libs/Observer/ |
Core |
Libs.PlayerPrefs |
Libs/PlayerPrefs/Runtime/ |
(standalone) |
Libs.PlayerPrefs.Editor |
Libs/PlayerPrefs/Editor/ |
Libs.PlayerPrefs |
Libs.UI |
Libs/UI/ |
Core |
Services.Analytics |
Services/Analytics/ |
Core, Libs.Installers |
Services.Assets |
Services/Assets/ |
Core, Libs.Installers |
Services.Audio |
Services/Audio/ |
Core, Libs.Installers |
Services.Camera |
Services/Camera/ |
Core, Libs.Installers |
Services.Inputs |
Services/Inputs/ |
Core, Libs.Installers |
Services.Scenes |
Services/Scenes/ |
Core, Libs.Installers |
Planned (not on disk yet)
| Asmdef | Path | References |
|---|---|---|
Libs.CommandStack |
Libs/CommandStack/ |
Core |
Services.Capture |
Services/Capture/ |
Core, Libs.Installers |
Services.Gallery |
Services/Gallery/ |
Core, Libs.Installers |
Features.Paper |
Features/Paper/ |
Core, Libs.Installers |
Features.MainMenu |
Features/MainMenu/ |
Core, Libs.Installers |
Features.DrawingCatalog |
Features/DrawingCatalog/ |
Core, Libs.Installers |
Features.ShapeBuilder |
Features/ShapeBuilder/ |
Core, Libs.Installers |
Features.Coloring |
Features/Coloring/ |
Core, Libs.Installers, Libs.CommandStack |
Features.History |
Features/History/ |
Core, Libs.Installers, Libs.CommandStack |
Features.Capture |
Features/Capture/ |
Core, Libs.Installers |
Features.Progression |
Features/Progression/ |
Core, Libs.Installers, Libs.PlayerPrefs |
Features.ColorBookFlow |
Features/ColorBookFlow/ |
Core, Libs.Installers, Libs.FSM |
Features.ArtBook |
Features/ArtBook/ |
Core, Libs.Installers |
Hard rules:
- No Service asmdef references any Feature asmdef.
- No Feature asmdef references another Feature asmdef.
- All Services and Features depend on
Libs.Installersso they can implementIServiceModule. - The compiler enforces this — if a
usingwon't resolve, the dependency is wrong.
21. LifetimeScope Concrete Sample
All scopes use the same pattern: a serialized MonoBehaviour[] list of IServiceModule installers. Each installer is a MonoBehaviour on a child GameObject of the scope. Scope iterates and calls Register. No hardcoded registrations in the scope itself. This is exactly what RootLifetimeScope.cs already does today.
RootLifetimeScope (Boot scene, persists forever) — actual code
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
public class RootLifetimeScope : LifetimeScope {
[SerializeField] private MonoBehaviour[] serviceModules;
protected override void Configure(IContainerBuilder builder) {
foreach (var module in serviceModules) {
if (module is IServiceModule serviceModule)
serviceModule.Register(builder);
}
}
}
The inspector lists the installer MonoBehaviours in serviceModules[]. Drag the children of the Boot scope GameObject (e.g. AudioServiceModule, CameraServiceModule, InputServiceModule, AssetProviderServiceModule, AnalyticsServiceModule, SceneServiceModule) into that slot. Each is a MonoBehaviour, IServiceModule.
ColorBookLifetimeScope (per-scene, child of Root) — same pattern
namespace Darkmatter.App.LifetimeScopes;
public sealed class ColorBookLifetimeScope : LifetimeScope {
[SerializeField] private MonoBehaviour[] sceneModules;
protected override void Configure(IContainerBuilder builder) {
foreach (var module in sceneModules) {
if (module is IServiceModule serviceModule)
serviceModule.Register(builder);
}
}
}
Drag the scene's installer MonoBehaviours into sceneModules[]:
PaperRigModuleDrawingCatalogModuleShapeBuilderModuleColoringModuleHistoryModuleCaptureModuleProgressionModuleColorBookFlowModule
Each registers its own classes via IServiceModule.Register(IContainerBuilder).
If a scope needs a non-installer reference (e.g. a
ColorBookSceneRefsMB holding camera + roots), expose it as a separate[SerializeField]andbuilder.RegisterInstance(...)it inside the scope'sConfigure. Don't put scene refs inside an installer — keep installers stateless across scenes.
22. Installer Pattern — Concrete Coloring Sample
Mirrors the existing CameraServiceModule.cs and InputServiceModule.cs — a MonoBehaviour implementing IServiceModule.Register.
namespace Darkmatter.Features.Coloring.Installers;
public sealed class ColoringModule : MonoBehaviour, IServiceModule {
[SerializeField] private ColoringConfig _config;
public void Register(IContainerBuilder builder) {
builder.RegisterInstance(_config);
builder.Register<ColoringStateRepository>(Lifetime.Scoped).AsSelf();
builder.Register<ColoringController>(Lifetime.Scoped)
.As<IColoringController>()
.AsSelf();
builder.Register<ColorPalettePresenter>(Lifetime.Scoped).AsSelf();
builder.RegisterEntryPoint<ColoringInputBinder>();
}
}
Convention:
- One
IServiceModuleper feature, named<Feature>Module(matchesCameraServiceModule,InputServiceModule,AnalyticsServiceModulealready in the project). MonoBehaviourlives on a GameObject under the scope's hierarchy; dragged into the scope'sserviceModules[]/sceneModules[]inspector list.- Method name is
Register, notInstall. There is noIInstallerin this project — usesIServiceModulefrom Libs.Installers. - Registers only its own types. Never touches another feature's types.
- If the installer needs to wire scene-bound MonoBehaviours into DI, expose them as
[SerializeField]fields on the installer itself andbuilder.RegisterInstance<IFoo>(_foo)them. See the plannedPaperRigModulein §32.5b for an example.
23. Command Pattern — PaintRegionCommand
namespace Darkmatter.Features.Coloring.Commands;
internal sealed class PaintRegionCommand : ICommand {
private readonly ColorRegionView _view;
private readonly Color _fromColor;
private readonly Color _toColor;
private readonly IEventBus _bus;
public PaintRegionCommand(ColorRegionView view, Color from, Color to, IEventBus bus) {
_view = view;
_fromColor = from;
_toColor = to;
_bus = bus;
}
public void Execute() {
_view.SetColor(_toColor);
_bus.Publish(new ColorAppliedSignal(_view.RegionId, _toColor));
}
public void Undo() {
_view.SetColor(_fromColor);
_bus.Publish(new ColorAppliedSignal(_view.RegionId, _fromColor));
}
}
Usage in controller:
public void PaintRegion(ColorRegionView view) {
var current = _state.CurrentColor;
if (view.Color == current) return; // no-op
var cmd = new PaintRegionCommand(view, view.Color, current, _bus);
_undoStack.Push(cmd); // Push executes + records
}
Same pattern applies to SnapPieceCommand if shape-builder steps should be undoable (optional for v1).
24. CommandStack — Libs/CommandStack
namespace Darkmatter.Lib.CommandStack;
public sealed class BoundedUndoStack : IUndoStack {
private readonly Deque<ICommand> _undo = new();
private readonly Stack<ICommand> _redo = new();
private readonly int _capacity;
public BoundedUndoStack(int capacity = 20) => _capacity = capacity;
public bool CanUndo => _undo.Count > 0;
public bool CanRedo => _redo.Count > 0;
public void Push(ICommand cmd) {
cmd.Execute();
_undo.AddLast(cmd);
if (_undo.Count > _capacity) _undo.RemoveFirst();
_redo.Clear();
}
public void Undo() {
if (!CanUndo) return;
var cmd = _undo.Last;
_undo.RemoveLast();
cmd.Undo();
_redo.Push(cmd);
}
public void Redo() {
if (!CanRedo) return;
var cmd = _redo.Pop();
cmd.Execute();
_undo.AddLast(cmd);
}
public void Clear() {
_undo.Clear();
_redo.Clear();
}
}
Deque<T> keeps the oldest entry cheap to evict when the cap fires.
25. View / Presenter Pair — Color Palette
View (MonoBehaviour, setters only)
namespace Darkmatter.Features.Coloring.UI;
public sealed class ColorPaletteView : MonoBehaviour, IColorPaletteView {
[SerializeField, RequireInterface(typeof(IColorButtonView))]
private MonoBehaviour[] _buttonsRaw;
private IColorButtonView[] _buttons;
public event Action<int> OnColorButtonClicked;
private void Awake() {
_buttons = _buttonsRaw.Cast<IColorButtonView>().ToArray();
for (var i = 0; i < _buttons.Length; i++) {
var idx = i;
_buttons[i].OnClicked += () => OnColorButtonClicked?.Invoke(idx);
}
}
public void SetColors(IReadOnlyList<Color> colors) {
for (var i = 0; i < _buttons.Length; i++)
_buttons[i].SetVisible(i < colors.Count);
for (var i = 0; i < colors.Count; i++)
_buttons[i].SetColor(colors[i]);
}
public void SetSelected(int index) {
for (var i = 0; i < _buttons.Length; i++)
_buttons[i].SetSelected(i == index);
}
}
Presenter (pure C#)
namespace Darkmatter.Features.Coloring.UI;
public sealed class ColorPalettePresenter : IStartable, IDisposable {
private readonly IColorPaletteView _view;
private readonly ColoringStateRepository _state;
public ColorPalettePresenter(IColorPaletteView view, ColoringStateRepository state) {
_view = view;
_state = state;
}
public void Start() {
_view.SetColors(_state.Palette.Colors);
_view.SetSelected(_state.SelectedIndex);
_view.OnColorButtonClicked += OnClicked;
_state.SelectedIndexChanged += OnIndexChanged;
}
private void OnClicked(int index) => _state.SelectColor(index);
private void OnIndexChanged(int index) => _view.SetSelected(index);
public void Dispose() {
_view.OnColorButtonClicked -= OnClicked;
_state.SelectedIndexChanged -= OnIndexChanged;
}
}
Same shape repeats for every feature's UI.
26. ShapeBuilder — Snap Algorithm
// In ShapePieceView.OnPointerUp:
public void OnDragEnd(Vector2 worldPos) {
var slot = transform.position; // assigned target slot
var d = Vector2.Distance(worldPos, slot);
if (d <= _piece.SnapRadius) {
SnapToSlot();
} else if (d <= _piece.SnapRadius * 1.5f) {
// Toddler grace zone — snap anyway, play happy sound
SnapToSlot();
_audio.PlayOneShot(_clips.NiceTry);
} else {
ReturnToTrayAnimated();
}
}
private void SnapToSlot() {
_locked = true;
transform.DOMove(_piece.SlotPosition, 0.25f).SetEase(Ease.OutBack);
_audio.PlayOneShot(_clips.Snap);
_bus.Publish(new PieceSnappedSignal(_piece.PieceId));
}
Controller listens for PieceSnappedSignal, counts against expected piece count, fires ShapeAssembledSignal when complete.
27. Rendering Order & Sorting
URP 2D with a single ArtCamera ortho cam.
| Sorting Layer | Order | Contents |
|---|---|---|
PaperBackground |
0 | Paper bg sprite (under everything) |
ArtworkRegions |
100 | ColorRegionView sprites (the colorable shapes) |
ArtworkPieces |
200 | ShapePieceView sprites (during build) |
Effects |
300 | Particle bursts, sparkles |
UIWorld |
400 | World-space prompts (rare; mostly Canvas) |
Canvas HUD lives on UICamera (Overlay), never sorts against ArtCamera. Capture renders only ArtCamera's layers → HUD physically cannot leak into saved PNG.
28. SavedArtwork JSON Schema
{
"id": "f3a8e2d4-...",
"templateId": "animals/elephant",
"createdUtc": "2026-05-26T16:42:11Z",
"imagePath": "Gallery/f3a8e2d4-....png",
"thumbnailPath": "Gallery/f3a8e2d4-....thumb.png",
"regions": [
{ "regionId": "body", "color": "#FFB347" },
{ "regionId": "ears", "color": "#FF6961" }
]
}
regions[] lets the gallery reopen an artwork for further edits in a future version (out of scope v1, but the schema reserves the field now to avoid migration later).
Paths are relative to persistentDataPath. Never store absolute paths — they change between OS updates on some platforms.
29. Boot & Error Handling
Status: not implemented. No
AppBootclass exists. Today, RootLifetimeScope.cs only iterates installer MonoBehaviours and registers them — nothing runs after that. The block below is the target sequence whenAppBootis added as anIAsyncStartableentry point underApp/Boot/.
AppBoot.StartAsync() (planned — Features/Boot/AppBoot.cs, registered via builder.RegisterEntryPoint<AppBoot>())
├─ try Addressables.InitializeAsync()
│ fail → show "Tap to retry" splash
├─ try preload palette + UI sounds (Addressables labels)
│ fail → log + continue (non-fatal)
├─ try _persistence.LoadAsync()
│ fail → start with empty progression (don't crash)
├─ _scenes.LoadAsync("MainMenu")
└─ done
Toddler-mode error UI:
- One large smiling icon.
- One big "tap" button.
- No text, no error codes.
- A small upper-right gear opens a parent-only diagnostic screen (long-press 3 s to unlock).
30. Setup Checklist (new dev, day one)
- Open
Colorbook.slnat the repo root. - Open
Assets/Darkmatter/Scenes/Boot.unity(currently the only scene wired). - Inspect the
RootLifetimeScopeGameObject — confirm itsserviceModules[]list references the child installer MonoBehaviours (AudioServiceModule,CameraServiceModule,InputServiceModule, etc.). - Hit Play from
Boot.unity. Other scenes (MainMenu,ColorBook,ArtBook) don't exist yet — they're listed in §6 / §4c as planned work. - When new scene scopes land, the same rule applies: never start a scene mid-flow, always enter from
Boot.unityso the root scope exists. - When drawings are authored: duplicate the template folder under
Content/Gameplay/Drawings/<theme>/<id>/, editTemplate.asset(pieces + regions), add to the appropriate Addressables group. - Run
Tests > EditModeandTests > PlayModebefore pushing (test infra not set up yet — see §16).
31. Quick Reference — Class ↔ Layer ↔ Asmdef
| Class | Layer | Asmdef |
|---|---|---|
IDrawingTemplate, ShapePieceDTO, ColorRegionDTO |
Core | Core |
IPaperRig, IArtInputBridge |
Core | Core |
ICommand, IUndoStack |
Core | Core |
BoundedUndoStack |
Libs | Libs.CommandStack |
AddressableAssetProviderService |
Services | Services.Assets |
FileGalleryService |
Services | Services.Gallery |
RenderTextureCaptureService |
Services | Services.Capture |
PaperRig, ArtInputBridge, PaperRigModule |
Features | Features.Paper |
ColoringController, PaintRegionCommand |
Features | Features.Coloring |
ShapeBuilderController, ShapePieceView |
Features | Features.ShapeBuilder |
HistoryController |
Features | Features.History |
ColorBookFlowController |
Features | Features.ColorBookFlow |
GalleryPresenter, GalleryGridView |
Features | Features.ArtBook |
ColorBookLifetimeScope, AppBoot |
App | Darkmatter.App |
If a class's natural home doesn't match its asmdef, the architecture is bent — fix the placement, don't add a reference.
32. Class Reference (Detailed)
Status: target spec, mostly unimplemented. Of everything below, only the following Service classes exist on disk today:
AddressableAssetProviderService,AudioService/SfxPlayer,CameraService,SceneService,InputReaderSO,FirebaseAnalyticsSystem. Everything else (Paper, Drawing, Coloring, History, Capture, Gallery, Progression, ColorBookFlow, ArtBook, AppBoot) is the target shape for when those classes are written. Treat this section as a contract for new code, not documentation of current state.
Canonical breakdown of every concrete class and interface. For each: purpose, public surface (signatures), injected dependencies, and collaborators (signals or interfaces it talks to).
Convention used below
// fields:= constructor-injected dependencies// pub:= events / signals fired// sub:= events / signals consumed- All async returns are
UniTaskunless noted.- Folder labels follow the actual nesting pattern:
Core/Contracts/Features/<Name>/,Core/Contracts/Services/<Name>/,Core/Data/Dynamic/Features/<Name>/,Features/<Name>/<Sub>/,Services/<Name>/<Sub>/.
32.1 Core Contracts
Pure interfaces and DTOs. Zero logic.
IDrawingTemplate (Core/Contracts/Features/Drawing — planned)
Immutable view of a single drawing's authored data.
public interface IDrawingTemplate {
string Id { get; } // e.g. "animals/elephant"
string DisplayName { get; } // user-facing
Sprite Thumbnail { get; } // 256×256 for catalog grid
Sprite PaperBackground { get; } // composited under artwork
IReadOnlyList<ShapePieceDTO> Pieces { get; } // for ShapeBuilder
IReadOnlyList<ColorRegionDTO> Regions { get; } // for Coloring
}
Implemented by DrawingTemplateSO (ScriptableObject) loaded via Addressables.
IDrawingTemplateCatalog (Core/Contracts/Features/Drawing — planned)
Authority on which drawings exist, completion state, and "next" selection.
public interface IDrawingTemplateCatalog {
UniTask InitializeAsync(); // pulls manifest from Addressables
IReadOnlyList<string> AllTemplateIds { get; }
UniTask<Sprite> GetThumbnailAsync(string id); // cheap; for grid
UniTask<IDrawingTemplate> LoadAsync(string id); // expensive; full template
void Release(string id); // Addressables ref count down
string NextUnseen(string currentId); // progression hint
}
IColorPalette (Core/Contracts/Features/Coloring — planned)
Set of colors offered to the child. Authored as ColorPaletteSO.
public interface IColorPalette {
string Id { get; }
IReadOnlyList<Color> Colors { get; } // 6–10 entries typical
}
ICommand & IUndoStack (Core/Contracts/Features/History — planned)
Already shown in section 8. Each undoable user action is one ICommand; the stack is bounded.
IGalleryService (Core/Contracts/Services/Gallery — planned)
Persistent store of saved artwork PNGs.
public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync(); // sorted newest first
UniTask<Texture2D> LoadFullAsync(string artworkId); // for fullscreen view
UniTask<Texture2D> LoadThumbnailAsync(string artworkId);
UniTask DeleteAsync(string artworkId);
}
ICaptureService (Core/Contracts/Services/Capture — planned)
Snapshots the paper RT to a PNG blob. No arguments — dimensions and content come from IPaperRig.Surface.
public interface ICaptureService {
UniTask<byte[]> CaptureAsync();
}
IPaperRig (Core/Contracts/Features/Paper — planned)
Shared art rig. The single source of truth for everything that lives in the drawing world.
public interface IPaperRig {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface
RenderTexture Surface { get; } // 2048×2048 ARGB32 — the paper itself
Transform PaperRoot { get; } // parent of regions/pieces/paper bg
Vector2 DesignSize { get; } // world units, e.g. (20, 20)
Rect DesignRect { get; } // centered on origin
}
IArtInputBridge (Core/Contracts/Features/Paper — planned)
Converts screen-space pointer coords to art-world coords inside the RT.
public interface IArtInputBridge {
bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos);
}
Returns false when the pointer is outside the displayed RawImage rect (toddler tapped the HUD or backdrop). Every art-world raycast goes through this.
IProgressionService (Core/Contracts/Features/Progression — planned)
Tracks which templates the child has completed and what they last opened.
public interface IProgressionService {
UniTask LoadAsync();
UniTask SaveAsync();
IReadOnlyCollection<string> CompletedTemplateIds { get; }
string LastOpenedTemplateId { get; }
void MarkCompleted(string templateId);
void SetLastOpened(string templateId);
}
IAssetProviderService (Core/Contracts/Services/Assets — ✅ exists)
Addressables wrapper. Hides handle bookkeeping from features.
public interface IAssetProviderService {
UniTask InitializeAsync();
UniTask<T> LoadAsync<T>(string address) where T : UnityEngine.Object;
UniTask<IReadOnlyList<T>> LoadByLabelAsync<T>(string label) where T : UnityEngine.Object;
void Release(string address);
void ReleaseAll();
}
IEventBus (Libs/Observer — ✅ exists; note the folder is Observer, not EventBus)
public interface IEventBus {
void Publish<T>(T signal) where T : struct;
IDisposable Subscribe<T>(Action<T> handler) where T : struct;
}
Signals are structs to avoid GC. Disposable subscription so presenters can unsubscribe in Dispose().
32.2 Services Layer
Concrete infrastructure. One implementation each. All singletons in RootLifetimeScope, registered via per-service MonoBehaviour, IServiceModule installers.
AddressableAssetProviderService (Services/Assets — ✅ exists)
Implements IAssetProviderService.
- Responsibility: Wrap
Addressables.LoadAssetAsync<T>and ref-count handles by address. - State:
Dictionary<string, AsyncOperationHandle>keyed by address. - Notes:
Release(address)decrements;ReleaseAll()for scene teardown. Initialization must complete before any other service may load.
FileGalleryService (Services/Gallery — planned)
Implements IGalleryService.
// fields:
// IPathProvider _paths (wraps Application.persistentDataPath for tests)
// IThumbnailGenerator _thumb (downscale + encode)
// IEventBus _bus
// pub: ArtworkSavedSignal, ArtworkDeletedSignal
- Save flow: write
{guid}.png.tmp→ fsync → rename; generate thumbnail on a worker; write sidecar JSON last (so partial saves are detectable by absence of JSON). - List flow: enumerate
*.jsoninGallery/, deserialize, sort byCreatedUtc desc. - Delete flow: delete png + thumb + json; missing files ignored (idempotent).
RenderTextureCaptureService (Services/Capture — planned)
Implements ICaptureService.
- Steps: allocate
RenderTexture(width, height, 0, ARGB32)→ bind toartCamera.targetTexture→artCamera.Render()→ReadPixelsintoTexture2D→ compositepaperBackgroundunderneath (single shader blit) →EncodeToPNG→ release RT + textures. - Threading: PNG encode happens on a
UniTask.RunOnThreadPoolto avoid hitching the main thread on tablets. - Sizing: default 2048², overridable. Capped at device max texture size.
JsonPersistenceService (Services/Persistence — planned; today Libs/PlayerPrefs covers small-key state)
Implements IPersistenceService (small JSON blob; not the gallery).
public interface IPersistenceService {
UniTask<T> LoadAsync<T>(string key) where T : class, new();
UniTask SaveAsync<T>(string key, T value);
}
- Path:
Application.persistentDataPath/save.json. - Format: single JSON object keyed by
keyso multiple services can share one file. - Atomicity: write to
save.json.tmp→ rename.
SceneService (Services/Scenes — ✅ exists)
Implements ISceneService. Wraps SceneManager.LoadSceneAsync with UniTask plus a fade-curtain.
public interface ISceneService {
UniTask LoadAsync(string sceneName, LoadSceneMode mode = LoadSceneMode.Single);
UniTask UnloadAsync(string sceneName);
}
AudioService (Services/Audio — ✅ exists; see also SfxPlayer)
Implements IAudioService. Plays SFX clips loaded by address, mixes via Unity AudioMixer groups.
public interface IAudioService {
UniTask PreloadAsync(string label); // e.g. "sfx,ui"
void PlayOneShot(string clipId, float volume = 1f);
void SetCategoryVolume(AudioCategory cat, float v01);
}
Holds an internal Dictionary<string, AudioClip> populated at preload.
InputReaderSO (Services/Inputs/Readers — ✅ exists)
ScriptableObject wrapping the new Input System; exposes events.
public interface IInputReader {
event Action<Vector2> PointerDown; // world pos
event Action<Vector2> PointerDrag;
event Action<Vector2> PointerUp;
}
- Why an SO: assignable in inspector and survives scene loads, but still resolvable via DI (
builder.RegisterInstance(_inputReader).As<IInputReader>()).
32.3 Libs
Generic, project-agnostic utilities.
BoundedUndoStack (Libs/CommandStack — planned)
Implements IUndoStack. Source already in section 24.
- Capacity: default 20.
- Invariant:
_redocleared on any newPush. - Edge cases:
Undo/Redoon empty stack is a no-op (never throws).
EventBus (Libs/Observer — ✅ exists)
Implements IEventBus with a Dictionary<Type, Delegate> of Action<T> per signal type.
- Subscribe returns an
IDisposablethat removes the handler onDispose. - Publish snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch).
StateMachine / IState / State (Libs/FSM — ✅ exists)
Generic state machine. Current shape on disk uses IState / State / StateMachine (see Libs/FSM/). ColorBookFlowController (planned) will use this. The generic sketch below is the target shape if you decide to make it strongly-typed via an enum — verify against actual API before consuming.
public sealed class Fsm<TState> where TState : struct, Enum {
public TState Current { get; }
public event Action<TState, TState> Transitioned;
public void Bind(TState state, IFsmState handler);
public void Go(TState next); // calls Exit on old, Enter on new
}
public interface IFsmState { void Enter(); void Exit(); }
32.4 Feature — DrawingCatalog (planned)
DrawingCatalogController (Systems)
Headless logic. Owns the list of template IDs visible in the grid.
// fields: IDrawingTemplateCatalog _catalog, IEventBus _bus
public sealed class DrawingCatalogController : IAsyncStartable {
public IReadOnlyList<string> VisibleIds { get; }
public event Action ListChanged;
public UniTask StartAsync(CancellationToken ct); // pulls catalog, refreshes list
public void OnTemplateSelected(string id); // bus.Publish(new DrawingSelectedSignal(id))
}
// pub: DrawingSelectedSignal
DrawingCatalogPresenter (UI)
Bridges controller ↔ view.
// fields: IDrawingCatalogView _view, DrawingCatalogController _ctrl, IDrawingTemplateCatalog _catalog
public sealed class DrawingCatalogPresenter : IStartable, IDisposable {
public void Start(); // wires view.OnItemClicked → _ctrl.OnTemplateSelected
public void Dispose();
}
- Thumbnail load:
_catalog.GetThumbnailAsync(id)per visible cell, with placeholder while loading.
DrawingCatalogView : MonoBehaviour (UI)
Implements IDrawingCatalogView. Pure setters + click event.
public interface IDrawingCatalogView {
event Action<string> OnItemClicked;
void SetItems(IReadOnlyList<CatalogItemVM> items); // vm = id + Sprite thumbnail
}
32.5 Feature — ShapeBuilder (planned)
ShapeBuilderController (Systems)
Spawns shape pieces for the selected template, tracks snap progress, fires ShapeAssembledSignal when complete.
// fields: IDrawingTemplateCatalog _catalog, ShapePieceFactory _factory, IEventBus _bus, ShapeBuilderConfig _cfg
public sealed class ShapeBuilderController : IDisposable {
public IReadOnlyList<ShapePieceView> Active { get; }
public UniTask BuildAsync(string templateId); // load template, spawn pieces in tray
public void Reset(); // clear, unsubscribe
}
// sub: DrawingSelectedSignal
// pub: ShapeAssembledSignal
- Internal: counts
PieceSnappedSignalagainst expected piece count.
ShapePieceView : MonoBehaviour (Views)
World-space draggable sprite with collider. Source for snap-or-return logic shown in section 26.
public sealed class ShapePieceView : MonoBehaviour {
public string PieceId { get; }
public bool IsLocked { get; }
public event Action<string> Snapped; // raised when piece locks into slot
public void Initialize(ShapePieceDTO dto, IInputReader input, IAudioService audio);
}
- No public mutators for position once locked — controller treats
IsLockedas the source of truth.
ShapePieceFactory (Systems)
Instantiates ShapePieceView prefabs from a pool. Avoids re-instantiating across "Next" cycles on the same template family.
public sealed class ShapePieceFactory {
public ShapePieceView Spawn(ShapePieceDTO dto, Transform parent);
public void Despawn(ShapePieceView view);
}
32.5b Feature — Paper (planned)
The shared art rig — RT, offscreen camera, screen↔world bridge. Every other feature in the ColorBook scene resolves IPaperRig and IArtInputBridge from DI and never touches Screen.* or Camera.* directly.
PaperRig : MonoBehaviour, IPaperRig (Rig)
Scene-bound component placed on a GameObject in ColorBook.unity. Owns the RT lifecycle.
// inspector fields:
// Camera _artCamera (Orthographic, aspect=1, fixed ortho size)
// Transform _paperRoot (parent of regions/pieces)
// Vector2 _designSize = (20, 20) (world units; matches 2048×2048 at PPU=100)
// int _surfaceSize = 2048 (RT side length, square)
public sealed class PaperRig : MonoBehaviour, IPaperRig {
public Camera ArtCamera => _artCamera;
public RenderTexture Surface => _surface;
public Transform PaperRoot => _paperRoot;
public Vector2 DesignSize => _designSize;
public Rect DesignRect => new(-_designSize / 2f, _designSize);
}
- Awake: allocate
_surface = new RenderTexture(_surfaceSize, _surfaceSize, 0, ARGB32) { name = "PaperSurface" };then_surface.Create()and_artCamera.targetTexture = _surface; _artCamera.aspect = 1f; _artCamera.orthographicSize = _designSize.y / 2f;. - OnDestroy:
_surface.Release(); Object.Destroy(_surface);. - No update logic — the camera renders every frame automatically because
targetTextureis set. - Important:
_artCamera'sorthographicSizeandaspectare set once and never touched again. The RT contents are deterministic.
ArtInputBridge : MonoBehaviour, IArtInputBridge (Input)
Lives on the same UI Canvas as the paper RawImage.
// inspector fields:
// RawImage _paperImage (the on-screen paper)
// RectTransform _paperRect (== _paperImage.rectTransform)
// Camera _uiCamera (Canvas event camera)
// IPaperRig _rig (injected via VContainer + IInjectable, or resolved in Start)
public bool TryScreenToArtWorld(Vector2 screenPos, out Vector2 artWorldPos) {
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
_paperRect, screenPos, _uiCamera, out var local)) {
artWorldPos = default; return false;
}
var rect = _paperRect.rect;
var uv = new Vector2(
(local.x - rect.xMin) / rect.width,
(local.y - rect.yMin) / rect.height);
if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1) {
artWorldPos = default; return false;
}
artWorldPos = _rig.ArtCamera.ViewportToWorldPoint(uv);
return true;
}
- Returns
falsewhen the toddler tapped outside the RawImage (HUD button area, backdrop, off-screen). - Used by every feature that does world-space picking —
Coloring,ShapeBuilder, and any future feature like stickers.
PaperRigModule : MonoBehaviour, IServiceModule (Installers)
Scene-scoped installer. Dragged onto ColorBookLifetimeScope._installers[].
// inspector fields:
// PaperRig _rig
// ArtInputBridge _bridge
public void Register(IContainerBuilder builder) {
builder.RegisterInstance<IPaperRig>(_rig);
builder.RegisterInstance<IArtInputBridge>(_bridge);
}
- Registers as
Instancebecause both are MonoBehaviours already in the scene. - Lifetime is implicitly tied to the scene (Unity destroys them on unload).
32.6 Feature — Coloring (planned)
ColoringStateRepository (Repository)
In-memory model. Owns "currently selected color" and the palette in use.
public sealed class ColoringStateRepository {
public IColorPalette Palette { get; private set; }
public int SelectedIndex { get; private set; }
public Color CurrentColor => Palette.Colors[SelectedIndex];
public event Action<int> SelectedIndexChanged;
public void SetPalette(IColorPalette palette); // resets SelectedIndex to 0
public void SelectColor(int index);
}
- Why a repository: presenter and controller both need to read/write current color; an event-emitting POCO is simpler than wiring two signals.
ColoringController (Systems) — implements IColoringController
Builds and pushes PaintRegionCommand instances; spawns ColorRegionView per region.
// fields: IUndoStack _undo, ColoringStateRepository _state, ColorRegionFactory _factory, IEventBus _bus
public interface IColoringController {
UniTask SpawnRegionsAsync(IDrawingTemplate template);
void PaintRegion(ColorRegionView view); // builds command, pushes to undo stack
void Clear();
}
// sub: ShapeAssembledSignal (via flow controller, not direct)
// pub: ColorAppliedSignal (via PaintRegionCommand)
ColorRegionView : MonoBehaviour (Views)
Sprite + PolygonCollider2D, on Artwork layer. Tapped via Physics2D.OverlapPoint from ColoringInputBinder.
public sealed class ColorRegionView : MonoBehaviour {
public string RegionId { get; }
public Color Color { get; } // current paint
public void Initialize(ColorRegionDTO dto);
public void SetColor(Color c); // setter only; no logic
}
ColoringInputBinder (Systems) — IStartable, IDisposable
Subscribes to IInputReader.PointerDown. On each tap:
_bridge.TryScreenToArtWorld(screenPos, out var artPos)— bail if outside the paper.Physics2D.OverlapPoint(artPos, _artworkMask)against theArtworklayer.- If hit,
ColoringController.PaintRegion(hit.GetComponent<ColorRegionView>()).
// fields: IInputReader _input, IArtInputBridge _bridge, IColoringController _coloring, LayerMask _artworkMask
Note: _bridge is the same instance the entire scene uses — no per-feature coordinate math.
PaintRegionCommand (Commands)
Source in section 23. Holds view, fromColor, toColor, bus. Symmetrical execute/undo.
ColorPaletteView, ColorPalettePresenter (UI)
Sources in section 25. Presenter binds ColoringStateRepository.SelectedIndexChanged ↔ IColorPaletteView.
ColorRegionFactory (Systems)
Mirror of ShapePieceFactory for regions. Pool-friendly.
32.7 Feature — History (planned)
HistoryController (Systems) — IStartable, IDisposable
Owns the per-session IUndoStack (registered scoped, so a new ColorBook scene = new stack).
// fields: IUndoStack _stack, IEventBus _bus
public sealed class HistoryController : IStartable, IDisposable {
public bool CanUndo => _stack.CanUndo;
public bool CanRedo => _stack.CanRedo;
public event Action StateChanged;
public void Undo(); // _stack.Undo() + StateChanged
public void Redo();
// sub: DrawingSelectedSignal → _stack.Clear()
}
HistoryButtonsView : MonoBehaviour (UI)
Two big arrow buttons. Setters only.
public interface IHistoryButtonsView {
event Action UndoClicked;
event Action RedoClicked;
void SetUndoEnabled(bool enabled);
void SetRedoEnabled(bool enabled);
}
HistoryPresenter (UI)
Wires controller StateChanged ↔ view enable/disable; view click events → controller.
32.8 Feature — Capture (planned)
CaptureController (Systems)
The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures.
// fields: ICaptureService _capture, IGalleryService _gallery, IEventBus _bus
public sealed class CaptureController {
public bool IsCapturing { get; }
public UniTask<SavedArtworkDTO> CaptureCurrentAsync(string templateId);
}
// pub: ArtworkCapturedSignal (mid-flow), ArtworkSavedSignal (post-save)
- Flow:
_capture.CaptureAsync()→_gallery.SaveAsync(bytes, templateId)→ publish signals. - Concurrency: sets
IsCapturing = trueon entry; UI binds button enabled to!IsCapturingto prevent double-tap. - No camera or sprite args — capture reads
IPaperRig.Surfacedirectly inside the service.
CaptureButtonPresenter (UI)
Wires button click → CaptureController.CaptureCurrentAsync. Disables button while in progress. Shows toast on ArtworkSavedSignal.
32.9 Feature — Progression (planned)
ProgressionService (Systems) — implements IProgressionService
The only place that knows what "completed" means.
- Persistence: delegates to
IPersistenceServiceunder key"progression". - Load order:
AppBootcallsLoadAsync()early. - Save trigger: after
MarkCompleted, debounced 500 ms to coalesce a burst of "Next" presses.
ProgressionRepository (Repository)
Pure in-memory holder used by the service. Separated so tests can inspect state without going through file IO.
32.10 Feature — ColorBookFlow (planned)
ColorBookFlowController (Systems) — IStartable, IDisposable
The only orchestrator inside the ColorBook scene. Drives the panel FSM: Catalog → Building → Coloring → Done.
// fields:
// IEventBus _bus
// IDrawingTemplateCatalog _catalog
// ShapeBuilderController _builder
// IColoringController _coloring
// CaptureController _capture
// IProgressionService _progression
// ColorBookSceneRefs _refs (panel roots to enable/disable)
// Fsm<ColorBookState> _fsm
-
State table:
State On enter Triggers exit CatalogShow catalog panel DrawingSelectedSignalBuilding_builder.BuildAsync(id)ShapeAssembledSignalColoring_coloring.SpawnRegionsAsync(template)"Next" or "Capture" pressed DoneRun autosave capture, mark completed, Go(Catalog)for nextalways advances -
"Next" sequence:
_capture.CaptureCurrentAsync→_progression.MarkCompleted→_catalog.Release(current)→_catalog.LoadAsync(_catalog.NextUnseen(current))→ re-enterBuilding.
32.11 Feature — ArtBook (planned)
GalleryPresenter (UI) — IAsyncStartable, IDisposable
Lists artworks, opens fullscreen view, deletes, shares.
// fields: IGalleryService _gallery, IGalleryView _view, IExternalShareService _share, IEventBus _bus
- Start:
_gallery.ListAsync()→_view.SetItems(...). - Subscribes to
ArtworkSavedSignalto live-refresh if the user pops back in.
IGalleryView (UI)
public interface IGalleryView {
event Action<string> OnArtworkTapped;
event Action<string> OnDeleteRequested;
event Action<string> OnShareRequested;
void SetItems(IReadOnlyList<GalleryItemVM> items);
void ShowFullscreen(Texture2D full);
void HideFullscreen();
}
IExternalShareService (Core)
Platform plugin shim (iOS Photos / Android MediaStore).
public interface IExternalShareService {
UniTask SaveToCameraRollAsync(byte[] png);
UniTask ShareAsync(byte[] png, string subject);
}
32.12 App Layer
AppBoot (App/Boot — planned; folder doesn't exist yet) — IAsyncStartable
Single entry point. Steps in section 29.
// fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress,
// IAudioService _audio, ISceneService _scenes, BootConfig _cfg
public sealed class AppBoot : IAsyncStartable {
public UniTask StartAsync(CancellationToken ct);
}
LifetimeScopes
RootLifetimeScope— ✅ exists (source). Iterates a serializedMonoBehaviour[] serviceModulesand callsRegisteron eachIServiceModule. Persists for app lifetime.MainMenuLifetimeScope— planned. Same pattern as Root (serialized installer list, no hardcoded registrations).ColorBookLifetimeScope— planned. Same pattern; installer list includesPaperRigModule, feature installers, and the flow controller installer.ArtBookLifetimeScope— planned.
All scope classes are thin: a serialized installer-MonoBehaviour list (+ optional scene refs as separate fields) and a Configure(IContainerBuilder) that iterates and calls Register.
32.13 Cross-cutting types
ColorBookSceneRefs : MonoBehaviour (App — planned)
Aggregates scene-bound Unity references that features need: Camera artCamera, Transform catalogRoot, Transform builderRoot, Transform coloringRoot, RectTransform hudRoot, ColorPaletteView paletteView, HistoryButtonsView historyView. Registered in ColorBookLifetimeScope via builder.RegisterInstance(_sceneRefs) so features don't Find things.
Most of these refs are subsumed by
IPaperRignow (which ownsArtCameraandPaperRoot).ColorBookSceneRefsreduces to the HUD-side refs (palette view, history buttons, panel roots).
IServiceModule (Libs/Installers — ✅ exists)
public interface IServiceModule {
void Register(IContainerBuilder builder);
}
Implemented as MonoBehaviour per feature/service so scopes can drag them in the inspector (CameraServiceModule.cs shows the pattern). The method is Register, not Install — there is no IInstaller in this project.
32.14 Class summary table
| Class | Layer | Role | Key dependencies |
|---|---|---|---|
AppBoot |
App | Startup sequencer | assets, persist, progression, scenes |
RootLifetimeScope |
App | Root DI | configs |
ColorBookLifetimeScope |
App | Scene DI | scene refs, installers |
DrawingCatalogController |
Feature | Grid logic | catalog, bus |
DrawingCatalogPresenter |
Feature | UI bridge | view, controller, catalog |
ShapeBuilderController |
Feature | Piece spawn + snap tracking | catalog, factory, bus, cfg |
ShapePieceView |
Feature | Draggable piece MB | input, audio |
ColoringStateRepository |
Feature | Current color model | — |
ColoringController |
Feature | Region spawn + paint cmd | undo, state, factory, bus |
ColorRegionView |
Feature | Region sprite MB | — |
PaintRegionCommand |
Feature | Undoable paint | view, bus |
PaperRig |
Feature | RT + ArtCamera owner | — |
ArtInputBridge |
Feature | Screen→art-world picking | rig, raw image, ui cam |
PaperRigModule |
Feature | DI registration | rig, bridge |
HistoryController |
Feature | Undo/redo facade | undo stack, bus |
CaptureController |
Feature | Capture+save orchestration | capture svc, gallery, bus |
ColorBookFlowController |
Feature | Scene FSM | bus, catalog, builder, coloring, capture, progression |
GalleryPresenter |
Feature | Art book listing | gallery, share, view, bus |
BoundedUndoStack |
Lib | Capped undo store | — |
EventBus |
Lib | Pub/sub | — |
Fsm<TState> |
Lib | Generic FSM | — |
AddressableAssetProviderService |
Service | Addressables wrapper | — |
FileGalleryService |
Service | Gallery file IO | paths, thumb gen, bus |
RenderTextureCaptureService |
Service | PNG render from rig.Surface | paper rig |
JsonPersistenceService |
Service | Settings/progression IO | — |
SceneService |
Service | Async scene loads | — |
AudioService |
Service | SFX playback | assets |
ProgressionService |
Service | Completion tracking | persistence |
If you add a class not in this table, add it here in the same PR. This table is the cheap mental-model index — keep it honest.
Today only these rows are real on disk:
RootLifetimeScope(App),AddressableAssetProviderService(Service),AudioService(Service),CameraService(Service),SceneService(Service),InputReaderSO(Service), plus the Firebase analytics class, plus theLibs.*entries (EventBus,StateMachine,IServiceModule, PlayerPrefs lib, UI toggles). Everything else is the target.