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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user