539 lines
18 KiB
C#
539 lines
18 KiB
C#
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"));
|
|
}
|
|
|
|
} |