Initial commit
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Central manager for the ghost/preview drag-and-drop workflow.
|
||||
/// Supports both placing new blocks (SetupBlock) and repositioning
|
||||
/// already-placed scene objects (SetupExistingBlock).
|
||||
/// </summary>
|
||||
public class GhostManager : MonoBehaviour
|
||||
{
|
||||
private static GhostManager instance;
|
||||
public static GhostManager getInstance() => instance;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Ghost state
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private BlockData selectedBlockData;
|
||||
private GameObject ghostObject;
|
||||
private bool canPlace = false;
|
||||
[SerializeField] private UnityEvent onClearEvent;
|
||||
|
||||
// Per-prefab config
|
||||
private bool alignToSurfaceNormal = true;
|
||||
private bool snapRotation = false;
|
||||
private float followSpeed = 20f;
|
||||
private float heightOffset = 0f;
|
||||
private Color validTint = new Color(0.5f, 1f, 0.5f, 1f);
|
||||
private Color invalidTint = new Color(1f, 0.5f, 0.5f, 1f);
|
||||
private Transform attachPoint;
|
||||
private IValidator validator;
|
||||
|
||||
// Drag-frame state
|
||||
private bool isValidPlacement = true;
|
||||
private Vector3 currentSurfaceNormal = Vector3.up;
|
||||
private Vector3 currentSurfacePoint = Vector3.zero;
|
||||
private Vector3 rotationAxis = Vector3.up;
|
||||
|
||||
private Renderer[] ghostRenderers;
|
||||
|
||||
// Block component refs on the ghost
|
||||
private SocketContainer container;
|
||||
private JointBlock jointBlock;
|
||||
private Block block;
|
||||
|
||||
private bool isDragged = false;
|
||||
private bool isJustPlaced = false;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Existing-block editing state
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private GameObject editingSourceObject; // the real scene object currently being edited
|
||||
private Vector3 editingOriginalPos; // transform snapshot for cancel/undo
|
||||
private Quaternion editingOriginalRot;
|
||||
private bool isEditingExisting;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Movement strategy
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private IGhostMovementStrategy currentStrategy = new FreeMoveStrategy();
|
||||
|
||||
public void SetMovementStrategy(IGhostMovementStrategy strategy)
|
||||
{
|
||||
currentStrategy = strategy ?? new FreeMoveStrategy();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// External dependencies
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private RayInteractor rayInteractor;
|
||||
private SnapSystem snappingSystem;
|
||||
private Environment environment;
|
||||
private CommandHandler commandHandler;
|
||||
private JointPlacementHandler jointPlacementHandler;
|
||||
private SocketIterator socketIterator;
|
||||
private BaseInputProvider inputProvider;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Unity lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null) instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
rayInteractor = RayInteractor.getInstance();
|
||||
snappingSystem = SnapSystem.getInstance();
|
||||
environment = Environment.getInstance();
|
||||
commandHandler = CommandHandler.getInstance();
|
||||
jointPlacementHandler = JointPlacementHandler.getInstance();
|
||||
socketIterator = SocketIterator.getInstance();
|
||||
inputProvider = InputProviderManager.getInstance()?.getCurrentInputProvider();
|
||||
|
||||
environment.onBlockAddedEvent.AddListener(placed => afterCreation(placed.gameObject));
|
||||
|
||||
inputProvider.OnPlace.AddListener(OnPlace);
|
||||
inputProvider.OnDraggingEnd.AddListener(OnPlaceWithDragging);
|
||||
|
||||
inputProvider.OnRotate.AddListener(() =>
|
||||
{
|
||||
if (HasActiveGhost())
|
||||
ghostObject.transform.RotateAround(
|
||||
ghostObject.transform.position, rotationAxis, 90f);
|
||||
});
|
||||
|
||||
inputProvider.OnCancel.AddListener(ClearGhostData);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!HasActiveGhost()) return;
|
||||
|
||||
UpdateSurfaceTracking();
|
||||
|
||||
snappingSystem.UpdateSockets(currentSurfacePoint, ghostObject.transform);
|
||||
|
||||
SocketPoint targetSocket = snappingSystem.GetClosestSocket();
|
||||
SocketPoint mySocket = socketIterator.GetCurrentActiveSocket();
|
||||
|
||||
bool compatible = targetSocket != null &&
|
||||
(mySocket == null || targetSocket.CanAccept(mySocket));
|
||||
|
||||
var ctx = new GhostMovementContext(
|
||||
ghostTransform: ghostObject.transform,
|
||||
surfacePoint: currentSurfacePoint,
|
||||
surfaceNormal: currentSurfaceNormal,
|
||||
mySocket: mySocket,
|
||||
targetSocket: compatible ? targetSocket : null,
|
||||
attachPoint: attachPoint,
|
||||
followSpeed: followSpeed,
|
||||
heightOffset: heightOffset,
|
||||
alignToSurfaceNormal: alignToSurfaceNormal,
|
||||
snapRotation: snapRotation);
|
||||
|
||||
currentStrategy.UpdateMovement(ref ctx);
|
||||
|
||||
rotationAxis = ctx.RotationAxis;
|
||||
|
||||
isValidPlacement = validator != null
|
||||
? validator.IsValidPlacement(ghostObject)
|
||||
: true;
|
||||
|
||||
UpdateVisualFeedback(isValidPlacement);
|
||||
}
|
||||
|
||||
|
||||
public Transform GhostTransform =>
|
||||
ghostObject != null ? ghostObject.transform : null;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Placement
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void setDrag(bool drag) => isDragged = drag;
|
||||
|
||||
private void OnPlace()
|
||||
{
|
||||
if (InputModeManager.Is(InputMode.UI)) return;
|
||||
if (!HasActiveGhost()) return;
|
||||
if (isJustPlaced) return;
|
||||
|
||||
snappingSystem.Clear();
|
||||
|
||||
if (isEditingExisting)
|
||||
CommitExistingBlock();
|
||||
else
|
||||
PlaceBlock();
|
||||
|
||||
isJustPlaced = true;
|
||||
StartCoroutine(resetPlaced());
|
||||
clearGhostData();
|
||||
}
|
||||
|
||||
public void ForcePlace()
|
||||
{
|
||||
if (!HasActiveGhost()) return;
|
||||
snappingSystem.Clear();
|
||||
|
||||
if (isEditingExisting)
|
||||
CommitExistingBlock();
|
||||
else
|
||||
PlaceBlock();
|
||||
|
||||
clearGhostData();
|
||||
}
|
||||
|
||||
private IEnumerator resetPlaced()
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
isJustPlaced = false;
|
||||
}
|
||||
|
||||
private void OnPlaceWithDragging()
|
||||
{
|
||||
if (!isDragged) return;
|
||||
OnPlace();
|
||||
}
|
||||
|
||||
/// <summary>Place a brand-new block via create command.</summary>
|
||||
private void PlaceBlock()
|
||||
{
|
||||
if (!isValidPlacement) return;
|
||||
if (block != null)
|
||||
commandHandler.createBlock(
|
||||
block.getBlockData()?.blockName,
|
||||
ghostObject.transform.position,
|
||||
ghostObject.transform.rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit the repositioned existing block via a move command so undo works.
|
||||
/// The source object is re-shown at its new transform; the ghost is discarded.
|
||||
/// </summary>
|
||||
private void CommitExistingBlock()
|
||||
{
|
||||
if (!isValidPlacement)
|
||||
{
|
||||
RestoreMaterialAlpha(editingSourceObject);
|
||||
RestoreMaterialColor(editingSourceObject);
|
||||
SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable"));
|
||||
return;
|
||||
}
|
||||
if (editingSourceObject == null) return;
|
||||
commandHandler.moveBlock(
|
||||
editingSourceObject.GetComponent<Block>().getBlockId(),
|
||||
editingOriginalPos,
|
||||
ghostObject.transform.position,
|
||||
editingOriginalRot,
|
||||
ghostObject.transform.rotation);
|
||||
|
||||
RestoreMaterialAlpha(editingSourceObject);
|
||||
RestoreMaterialColor(editingSourceObject);
|
||||
SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable"));
|
||||
|
||||
environment.applyChanges();
|
||||
|
||||
// Null ghostObject BEFORE ClearGhostData runs so the else branch
|
||||
// has nothing to destroy ghostObject is the real object here
|
||||
ghostObject = null;
|
||||
editingSourceObject = null;
|
||||
isEditingExisting = false;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API new block
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void SetupBlock(BlockData blockData = null)
|
||||
{
|
||||
if (blockData == null && ghostObject != null)
|
||||
{
|
||||
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
||||
InputModeManager.Push(InputMode.ObjectDragging);
|
||||
ghostObject.SetActive(true);
|
||||
canPlace = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockData != null)
|
||||
selectedBlockData = blockData;
|
||||
|
||||
if (ghostObject != null)
|
||||
Destroy(ghostObject);
|
||||
|
||||
if (selectedBlockData == null)
|
||||
{
|
||||
ghostObject = null;
|
||||
canPlace = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
||||
InputModeManager.Push(InputMode.ObjectDragging);
|
||||
|
||||
GameObject prefab = selectedBlockData.blockPrefab;
|
||||
ghostObject = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation);
|
||||
|
||||
SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast"));
|
||||
InitialiseGhostFromObject(ghostObject);
|
||||
|
||||
isEditingExisting = false;
|
||||
currentStrategy = new FreeMoveStrategy();
|
||||
canPlace = true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API existing block
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Turns an already-placed scene object into an editable ghost in-place.
|
||||
/// The source object is hidden while editing; calling OnPlace commits the
|
||||
/// new transform via commandHandler.moveBlock so undo works.
|
||||
/// Calling ClearGhostData cancels and restores the original transform.
|
||||
/// </summary>
|
||||
/// <param name="sourceObject">The scene GameObject to reposition.</param>
|
||||
/// <param name="strategy">
|
||||
/// Movement strategy to use. Defaults to FreeMoveStrategy if null.
|
||||
/// For a gizmo-style UI pass an AxisMoveStrategy or AxisRotateStrategy.
|
||||
/// </param>
|
||||
public void SetupExistingBlock(GameObject sourceObject, IGhostMovementStrategy strategy = null)
|
||||
{
|
||||
if (sourceObject == null) return;
|
||||
|
||||
if (ghostObject != null)
|
||||
Destroy(ghostObject);
|
||||
|
||||
editingSourceObject = sourceObject;
|
||||
editingOriginalPos = sourceObject.transform.position;
|
||||
editingOriginalRot = sourceObject.transform.rotation;
|
||||
isEditingExisting = true;
|
||||
|
||||
// Use the object itself as the ghost no cloning needed
|
||||
ghostObject = sourceObject;
|
||||
|
||||
SetLayerRecursively(ghostObject, LayerMask.NameToLayer("Ignore Raycast"));
|
||||
InitialiseGhostFromObject(ghostObject);
|
||||
|
||||
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
||||
InputModeManager.Push(InputMode.ObjectDragging);
|
||||
|
||||
currentStrategy = strategy ?? new FreeMoveStrategy();
|
||||
canPlace = true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Ghost initialisation (shared between SetupBlock and SetupExistingBlock)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Reads GrabInteractable config, sets up sockets, renderers, and
|
||||
/// semi-transparent tint on the ghost object.
|
||||
/// </summary>
|
||||
private void InitialiseGhostFromObject(GameObject ghost)
|
||||
{
|
||||
GrabInteractable grabConfig = ghost.GetComponent<GrabInteractable>();
|
||||
if (grabConfig != null)
|
||||
{
|
||||
alignToSurfaceNormal = grabConfig.alignToSurfaceNormal;
|
||||
snapRotation = grabConfig.snapRotation;
|
||||
followSpeed = grabConfig.followSpeed;
|
||||
heightOffset = grabConfig.heightOffset;
|
||||
validTint = grabConfig.validTint;
|
||||
invalidTint = grabConfig.invalidTint;
|
||||
attachPoint = grabConfig.GetAttachPoint();
|
||||
validator = grabConfig.GetValidator();
|
||||
}
|
||||
else
|
||||
{
|
||||
alignToSurfaceNormal = true;
|
||||
snapRotation = false;
|
||||
followSpeed = 20f;
|
||||
heightOffset = 0f;
|
||||
validTint = new Color(0.5f, 1f, 0.5f, 1f);
|
||||
invalidTint = new Color(1f, 0.5f, 0.5f, 1f);
|
||||
validator = ghost.GetComponent<IValidator>();
|
||||
}
|
||||
|
||||
if (attachPoint == null)
|
||||
{
|
||||
attachPoint = new GameObject("attachPoint").transform;
|
||||
attachPoint.SetParent(ghost.transform);
|
||||
}
|
||||
|
||||
jointBlock = ghost.GetComponent<JointBlock>();
|
||||
block = ghost.GetComponent<Block>();
|
||||
|
||||
container = ghost.GetComponent<SocketContainer>();
|
||||
if (container != null && container.GetSocketCount() > 0)
|
||||
socketIterator.SetActiveContainer(container);
|
||||
else
|
||||
socketIterator.ClearActiveContainer();
|
||||
|
||||
ghostRenderers = ghost.GetComponentsInChildren<Renderer>().Where(x => x.gameObject.layer != LayerMask.NameToLayer("Gizmos")).ToArray();
|
||||
foreach (Renderer r in ghostRenderers)
|
||||
foreach (Material mat in r.materials)
|
||||
{
|
||||
if (mat.HasProperty("_Color"))
|
||||
{
|
||||
mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public bool HasActiveGhost() => ghostObject != null && ghostObject.activeSelf && canPlace;
|
||||
public bool isGhost(GameObject obj) => obj == ghostObject;
|
||||
|
||||
public void ClearGhost()
|
||||
{
|
||||
if (ghostObject != null)
|
||||
ghostObject.SetActive(false);
|
||||
canPlace = false;
|
||||
}
|
||||
|
||||
public void ClearGhostData()
|
||||
{
|
||||
if (InputModeManager.Has(InputMode.ObjectDragging))
|
||||
InputModeManager.Pop(InputMode.ObjectDragging);
|
||||
|
||||
if (isEditingExisting)
|
||||
{
|
||||
// Only runs on cancel � CommitExistingBlock clears these before we get here
|
||||
if (editingSourceObject != null)
|
||||
{
|
||||
editingSourceObject.transform.SetPositionAndRotation(
|
||||
editingOriginalPos, editingOriginalRot);
|
||||
RestoreMaterialAlpha(editingSourceObject);
|
||||
SetLayerRecursively(editingSourceObject, LayerMask.NameToLayer("Interactable"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ghostObject != null)
|
||||
Destroy(ghostObject);
|
||||
}
|
||||
|
||||
ghostObject = null;
|
||||
selectedBlockData = null;
|
||||
canPlace = false;
|
||||
editingSourceObject = null;
|
||||
isEditingExisting = false;
|
||||
|
||||
snappingSystem?.Clear();
|
||||
socketIterator?.ClearActiveContainer();
|
||||
onClearEvent.Invoke();
|
||||
}
|
||||
|
||||
private void UpdateSurfaceTracking()
|
||||
{
|
||||
if (rayInteractor.GetHitInfo(out RaycastHit hitInfo))
|
||||
{
|
||||
currentSurfacePoint = hitInfo.point;
|
||||
currentSurfaceNormal = hitInfo.normal.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSurfaceNormal = Vector3.up;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisualFeedback(bool isValid)
|
||||
{
|
||||
Color tint = isValid ? validTint : invalidTint;
|
||||
foreach (Renderer r in ghostRenderers)
|
||||
if (r != null) r.material.color = tint;
|
||||
}
|
||||
|
||||
private static void SetLayerRecursively(GameObject obj, int layer, Func<Transform, bool> Method)
|
||||
{
|
||||
if (Method(obj.transform))
|
||||
obj.layer = layer;
|
||||
foreach (Transform child in obj.transform)
|
||||
SetLayerRecursively(child.gameObject, layer, Method);
|
||||
}
|
||||
|
||||
private static void SetLayerRecursively(GameObject obj, int layer)
|
||||
{
|
||||
SetLayerRecursively(obj, layer, x =>
|
||||
{
|
||||
return x.gameObject.layer != LayerMask.NameToLayer("Gizmos");
|
||||
});
|
||||
}
|
||||
|
||||
private static void RestoreMaterialAlpha(GameObject obj, Func<GameObject, bool> Method)
|
||||
{
|
||||
foreach (Renderer r in obj.GetComponentsInChildren<Renderer>())
|
||||
if (Method(r.gameObject))
|
||||
{
|
||||
foreach (Material mat in r.materials)
|
||||
{
|
||||
if (mat.HasProperty("_Color"))
|
||||
{
|
||||
mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreMaterialAlpha(GameObject obj)
|
||||
{
|
||||
RestoreMaterialAlpha(obj, x =>
|
||||
{
|
||||
return x.layer != LayerMask.NameToLayer("Gizmos");
|
||||
});
|
||||
}
|
||||
private static void RestoreMaterialColor(GameObject obj, Func<GameObject, bool> Method)
|
||||
{
|
||||
foreach (Renderer r in obj.GetComponentsInChildren<Renderer>())
|
||||
if (Method(r.gameObject))
|
||||
{
|
||||
foreach (Material mat in r.materials)
|
||||
{
|
||||
if (mat.HasProperty("_Color"))
|
||||
{
|
||||
mat.color = new Color(1f, 1f, 1f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void RestoreMaterialColor(GameObject obj)
|
||||
{
|
||||
RestoreMaterialColor(obj, x =>
|
||||
{
|
||||
return x.layer != LayerMask.NameToLayer("Gizmos");
|
||||
});
|
||||
}
|
||||
|
||||
public void onClearAddListener(UnityAction action) => onClearEvent.AddListener(action);
|
||||
public void setupBlock(BlockData blockData = null) => SetupBlock(blockData);
|
||||
public void clearGhost() => ClearGhost();
|
||||
public void clearGhostData() => ClearGhostData();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Post-placement coroutines (new blocks only)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void afterCreation(GameObject blockObj)
|
||||
{
|
||||
if (blockObj == null) return;
|
||||
if (blockObj.GetComponent<BaseInteractable>() == null && blockObj.layer != LayerMask.NameToLayer("Ignore Interaction"))
|
||||
blockObj.AddComponent<BaseInteractable>();
|
||||
SetLayerRecursively(blockObj, LayerMask.NameToLayer("Interactable"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user