Files
2026-07-21 08:56:10 +03:00

131 lines
10 KiB
Markdown

# Unity 3D Meccano Editor - Technical Documentation
## 1. Architecture Overview
The Meccano Editor is built on a highly modular, event-driven architecture using several core design patterns to ensure decoupling and maintainability. At its heart, the system uses a custom **Dependency Injection** container to wire dependencies, the **Command Pattern** for all state-mutating actions (enabling Undo/Redo and Save/Load), and the **Observer Pattern** heavily through UnityEvents to communicate between layers.
The architecture separates concerns into distinct layers:
- **Data & Environment:** Manages the source of truth (block registry, instantiated objects).
- **Command Layer:** Encapsulates all modifications to the environment.
- **Interaction Layer:** Handles user intent, ghost block placement, and snapping.
- **Save/Load System:** Relies on command event sourcing to reconstruct the state.
---
## 2. Major Modules and Systems
### 2.1 Dependency Injection (`DependencyInjection/`)
**Purpose:** A custom, reflection-based DI framework that wires up dependencies on `Awake`.
- **Main Classes:** `Injector`, `RuntimeAnchor`, `IDependencyProvider`.
- **Data Flow:** Classes use the `[Inject]` attribute on fields/properties. Providers use `[Provide]`. `Injector` runs with `[DefaultExecutionOrder(-1000)]` to resolve these before `Start()`. It also supports reactive updates for `RuntimeAnchor` changes.
- **Dependencies:** Core framework, independent of other modules.
### 2.2 Blocks Layer (`BlocksLayer/`)
**Purpose:** Acts as the source of truth for the physical world and block definitions.
- **Main Classes:** `Environment`, `Block`, `BlockData`, `BlockFactory`.
- **Responsibility:** `Environment` is a Singleton that registers every instantiated block using a unique UUID. It fires `onBlockAddedEvent` and `onBlockRemovedEvent`. `BlockFactory` handles the instantiation of prefabs from `BlockData` definitions.
### 2.3 Command Layer (`CommandLayer/`)
**Purpose:** Encapsulates all state changes to support undo/redo and serialization.
- **Main Classes:** `CommandManager`, `CommandHandler`, `ICommand`, `CommandDTO`.
- **Data Flow:** UI or Interactors call methods on `CommandHandler` (e.g., `createBlock`, `moveBlock`). The handler wraps the intent in an `ICommand` and passes it to `CommandManager`, which executes it and pushes it onto a `LinkedList` (commandStack).
### 2.4 Interaction Layer (`InteractionLayer/`)
**Purpose:** Translates raw user input into actionable placements, snapping, and selections.
- **Main Classes:** `InteractionManager`, `GhostManager`, `RayInteractor`, `SnapSystem`.
- **Responsibility:**
- `RayInteractor` casts rays to detect `IInteractable` objects or the world grid.
- `GhostManager` handles drag-and-drop workflows. It creates a semi-transparent "ghost" of a block, updates its position via a `IGhostMovementStrategy` (Strategy Pattern), and validates placement.
- `SnapSystem` works with `GhostManager` to find the nearest `SocketPoint` and align the block dynamically.
### 2.5 Sockets & Joints System (`Sockets/`)
**Purpose:** Simulates the mechanical connections (pins and holes) of Meccano pieces.
- **Main Classes:** `SocketPoint`, `SocketContainer`, `JointBlock`.
- **Responsibility:** `SocketPoint` defines a physical connection node (hole or pin) and its radius/compatibility. `SocketContainer` manages multiple sockets on a single piece. `JointBlock` automatically creates Physics Joints (`FixedJoint`, `HingeJoint`, `ConfigurableJoint`) between compatible `SocketPoint`s when blocks are snapped together.
### 2.6 Save / Load System (`SaveSystem/`)
**Purpose:** Persists the user's creation using an Event Sourcing approach.
- **Main Classes:** `EnvironmentObserver`, `CommandSaveHandler`, `CommandSaveUtil`.
- **Responsibility:** Instead of saving the literal positions of every block, it serializes the *history of commands* into `CommandDTO` JSON objects. `EnvironmentObserver` auto-saves when the environment changes. On load, the system clears the environment and re-executes all serialized commands.
### 2.7 Model Checking System (`ModelCheckingSystem/`)
**Purpose:** Evaluates the user's current build against a target "solution" map.
- **Main Classes:** `ModelCheckManager`, `BlocksMap`, `BlockNode`.
- **Responsibility:** Listens to the `Environment`. When a block is added, it creates a `BlockNode` (quantized position/rotation) and compares it against the `BlocksMap` (ScriptableObject representing the goal). It fires events when a piece is correct or when the entire model is complete.
---
## 3. Important Design Patterns
- **Command Pattern:** Used in `CommandLayer` to encapsulate requests (`CreateBlockCommand`, `MoveBlockCommand`). Enables Undo/Redo.
- **Event Sourcing:** The Save/Load system works by recording and replaying Commands rather than dumping scene state.
- **Observer Pattern:** Implemented via `UnityEvent` across the codebase (e.g., `Environment.onBlockAddedEvent`, Input actions).
- **Singleton Pattern:** Heavily used for Managers (`CommandManager`, `Environment`, `InteractionManager`, `GhostManager`).
- **Strategy Pattern:** Used in `GhostManager` via `IGhostMovementStrategy` (e.g., `FreeMoveStrategy`, `AxisMoveStrategy`) to easily swap how an object moves during drag-and-drop.
- **Dependency Injection:** Custom implementation (`Injector.cs`) to decouple services and handle reactive state anchors.
---
## 4. Systems Deep-Dive
### 4.1 Serialization and Save-Load (Event Sourcing)
The save system does not save GameObjects. When auto-saving triggers (`EnvironmentObserver.onChangeEvent`), it loops through the `Environment`'s `blockRegistry` and generates a `CreateBlockCommand` for each block's current state. These are mapped to `CommandDTO` structs and serialized to `commands.json`.
*Loading* wipes the environment, parses the JSON back into `CommandDTO`s, converts them to `ICommand`s, and executes them sequentially over a Coroutine to visually rebuild the scene.
### 4.2 Undo and Redo
`CommandManager` maintains two `LinkedList<ICommand>`s: `commandStack` and `redoStack`.
- **Execute:** Pushes to `commandStack`, clears `redoStack`. Enforces a memory-leak safeguard (`maxCapacity` = 100).
- **Undo:** Pops from `commandStack`, calls `command.undo()`, pushes to `redoStack`.
- **Redo:** Pops from `redoStack`, calls `command.execute()`, pushes to `commandStack`.
### 4.3 Input Handling
Abstracted through `BaseInputProvider` and implemented for PC in `PCIntputProvider`. It maps Unity's new Input System (`InputAction`) to generic UnityEvents (`OnCursorMoved`, `OnPlace`, `OnRotate`).
`InputModeManager` acts as a state stack. Modes like `ObjectDragging` or `UI` can be pushed or popped to suppress or route inputs appropriately (e.g., ignoring world clicks while a UI is open).
### 4.4 Editor Tools & Runtime Separation
The project includes Editor-specific functionalities. For example, in `ModelCheckManager`, there is a NaughtyAttributes button `[Button("Save Current Map as Asset")]` wrapped in `#if UNITY_EDITOR` that allows a developer to build a model in Play Mode, and save it as a `BlocksMap` ScriptableObject to be used as a puzzle target later.
---
## 5. Initialization and Lifecycle Flow
1. **Pre-Awake (`[DefaultExecutionOrder(-1000)]`):** `Injector.Awake()` runs. It scans all `MonoBehaviour`s, finds `IDependencyProvider`s, registers dependencies, and injects them into fields marked `[Inject]`.
2. **Awake:** Singletons instantiate themselves (e.g., `Environment`, `InteractionManager`, `GhostManager`). `VirtualPlatformManager` creates the visual grid.
3. **Start:** Managers wire up their internal listeners. `InteractionManager` maps UI selections to the `GhostManager`. `EnvironmentObserver` binds to the Environment's change event.
4. **Runtime Loop:**
- `RayInteractor` constantly updates `lastHit`.
- On Click -> `GhostManager` either commits a placed block via `CommandHandler`, or `RayInteractor` selects an existing block.
- `GhostManager.Update()` calculates snapping and validates positions via `IValidator`.
- `CommandManager` executes commands -> updates `Environment` -> triggers auto-save via `EnvironmentObserver`.
---
## 6. Extensibility Points: How to Add New Features
### How to Add a New Object Type (Meccano Piece)
1. Create a prefab for the piece. Add the `Block` component.
2. Add `SocketContainer` and `SocketPoint` components to define where pins/holes exist. Set their `SocketType` and `radius`.
3. If the piece moves (like a hinge), add a `JointBlock` component and configure the `JointBlockType`.
4. Create a new `BlockData` ScriptableObject in the `Resources/BlocksData` folder and assign your prefab.
5. `BlockFactory` automatically loads all `BlockData` from Resources on Start.
### How to Add a New Command
1. Create a class implementing `ICommand` (e.g., `ScaleBlockCommand`).
2. Implement `execute()` (forward logic) and `undo()` (reverse logic).
3. Update `CommandDTO` to handle serialization of your new command's specific data (e.g., adding scale vectors).
4. Expose a method in `CommandHandler` to trigger it.
### How to Add Saving Support for New Data
If you modify block state beyond Position/Rotation (e.g., color, scale), you must:
1. Update `CreateBlockCommand` and `MoveBlockCommand` (or create new commands) to store this data.
2. Update the `CommandDTO` class to serialize these new fields.
3. Modify `EnvironmentObserver.addAllCommands()` to capture this new state when generating the snapshot.
---
## 7. Common Pitfalls and Architectural Weaknesses
1. **Singleton Overuse vs. DI:** The project uses *both* an advanced custom Dependency Injection container (`Injector.cs`) AND widespread traditional Singletons (`Environment.getInstance()`). This can cause confusion about how to access services. Stick to DI for new features to maintain testability.
2. **Save System Bottleneck:** Because the save system relies on Event Sourcing (replaying commands), loading a massive project requires instantiating and executing hundreds of commands sequentially. If projects get very large, a state-snapshot save mechanism might be more performant than event replay.
3. **Socket Physics Initialization:** `JointBlock` creates Unity Physics Joints (`HingeJoint`, etc.) at runtime. Physics glitches can occur if blocks overlap or if the `jointBreakForce` is exceeded. Use `IValidator` strictly to prevent overlapping placements.
4. **Command Memory Leak:** `CommandManager` clears the `commandStack` beyond 100 items to save memory, but `EnvironmentObserver` saves the state by looping through *current* blocks, not the command stack. This means the save file is a fresh snapshot, which is good, but undoing past 100 steps is impossible.