Files
JuegoSim/Assets/_Project/Scripts/Documentation/technical_documentation.md
T
2026-07-21 08:56:10 +03:00

10 KiB

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 SocketPoints 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 CommandDTOs, converts them to ICommands, 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 MonoBehaviours, finds IDependencyProviders, 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.