Initial commit
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
# Unity 3D Meccano Editor - Detailed Class Documentation
|
||||
|
||||
This document provides a deep dive into the most important classes in the Meccano Editor system. It explains their roles, how they fit into the broader architecture, and includes relevant code snippets to illustrate their inner workings.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Command Layer
|
||||
|
||||
The Command Layer is the backbone of the application's state management. It encapsulates all user actions (like creating, moving, or deleting a block) so that they can be tracked, reversed, and serialized.
|
||||
|
||||
### `ICommand` Interface
|
||||
Every action that modifies the world must implement this interface.
|
||||
|
||||
```csharp
|
||||
public interface ICommand
|
||||
{
|
||||
void execute();
|
||||
void undo();
|
||||
CommandDTO toDTO();
|
||||
}
|
||||
```
|
||||
|
||||
### `CreateBlockCommand`
|
||||
This class implements `ICommand` to handle the instantiation of a new block. Notice how it stores all necessary parameters (prefab ID, position, rotation) so that it can be reversed (`undo`) or serialized (`toDTO`).
|
||||
|
||||
```csharp
|
||||
public class CreateBlockCommand : ICommand
|
||||
{
|
||||
private string prefabId;
|
||||
private Vector3 position;
|
||||
private Quaternion rotation;
|
||||
private string blockId; // Captured after creation
|
||||
|
||||
// Dependencies
|
||||
private BlockFactory factory;
|
||||
private Environment environment;
|
||||
|
||||
public void execute()
|
||||
{
|
||||
// Creates the physical object in the scene
|
||||
GameObject objectCreated = factory.createblock(prefabId, position, rotation, blockId, layerDiff);
|
||||
|
||||
// We capture the generated UUID so we can target it during Undo
|
||||
if (objectCreated != null)
|
||||
{
|
||||
Block blockComponent = objectCreated.GetComponent<Block>();
|
||||
blockId = blockComponent.getBlockId();
|
||||
}
|
||||
}
|
||||
|
||||
public void undo()
|
||||
{
|
||||
// Completely removes the block from the environment registry and destroys it
|
||||
environment.removeBlock(blockId);
|
||||
}
|
||||
|
||||
public CommandDTO toDTO()
|
||||
{
|
||||
return new CommandDTO.CommandDTOBuilder()
|
||||
.SetCommandName("CreateCommand")
|
||||
.AddParameter("prefabId", prefabId)
|
||||
.AddParameter("position", position)
|
||||
.AddParameter("rotation", rotation)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `CommandManager`
|
||||
This Singleton maintains the history of executed commands. It uses two `LinkedList`s to handle the `undo` and `redo` operations efficiently.
|
||||
|
||||
```csharp
|
||||
public class CommandManager
|
||||
{
|
||||
private LinkedList<ICommand> commandStack = new LinkedList<ICommand>();
|
||||
private LinkedList<ICommand> redoStack = new LinkedList<ICommand>();
|
||||
private int maxCapacity = 100; // Prevents memory leaks on long sessions
|
||||
|
||||
public void executeCommand(ICommand command)
|
||||
{
|
||||
command.execute();
|
||||
|
||||
commandStack.AddFirst(command);
|
||||
redoStack.Clear(); // Any new action invalidates the redo stack
|
||||
|
||||
if (commandStack.Count > maxCapacity)
|
||||
{
|
||||
commandStack.RemoveLast();
|
||||
}
|
||||
}
|
||||
|
||||
public void undo()
|
||||
{
|
||||
if (commandStack.Count > 0)
|
||||
{
|
||||
ICommand command = commandStack.First.Value;
|
||||
commandStack.RemoveFirst();
|
||||
command.undo();
|
||||
redoStack.AddFirst(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dependency Injection
|
||||
|
||||
The project uses a custom reflection-based DI container to avoid tightly coupling classes together and relying too heavily on Singletons for internal systems.
|
||||
|
||||
### `Injector`
|
||||
This class runs very early in the Unity lifecycle (`[DefaultExecutionOrder(-1000)]`). It searches for all `MonoBehaviour` instances, collects those implementing `IDependencyProvider`, and injects their provided values into any fields marked with `[Inject]`.
|
||||
|
||||
```csharp
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public class Injector : MonoBehaviour
|
||||
{
|
||||
void Awake()
|
||||
{
|
||||
var monoBehaviours = FindMonoBehaviours();
|
||||
|
||||
// 1. Collect IDependencyProvider components and register what they provide via [Provide]
|
||||
var providers = monoBehaviours.OfType<IDependencyProvider>();
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
RegisterProvider(provider);
|
||||
}
|
||||
|
||||
// 2. Inject all injectable MonoBehaviours (fields with [Inject])
|
||||
var injectables = monoBehaviours.Where(IsInjectable);
|
||||
foreach (var injectable in injectables)
|
||||
{
|
||||
Inject(injectable);
|
||||
}
|
||||
|
||||
// 3. Setup watchers for RuntimeAnchors
|
||||
SetupAnchorWatchers(injectables);
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. The Environment and Save System
|
||||
|
||||
The `Environment` is the source of truth for the physical world. The Save system works by "listening" to the Environment.
|
||||
|
||||
### `Environment`
|
||||
It maintains a `Dictionary` of all active blocks mapped by a UUID.
|
||||
|
||||
```csharp
|
||||
public class Environment : MonoBehaviour
|
||||
{
|
||||
private Dictionary<string, Block> blockRegistry = new Dictionary<string, Block>();
|
||||
|
||||
// Observers listen to these to react to the world state changing
|
||||
public UnityEvent<Block> onBlockAddedEvent;
|
||||
public UnityEvent<Block> onBlockRemovedEvent;
|
||||
public UnityEvent onChangeEvent;
|
||||
|
||||
public void addBlock(Block block, string existingUUID = null)
|
||||
{
|
||||
// 1. Assign ID: Use existing (Undo/Redo) or generate a new one (Fresh placement)
|
||||
string uuid = string.IsNullOrEmpty(existingUUID) ? Guid.NewGuid().ToString() : existingUUID;
|
||||
block.setBlockId(uuid);
|
||||
|
||||
// 2. Register in the dictionary
|
||||
blockRegistry[uuid] = block;
|
||||
|
||||
applyChanges(); // Invokes onChangeEvent
|
||||
onBlockAddedEvent.Invoke(block);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `EnvironmentObserver`
|
||||
This class demonstrates the "Event Sourcing" approach to saving. When `autoSaving` is enabled, it listens to the `onChangeEvent`. Instead of saving GameObjects, it loops through the current registry and builds a fresh stack of `CreateBlockCommand`s, then serializes them.
|
||||
|
||||
```csharp
|
||||
public class EnvironmentObserver : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
environment.onChangeEvent.AddListener(onChange);
|
||||
}
|
||||
|
||||
public void save()
|
||||
{
|
||||
commandSaveHandler.clearCommandList();
|
||||
|
||||
// Loop through everything in the Environment
|
||||
Dictionary<string, Block> blockRegistry = environment.getBlockRegistry();
|
||||
foreach (var blockEntry in blockRegistry)
|
||||
{
|
||||
if (blockEntry.Value != null)
|
||||
{
|
||||
// Create a command representing the current state of the block
|
||||
string prefabId = blockEntry.Value.getBlockData()?.blockName;
|
||||
Vector3 position = blockEntry.Value.transform.position;
|
||||
Quaternion rotation = blockEntry.Value.transform.rotation;
|
||||
|
||||
ICommand createCommand = new CreateBlockCommand(prefabId, position, rotation);
|
||||
|
||||
// Convert to a Data Transfer Object and add to the save queue
|
||||
commandSaveHandler.addLastCommand(createCommand.toDTO());
|
||||
}
|
||||
}
|
||||
|
||||
// Write the queue to JSON
|
||||
commandSaveHandler.saveCommands();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Object Interaction: GhostManager
|
||||
|
||||
The `GhostManager` handles the visual drag-and-drop feedback. It creates a semi-transparent "Ghost" of the object being manipulated.
|
||||
|
||||
```csharp
|
||||
public class GhostManager : MonoBehaviour
|
||||
{
|
||||
// ... State tracking variables
|
||||
private IGhostMovementStrategy currentStrategy = new FreeMoveStrategy();
|
||||
|
||||
public void SetupBlock(BlockData blockData = null)
|
||||
{
|
||||
// Instantiate a fake "ghost" object for the user to drag around
|
||||
GameObject prefab = selectedBlockData.blockPrefab;
|
||||
ghostObject = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation);
|
||||
|
||||
// Make the ghost semi-transparent and ignore physics raycasts
|
||||
SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast"));
|
||||
InitialiseGhostFromObject(ghostObject); // Turns materials translucent
|
||||
|
||||
canPlace = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!HasActiveGhost()) return;
|
||||
|
||||
UpdateSurfaceTracking();
|
||||
|
||||
// Trigger Snapping logic to find nearby sockets
|
||||
snappingSystem.UpdateSockets(currentSurfacePoint, ghostObject.transform);
|
||||
|
||||
// Create a context object to pass into the movement strategy
|
||||
var ctx = new GhostMovementContext(...);
|
||||
|
||||
// Strategy pattern: Allows swapping between free-move, axis-locked, etc.
|
||||
currentStrategy.UpdateMovement(ref ctx);
|
||||
|
||||
// Validate the placement (e.g. are we colliding with something?)
|
||||
isValidPlacement = validator != null ? validator.IsValidPlacement(ghostObject) : true;
|
||||
UpdateVisualFeedback(isValidPlacement); // Turns green or red
|
||||
}
|
||||
|
||||
private void PlaceBlock()
|
||||
{
|
||||
if (!isValidPlacement) return;
|
||||
|
||||
// We do NOT instantiate the real block here! We delegate to the CommandHandler.
|
||||
if (block != null)
|
||||
commandHandler.createBlock(
|
||||
block.getBlockData()?.blockName,
|
||||
ghostObject.transform.position,
|
||||
ghostObject.transform.rotation);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Sockets & Mechanical Connections
|
||||
|
||||
The mechanical logic relies on `SocketPoint`s representing holes and pins, and `JointBlock`s which create physical constraints between them.
|
||||
|
||||
### `SocketPoint`
|
||||
Defines compatibility via radii and a `SocketType` enum.
|
||||
|
||||
```csharp
|
||||
public class SocketPoint : MonoBehaviour, ISocket
|
||||
{
|
||||
[SerializeField] private SocketType socketType = SocketType.Regular;
|
||||
[SerializeField] private float socketRadius = 0.1f;
|
||||
[SerializeField] private List<SocketType> acceptableTypes = new List<SocketType>();
|
||||
[SerializeField] private float radiusTolerance = 0.05f;
|
||||
|
||||
public bool CanAccept(ISocket otherSocket)
|
||||
{
|
||||
// Must be empty
|
||||
if (isOccupied) return false;
|
||||
|
||||
// Must match type
|
||||
if (acceptableTypes.Count > 0 && !acceptableTypes.Contains(otherSocket.GetSocketType()))
|
||||
return false;
|
||||
|
||||
// The hole must be larger than or equal to the pin, within a tolerance
|
||||
float radiusDiff = socketRadius - otherSocket.GetSocketRadius();
|
||||
return radiusDiff >= -radiusTolerance && radiusDiff <= radiusTolerance;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `JointBlock`
|
||||
When two pieces are successfully snapped together, `JointBlock` takes over to simulate the mechanical connection by creating Unity Physics joints.
|
||||
|
||||
```csharp
|
||||
public class JointBlock : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private JointBlockType jointType = JointBlockType.FixedPivot;
|
||||
|
||||
public void CreatePhysicsJoint(GameObject targetObj, SocketPoint socket)
|
||||
{
|
||||
Rigidbody jointRb = GetComponent<Rigidbody>();
|
||||
if (jointRb == null) jointRb = gameObject.AddComponent<Rigidbody>();
|
||||
|
||||
Rigidbody targetRb = targetObj.GetComponent<Rigidbody>();
|
||||
if (targetRb == null) targetRb = targetObj.AddComponent<Rigidbody>();
|
||||
|
||||
// Create appropriate joint type
|
||||
Joint joint = CreateJointByType(targetRb, socket);
|
||||
if (joint != null)
|
||||
{
|
||||
createdJoints.Add(joint);
|
||||
}
|
||||
}
|
||||
|
||||
private Joint CreateJointByType(Rigidbody connectedBody, SocketPoint socket)
|
||||
{
|
||||
switch (jointType)
|
||||
{
|
||||
case JointBlockType.HingePivot:
|
||||
HingeJoint joint = gameObject.AddComponent<HingeJoint>();
|
||||
joint.connectedBody = connectedBody;
|
||||
joint.axis = transform.InverseTransformDirection(transform.forward);
|
||||
joint.anchor = transform.InverseTransformPoint(socket.GetTransform().position);
|
||||
return joint;
|
||||
|
||||
case JointBlockType.FixedPivot:
|
||||
default:
|
||||
return gameObject.AddComponent<FixedJoint>();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c248b3fbfd507e7499432ee1d043d2ab
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,106 @@
|
||||
# Meccano Editor: Extension & Future Modifications Guide
|
||||
|
||||
This guide is designed for developers who are actively expanding the Meccano Editor. It provides step-by-step instructions on how to add new classes and features into the existing architecture, and outlines the known technical debt and future modifications required for scalability.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: How to Add New Classes to Existing Modules
|
||||
|
||||
### 1. How to Add a New Meccano Block (Piece)
|
||||
To introduce a new mechanical piece (e.g., a gear or a new strut) into the application:
|
||||
1. **Create the Prefab:** Model your piece and save it as a Unity Prefab.
|
||||
2. **Add Core Components:**
|
||||
- Add the `Block` script (required for the system to track its UUID).
|
||||
- Add `GrabInteractable` (so the user can click and drag it).
|
||||
3. **Configure Sockets:**
|
||||
- Add a `SocketContainer` component to manage connection points.
|
||||
- For every hole or pin, create a child object and add a `SocketPoint`. Set the `SocketType` and `radius`.
|
||||
4. **Configure Joints (If Moving):**
|
||||
- If the piece is meant to rotate/pivot, add a `JointBlock` component and set the `JointBlockType` (e.g., `HingePivot`).
|
||||
5. **Create `BlockData`:**
|
||||
- Right-click in your project window -> Create -> Block Data.
|
||||
- Name it, give it a thumbnail, and drag your new prefab into the `blockPrefab` field.
|
||||
- Move this `BlockData` asset into the `Resources/BlocksData` folder. `BlockFactory` automatically loads everything in this folder on startup.
|
||||
|
||||
### 2. How to Add a New Command
|
||||
If you want to add a new user action (e.g., scaling a block, painting a block a different color), you must use the Command Pattern so it can be undone and saved.
|
||||
1. **Create the Command Class:** Create a new script in `CommandLayer/Commands` and implement `ICommand`.
|
||||
```csharp
|
||||
public class ColorBlockCommand : ICommand
|
||||
{
|
||||
private string blockId;
|
||||
private Color newColor;
|
||||
private Color previousColor;
|
||||
|
||||
public void execute() { /* Apply newColor to the block */ }
|
||||
public void undo() { /* Revert to previousColor */ }
|
||||
public CommandDTO toDTO()
|
||||
{
|
||||
return new CommandDTO.CommandDTOBuilder()
|
||||
.SetCommandName("ColorBlockCommand")
|
||||
.AddParameter("blockId", blockId)
|
||||
.AddParameter("newColor", newColor)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
```
|
||||
2. **Update CommandDTO Parsing:** In the `ColorBlockCommand` constructor, ensure it can reconstruct itself from a `CommandDTO` parameter dictionary (for loading saves).
|
||||
3. **Update `CommandHandler`:** Add a wrapper function `public void colorBlock(...)` in `CommandHandler.cs` that instantiates your command and passes it to `CommandManager.executeCommand(...)`.
|
||||
|
||||
### 3. How to Add a New Ghost Movement Strategy (GhostManagerV2)
|
||||
If you want a piece to move differently when dragging (e.g., snapping strictly to a grid, or rotating only around a specific axis), you can leverage the `GhostManagerV2` system:
|
||||
1. Create a new class implementing `IGhostMovementStrategy`.
|
||||
2. Implement the `UpdateMovement(ref GhostMovementContext ctx)` method.
|
||||
3. Apply it by calling `GhostManagerV2.getInstance().SetMovementStrategy(new MyCustomStrategy());`.
|
||||
*(Note: `GhostManagerV2` utilizes a highly decoupled `BlockEditContext` where the strategy only handles position/rotation calculations, keeping it separated from validation and placement logic).*
|
||||
|
||||
### 4. How to Inject a New System Service
|
||||
If you create a new Manager class that needs to be accessed globally without using a Singleton:
|
||||
1. Make your MonoBehaviour implement `IDependencyProvider`.
|
||||
2. Write a method returning your service and tag it with `[Provide]`.
|
||||
3. In any other class that needs it, declare the field and tag it with `[Inject]`. The `Injector` will automatically wire them together before `Start()`.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Future Modifications & Technical Debt
|
||||
|
||||
As the project scales, several core systems will need to be refactored to support larger assemblies and better performance.
|
||||
|
||||
### 1. Optimize the Save/Load System (Event Sourcing Bottleneck)
|
||||
**Current State:** The system serializes the environment by generating a massive queue of `CreateBlockCommand`s, saving them to JSON, and replaying them one by one during Load.
|
||||
**The Problem:** If a user builds a complex machine with 5,000 blocks, loading the file requires running `Instantiate()` 5,000 times sequentially through the Command Manager, which is extremely slow.
|
||||
**Future Modification:**
|
||||
- **State Snapshots:** Instead of strictly relying on Command History for saving, implement a true "State Dump". The save file should just hold an array of piece positions/rotations. On load, use a single batched process to instantiate everything immediately, bypassing the Command Stack entirely.
|
||||
|
||||
### 2. Refactor Memory Leaks in the Command History
|
||||
**Current State:** `CommandManager` caps the `commandStack` at 100 entries to prevent running out of RAM.
|
||||
**The Problem:** If a user makes 101 moves, the very first move is permanently deleted from the undo history. While this prevents memory leaks, it destroys the complete continuous timeline.
|
||||
**Future Modification:**
|
||||
- Implement **History Serialization**. When the stack exceeds 100 items, write the oldest 50 items to a temporary local temp file (e.g., `history_chunk_1.tmp`) and remove them from RAM. If the user presses "Undo" 100 times, asynchronously read the file back into RAM.
|
||||
|
||||
### 3. Physics Joint Instability
|
||||
**Current State:** `JointBlock` creates Unity `HingeJoint` or `FixedJoint` components on the fly when pieces are snapped together.
|
||||
**The Problem:** Unity's PhysX engine struggles with long chains of joints. If a user builds a long robotic arm with 20 consecutive hinges, the physics engine will likely jitter, rubber-band, or explode due to unresolved solver iterations.
|
||||
**Future Modification:**
|
||||
- **Articulation Bodies:** Migrate the mechanical system from standard `Rigidbody` + `Joint` components to Unity's **ArticulationBody** system. Articulation Bodies are specifically designed for robotic, kinematic chains and do not suffer from the rubber-banding issues of standard joints.
|
||||
|
||||
### 4. Standardize Dependency Injection vs. Singletons
|
||||
**Current State:** The codebase is fragmented. Some systems use the custom `[Inject]` DI container (`Injector.cs`), while others still rely on `ClassName.getInstance()` (Singletons).
|
||||
**The Problem:** Singletons make unit testing nearly impossible because dependencies are hardcoded. It also makes scene transitions difficult because Singletons often persist unexpectedly.
|
||||
**Future Modification:**
|
||||
- Remove `getInstance()` entirely from classes like `CommandManager`, `Environment`, and `GhostManager`.
|
||||
- Rely 100% on the existing `Injector` system to provide these instances.
|
||||
|
||||
### 5. Multi-Select and Group Manipulations
|
||||
**Current State:** Commands generally affect single blocks (e.g., `MoveBlockCommand`). Also, `GhostManagerV2` uses a `BlockEditContext` that is strictly designed for a *single* block (one GhostObject, one BlockData, etc.).
|
||||
**Future Modification:**
|
||||
- **Command Layer:** Implement a `CompositeCommand` pattern that contains a `List<ICommand>`. This allows moving 50 blocks simultaneously and undoing it as a single step.
|
||||
- **Interaction Layer (The "Gizmo vs AxialMoveStrategy" Approach):** Rather than trying to force `GhostManagerV2` to support multiple blocks, the better approach for multi-select is to handle block manipulation strictly via custom gizmo scripts (like `TransformManager.cs` and `GizmoMoveElement.cs`) *after* blocks are placed.
|
||||
- You should remove/deprecate `AxialMoveStrategy` from the Ghost pipeline. The GhostManager is best suited for drag-and-drop snapping of a *single* new or existing block. Once multiple pieces are selected in the world, use the `TransformManager` to apply direct transform changes to the group, and fire the `CompositeCommand` on mouse up. This keeps `GhostManagerV2`'s interfaces clean and avoids violating its single-block responsibility.
|
||||
|
||||
### 6. Migrate fully to GhostManagerV2
|
||||
**Current State:** The project currently contains both `GhostManager` and `GhostManagerV2` / `GhostController`.
|
||||
**Future Modification:**
|
||||
- After fixing any remaining bugs in `GhostManagerV2`, completely delete the legacy `GhostManager`.
|
||||
- Ensure all input bindings in `InteractionManager` point exclusively to `GhostManagerV2`.
|
||||
- V2's architecture relies on `BlockEditContext`, `IPlacementHandler`, and `IRotationHandler` which dramatically reduces the monolithic nature of the original manager. Adopting V2 entirely will make future extensions (like VR placement or custom snap behaviors) significantly easier.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6b84f0bfc16b64478abd413fad31635
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 102e81a1722cbe441b79d077128c93dd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,100 @@
|
||||
# Video Presentation Guide: Unity 3D Meccano Editor
|
||||
|
||||
This guide is structured to help you present your Meccano Editor to other developers. It flows from high-level concepts down to implementation details, ensuring the audience understands *why* decisions were made before seeing *how* they are implemented.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Overview (0:00 - 2:00)
|
||||
**Goal:** Hook the viewer and explain the core functionality.
|
||||
* **On Screen:** Start in the Unity Editor in "Play Mode". Build a quick 3- or 4-piece Meccano assembly. Show the grid, snap a few pieces together, and undo an action.
|
||||
* **What to Mention:**
|
||||
- This is a 3D Meccano building application where players can snap mechanical parts together.
|
||||
- It supports undo/redo, saving/loading, and complex mechanical joints.
|
||||
- The architecture is highly decoupled, event-driven, and relies heavily on the Command Pattern to manage state.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Architecture (2:00 - 4:00)
|
||||
**Goal:** Explain the foundational pillars of the codebase.
|
||||
* **On Screen:** Open an architectural diagram (or draw one on screen/whiteboard) showing three columns: `Interaction Layer -> Command Layer -> BlocksLayer (Environment)`.
|
||||
* **Important Things to Mention:**
|
||||
- The separation of concerns: The Interaction layer ONLY handles input and ghosts. It *never* directly modifies the Environment.
|
||||
- The `Environment` class is the single source of truth for instantiated blocks.
|
||||
- **Code to Open:** `Injector.cs`
|
||||
- Briefly show the custom Dependency Injection system. Explain why you built `[Inject]` and `[Provide]` attributes: to guarantee dependencies are resolved before `Start()` and to handle reactive `RuntimeAnchor` changes cleanly.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data & Commands (4:00 - 7:00)
|
||||
**Goal:** Explain how state mutations are encapsulated.
|
||||
* **On Screen:** Split screen or switch between `CommandManager.cs` and `CreateBlockCommand.cs`.
|
||||
* **What to Mention:**
|
||||
- Introduce the **Command Pattern**. Explain that every action is an `ICommand`.
|
||||
- Show how `CreateBlockCommand` stores the required data (prefab ID, position, rotation) to execute *and* reverse the action.
|
||||
- Highlight the `CommandManager` class. Show the `commandStack` and `redoStack`.
|
||||
* **Common Questions to Address:**
|
||||
- *Why not just instantiate blocks directly from the UI?* -> "If we instantiate directly, we lose the ability to easily undo actions or serialize the history for saves."
|
||||
|
||||
---
|
||||
|
||||
## 4. Object Manipulation & The Ghost System (7:00 - 10:00)
|
||||
**Goal:** Explain the drag-and-drop workflow and how visual feedback is given before committing an action.
|
||||
* **On Screen:** Go back to Unity Play Mode. Pick up a block, drag it around (it should look semi-transparent), and hover it over a valid and invalid spot.
|
||||
* **Code to Open:** `GhostManager.cs` and `IGhostMovementStrategy.cs`.
|
||||
* **Important Things to Mention:**
|
||||
- `GhostManager` is the most complex UI component. It creates a dummy version of the block that doesn't interact with physics.
|
||||
- Mention the **Strategy Pattern**: `IGhostMovementStrategy` allows the ghost to behave differently (e.g., free movement vs. axis-constrained movement).
|
||||
- Show how, upon release, `GhostManager` doesn't place the block itself, but instead fires off a Command to the `CommandHandler`.
|
||||
|
||||
---
|
||||
|
||||
## 5. UI & Interaction (10:00 - 12:00)
|
||||
**Goal:** Show how user input flows into the system.
|
||||
* **Code to Open:** `PCIntputProvider.cs` and `RayInteractor.cs`.
|
||||
* **What to Mention:**
|
||||
- Show the use of Unity's new Input System (`InputAction`).
|
||||
- Explain how `BaseInputProvider` abstracts the input device, allowing easy future support for Mobile or VR.
|
||||
- Show `RayInteractor.cs` and explain that it strictly handles firing rays and casting events, delegating the logic of *what* happens to the `InteractionManager`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sockets & Joints (12:00 - 15:00)
|
||||
**Goal:** Explain the mechanical heart of the Meccano system.
|
||||
* **On Screen:** In the Editor (not Play Mode), select a Meccano piece prefab. Show its `SocketContainer` and `SocketPoint` components in the Inspector. Turn on Gizmos to show the socket radii/normals.
|
||||
* **Code to Open:** `SocketPoint.cs` and `JointBlock.cs`.
|
||||
* **What to Mention:**
|
||||
- `SocketPoint` defines the compatibility (hole vs pin, radius sizes).
|
||||
- Explain that when two compatible sockets snap, `JointBlock.cs` dynamically creates a Unity Physics Joint (like `FixedJoint` or `HingeJoint`) between the Rigidbody components.
|
||||
- **Common Questions to Address:**
|
||||
- *How do you prevent physics explosions?* -> "We validate placements using `IValidator` before creating the joints."
|
||||
|
||||
---
|
||||
|
||||
## 7. Save/Load System & Event Sourcing (15:00 - 17:00)
|
||||
**Goal:** Demonstrate how the Command Pattern makes saving almost trivial.
|
||||
* **On Screen:** Build something in Play Mode. Hit save. Clear the scene. Hit load. Watch it rebuild.
|
||||
* **Code to Open:** `EnvironmentObserver.cs` and `CommandSaveHandler.cs`.
|
||||
* **Important Things to Mention:**
|
||||
- **Event Sourcing:** We don't save a list of objects; we save a list of *instructions*.
|
||||
- `CommandDTO` structs serialize the command data to JSON.
|
||||
- When loading, we use a Coroutine (`CommandSaveHandler.excuteAllCommands()`) to replay the commands frame-by-frame.
|
||||
- **Honest Critique (Weakness):** Mention that while this is elegant, if a user makes 10,000 moves, the save file might get large and slow to replay. A future optimization could involve taking state snapshots.
|
||||
|
||||
---
|
||||
|
||||
## 8. Extending the System (17:00 - 19:00)
|
||||
**Goal:** Prove the architecture is developer-friendly.
|
||||
* **On Screen:** Open `BlockFactory.cs` and your `Resources/BlocksData` folder.
|
||||
* **What to Mention:**
|
||||
- **Adding a new block:** "Just create a prefab, add a `Block` and `SocketPoint` component, create a `BlockData` ScriptableObject, and put it in the Resources folder. The system picks it up automatically."
|
||||
- **Adding a new command:** "Implement `ICommand`, add your execute/undo logic, and update `CommandDTO` so it can be saved."
|
||||
|
||||
---
|
||||
|
||||
## 9. Final Walkthrough & Model Checking System (19:00 - 20:00)
|
||||
**Goal:** End on a high note by showing the 'Game' aspect (Puzzles).
|
||||
* **On Screen:** Open `ModelCheckManager.cs`.
|
||||
* **What to Mention:**
|
||||
- Explain the puzzle mode: The system can compare the current `Environment` against a predefined `BlocksMap` ScriptableObject.
|
||||
- Show the Editor tooling: A developer can build a structure in Play Mode, click a button, and generate a `BlocksMap` asset to serve as a new level objective.
|
||||
* **Closing Statement:** Thank the viewers and summarize that the decoupled, command-driven architecture makes the editor robust, testable, and easily extensible.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 349b6f2b5a673a541895a76f5736a523
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user