1050 lines
35 KiB
Markdown
1050 lines
35 KiB
Markdown
# 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](../Assets/Darkmatter_Architecture_Guide.md) 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 `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 (5–20). |
|
||
| 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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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();
|
||
}
|
||
```
|
||
|
||
### Gallery & Capture
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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.MarkCompleted` → `ICaptureService` 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 `Release`d 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.
|
||
- **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:
|
||
|
||
```csharp
|
||
[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](../Assets/Darkmatter_Architecture_Guide.md). 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)
|
||
|
||
```csharp
|
||
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)
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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:
|
||
|
||
```csharp
|
||
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`
|
||
|
||
```csharp
|
||
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)
|
||
|
||
```csharp
|
||
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#)
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
// 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
|
||
|
||
```json
|
||
{
|
||
"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.
|