Savya Bikram Shah 8a018d14c5 added readme
2026-05-26 17:23:58 +05:45
2026-05-26 16:47:16 +05:45
2026-05-26 16:47:16 +05:45
2026-05-26 16:47:16 +05:45
2026-05-26 16:47:16 +05:45
2026-05-26 17:23:58 +05:45
2026-05-26 16:47:16 +05:45
2026-05-26 17:23:58 +05:45

Color Book — Architecture Guide

A toddler-targeted (ages 26) 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 ContentCode/ 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 ICommand so 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 IEventBus signals or Core contracts.
  • Core may not contain logic. Interfaces, enums, DTOs, signal records only.
  • Core may reference UniTask for async contract signatures. Nothing else.

4. Folder Structure

Assets/Darkmatter/Code/
├── App/
│   ├── Boot/
│   │   └── AppBoot.cs
│   └── LifetimeScopes/
│       ├── RootLifetimeScope.cs
│       ├── MainMenuLifetimeScope.cs
│       ├── ColorBookLifetimeScope.cs
│       └── ArtBookLifetimeScope.cs
│
├── Core/
│   ├── Drawing/
│   │   ├── IDrawingTemplate.cs
│   │   ├── IDrawingTemplateCatalog.cs
│   │   ├── ShapePieceDTO.cs
│   │   └── ColorRegionDTO.cs
│   ├── Coloring/
│   │   ├── IColorPalette.cs
│   │   └── PaintCommandDTO.cs
│   ├── History/
│   │   ├── ICommand.cs
│   │   └── IUndoStack.cs
│   ├── Gallery/
│   │   ├── IGalleryService.cs
│   │   └── SavedArtworkDTO.cs
│   ├── Capture/
│   │   └── ICaptureService.cs
│   ├── Progression/
│   │   └── IProgressionService.cs
│   └── Signals/
│       ├── DrawingSelectedSignal.cs
│       ├── ShapeAssembledSignal.cs
│       ├── ColorAppliedSignal.cs
│       ├── ArtworkCapturedSignal.cs
│       └── ArtworkSavedSignal.cs
│
├── Libs/
│   ├── CommandStack/          (generic bounded undo/redo)
│   ├── EventBus/              (shared with bus game if monorepo)
│   └── FSM/                   (optional, for ColorBookFlow)
│
├── Services/
│   ├── Audio/
│   ├── Inputs/
│   ├── Assets/                (Addressables wrapper — IAssetProviderService)
│   ├── Scenes/
│   ├── Persistence/           (JSON / PlayerPrefs for non-image state)
│   ├── Gallery/               (file IO — PNG + sidecar JSON)
│   └── Capture/               (RenderTexture → PNG, paper bg compositing)
│
└── Features/
    ├── MainMenu/
    ├── DrawingCatalog/
    ├── ShapeBuilder/
    ├── Coloring/
    ├── History/
    ├── Capture/
    ├── Progression/
    ├── ColorBookFlow/         (orchestrates panel swap, next, capture chain)
    └── ArtBook/

Per-feature folder layout

Every feature follows the same internal shape:

Features/[Name]/
├── Installers/        IInstaller — VContainer registrations
├── Systems/           Controllers, services (pure C#)
├── Repository/        In-memory state holders
├── Commands/          ICommand implementations (if feature mutates undoable state)
├── UI/
│   ├── *Presenter.cs  Pure C#, listens to model, drives view
│   └── *View.cs       MonoBehaviour, setters only
├── Views/             World-space MonoBehaviours (sprites, colliders)
└── Docs/              Feature-specific markdown

Asset folder parallel

Assets/Darkmatter/Contents/
├── Drawings/
│   ├── Animals/<id>/
│   │   ├── Template.asset           (DrawingTemplateSO)
│   │   ├── Pieces/*.png
│   │   ├── Regions/*.png
│   │   └── PaperBackground.png
│   └── Vehicles/...
├── Palettes/*.asset                 (ColorPaletteSO)
├── Audio/
│   ├── UI/                          (tap, swipe, button)
│   └── Coloring/                    (fill, complete, sparkle)
└── UI/                              (HUD prefabs, fonts, icons)

5. Namespaces

Darkmatter.[Layer].[Module]

  • Darkmatter.Features.Coloring
  • Darkmatter.Features.ShapeBuilder
  • Darkmatter.Services.Gallery
  • Darkmatter.Services.Capture
  • Darkmatter.Core.Drawing
  • Darkmatter.Lib.CommandStack

Each maps 1:1 to a .asmdef.


6. Scenes & Lifetime Scopes

Scene Scope Contents
Boot.unity RootLifetimeScope All Services + IEventBus. Persists forever.
MainMenu.unity MainMenuLifetimeScope Menu presenter, art book entry.
ColorBook.unity ColorBookLifetimeScope DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow.
ArtBook.unity ArtBookLifetimeScope Gallery presenter, viewer, share.

Scopes nest: Root → (MainMenu | ColorBook | ArtBook). Services resolved from the root parent. Scene scopes only register their own features.

Boot chain

AppBoot runs once, in order:

  1. Initialize IAssetProviderService (Addressables init).
  2. Preload essential bundles (palettes, UI sounds).
  3. Load IProgressionService from disk.
  4. Load MainMenu scene.

Failures show a child-friendly retry screen; never crash.


7. Rendering Strategy

Hybrid: Sprites for artwork, Canvas for HUD.

Cameras

Camera Type Culling Mask Purpose
ArtCamera Orthographic Artwork, PaperBackground Renders the drawing only. Source for capture.
UICamera Overlay (or screen-space) UI HUD canvas, palette, buttons.

Layers

Layer Used by
Artwork Drawing region sprites, shape pieces, paper bg
UI All Canvas elements
Effects Particle bursts, sparkles on completion

Why hybrid

Need Choice Why
Per-region tap-to-fill Sprites + PolygonCollider2D Clean Physics2D.OverlapPoint; deterministic; no shader work for the toddler region count (520).
Drag/drop shape pieces Sprites + Physics2D Natural world bounds, easy snap distance checks.
Capture to PNG with paper bg Sprites under dedicated Camera RenderTexture from ArtCamera excludes HUD automatically.
Color palette, buttons Canvas Anchors handle aspect ratios. Buttons + ScrollRect free.
Drawing catalog grid Canvas GridLayoutGroup + ScrollRect, async thumbnail loader.

8. Core Contracts

All Core types are pure data or interfaces.

Drawing

namespace Darkmatter.Core.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

namespace Darkmatter.Core.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; }
}

History

namespace Darkmatter.Core.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();
}
namespace Darkmatter.Core.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; }
}

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.Capture;

public interface ICaptureService {
    UniTask<byte[]> CaptureAsync(Camera artCamera, Sprite paperBackground, int width = 2048, int height = 2048);
}

Signals

namespace Darkmatter.Core.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.

ShapeBuilder

  • Listens to DrawingSelectedSignal.
  • Loads template via IDrawingTemplateLoader, instantiates shape pieces at random off-slot positions.
  • Per piece: drag with ShapePieceView (sprite + collider). On drop, check distance to SlotPosition against SnapRadius; if within, snap and lock.
  • Fires ShapeAssembledSignal when all pieces locked.

Coloring

  • Listens to ShapeAssembledSignal.
  • Spawns one ColorRegionView per ColorRegionDTO (sprite + polygon collider on Artwork layer).
  • Listens to palette selection (current color held in ColoringStateRepository).
  • On region tap: builds PaintRegionCommand(regionId, oldColor, newColor), pushes to IUndoStack.
  • Command sets SpriteRenderer.color on undo/redo.
  • Fires ColorAppliedSignal for SFX / sparkle effects.

History

  • Owns the singleton IUndoStack for 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 / CanRedo is false.

Capture

  • Bound to the "Capture" button.
  • Calls ICaptureService.CaptureAsync(artCamera, template.PaperBackground) → PNG bytes.
  • Hands bytes to IGalleryService.SaveAsync(...).
  • Fires ArtworkCapturedSignal then ArtworkSavedSignal.
  • 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.MarkCompletedICaptureService autosave → IDrawingTemplateLoader.Release(currentId) → load next.
  • Built as a small FSM (Catalog → Building → Coloring → Done).

ArtBook

  • Separate scene.
  • GalleryPresenter calls IGalleryService.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).
  • ListAsync enumerates sidecar JSONs sorted by CreatedUtc desc.
  • Thumbnail generation happens once at save time on a worker thread.

12. Capture Pipeline

[Capture button or Next button]
        │
        ▼
ICaptureService.CaptureAsync(artCamera, paperBg)
        │
        ├─ Allocate RenderTexture (2048×2048, ARGB32)
        ├─ artCamera.targetTexture = rt
        ├─ Force render (artCamera.Render())
        ├─ ReadPixels into Texture2D
        ├─ Composite paperBg underneath (single shader pass or CPU blend)
        ├─ Encode PNG (Texture2D.EncodeToPNG)
        ├─ Release RT + temp texture
        └─ return byte[]
        ▼
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 ArtCamera only renders the Artwork layer.
  • Paper background can either be already present in the scene (cheap) or composited at capture time (lets the same drawing be saved with different papers).

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.
  • ViewMonoBehaviour, 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

  1. Is it a cross-assembly interface / enum / DTO?Core/
  2. Is it a generic, sellable utility?Libs/
  3. Is it infrastructure (input, audio, file IO, addressables, capture)?Services/
  4. Is it gameplay logic specific to coloring books?Features/
  5. 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 BrushStrokeCommand and a dynamic texture per region; out of scope for v1.
  • Multi-child profiles — single-profile for v1; multi-profile would slot in behind IProgressionService and IGalleryService keyed by profileId.
  • 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 folder under Code/ is its own .asmdef. References follow the layer rules exactly.

Asmdef Path References
Darkmatter.App App/ All Features, all Services, Core, Libs
Darkmatter.Core Core/ (none — UniTask allowed in async signatures)
Darkmatter.Lib.CommandStack Libs/CommandStack/ Darkmatter.Core
Darkmatter.Lib.EventBus Libs/EventBus/ Darkmatter.Core
Darkmatter.Lib.FSM Libs/FSM/ Darkmatter.Core
Darkmatter.Services.Audio Services/Audio/ Darkmatter.Core
Darkmatter.Services.Inputs Services/Inputs/ Darkmatter.Core
Darkmatter.Services.Assets Services/Assets/ Darkmatter.Core
Darkmatter.Services.Scenes Services/Scenes/ Darkmatter.Core
Darkmatter.Services.Persistence Services/Persistence/ Darkmatter.Core
Darkmatter.Services.Gallery Services/Gallery/ Darkmatter.Core
Darkmatter.Services.Capture Services/Capture/ Darkmatter.Core
Darkmatter.Features.MainMenu Features/MainMenu/ Darkmatter.Core, Libs
Darkmatter.Features.DrawingCatalog Features/DrawingCatalog/ Darkmatter.Core, Libs
Darkmatter.Features.ShapeBuilder Features/ShapeBuilder/ Darkmatter.Core, Libs
Darkmatter.Features.Coloring Features/Coloring/ Darkmatter.Core, Lib.CommandStack
Darkmatter.Features.History Features/History/ Darkmatter.Core, Lib.CommandStack
Darkmatter.Features.Capture Features/Capture/ Darkmatter.Core
Darkmatter.Features.Progression Features/Progression/ Darkmatter.Core
Darkmatter.Features.ColorBookFlow Features/ColorBookFlow/ Darkmatter.Core, Lib.FSM
Darkmatter.Features.ArtBook Features/ArtBook/ Darkmatter.Core

Hard rule: No Service asmdef references any Feature asmdef. No Feature asmdef references another Feature asmdef. Compiler enforces the architecture.


21. LifetimeScope Concrete Sample

RootLifetimeScope (Boot scene, persists forever)

namespace Darkmatter.App.LifetimeScopes;

public sealed class RootLifetimeScope : LifetimeScope {
    [SerializeField] private AudioServiceConfig _audioConfig;
    [SerializeField] private InputReaderSO _inputReader;

    protected override void Configure(IContainerBuilder builder) {
        // EventBus
        builder.Register<IEventBus, EventBus>(Lifetime.Singleton);

        // Services
        builder.RegisterInstance(_inputReader).As<IInputReader>();
        builder.Register<IAudioService, AudioService>(Lifetime.Singleton)
               .WithParameter(_audioConfig);
        builder.Register<IAssetProviderService, AddressableAssetProviderService>(Lifetime.Singleton);
        builder.Register<ISceneService, SceneService>(Lifetime.Singleton);
        builder.Register<IPersistenceService, JsonPersistenceService>(Lifetime.Singleton);
        builder.Register<IGalleryService, FileGalleryService>(Lifetime.Singleton);
        builder.Register<ICaptureService, RenderTextureCaptureService>(Lifetime.Singleton);

        // App entry
        builder.RegisterEntryPoint<AppBoot>();
    }
}

ColorBookLifetimeScope (per-scene, child of Root)

namespace Darkmatter.App.LifetimeScopes;

public sealed class ColorBookLifetimeScope : LifetimeScope {
    [SerializeField] private ColorBookSceneRefs _sceneRefs;   // ArtCamera, panel roots, prefabs
    [SerializeField] private IInstaller[] _installers;        // assigned in inspector

    protected override void Configure(IContainerBuilder builder) {
        builder.RegisterInstance(_sceneRefs);

        // Each feature ships an IInstaller
        foreach (var installer in _installers) installer.Install(builder);

        // Scene-scoped orchestrator
        builder.RegisterEntryPoint<ColorBookFlowController>();
    }
}

Drag these installers in the inspector:

  • DrawingCatalogServiceModule
  • ShapeBuilderServiceModule
  • ColoringServiceModule
  • HistoryServiceModule
  • CaptureFeatureModule
  • ProgressionServiceModule

22. Installer Pattern — Concrete Coloring Sample

namespace Darkmatter.Features.Coloring.Installers;

[CreateAssetMenu(menuName = "Darkmatter/Installers/Coloring")]
public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
    [SerializeField] private ColoringConfig _config;

    public void Install(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 IInstaller per feature.
  • ScriptableObject so it can be referenced by scene scope inspector.
  • Registers only its own types. Never touches another feature's types.

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

AppBoot.StartAsync()
  ├─ 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)

  1. Open Bus Game.sln (color book lives in same repo / Unity project per plan).
  2. Verify Addressables groups exist: Drawings_*, Palettes, Audio_*.
  3. Open Boot.unity → confirm RootLifetimeScope references the right configs.
  4. Open ColorBook.unity → confirm ColorBookLifetimeScope._installers[] is fully populated.
  5. Hit Play from Boot.unity (entry scene). Never start mid-flow — DI parent scope must exist.
  6. To author a new drawing: duplicate Animals/elephant/, edit Template.asset (pieces + regions), add to the appropriate Addressables group.
  7. Run Tests > EditMode and Tests > PlayMode before pushing.

31. Quick Reference — Class ↔ Layer ↔ Asmdef

Class Layer Asmdef
IDrawingTemplate, ShapePieceDTO, ColorRegionDTO Core Darkmatter.Core
ICommand, IUndoStack Core Darkmatter.Core
BoundedUndoStack Libs Darkmatter.Lib.CommandStack
AddressableAssetProviderService Services Darkmatter.Services.Assets
FileGalleryService Services Darkmatter.Services.Gallery
RenderTextureCaptureService Services Darkmatter.Services.Capture
ColoringController, PaintRegionCommand Features Darkmatter.Features.Coloring
ShapeBuilderController, ShapePieceView Features Darkmatter.Features.ShapeBuilder
HistoryController Features Darkmatter.Features.History
ColorBookFlowController Features Darkmatter.Features.ColorBookFlow
GalleryPresenter, GalleryGridView Features Darkmatter.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.

Description
No description provided
Readme 731 MiB
Languages
C# 87%
ShaderLab 7.2%
HLSL 3.3%
GLSL 2.5%