356 lines
12 KiB
C#
356 lines
12 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
public class GhostManagerV2 : MonoBehaviour
|
|
{
|
|
private static GhostManagerV2 instance;
|
|
public static GhostManagerV2 getInstance() => instance;
|
|
|
|
[SerializeField] private UnityEvent onClearEvent;
|
|
|
|
private BlockEditContext _ctx;
|
|
private bool _isDragged;
|
|
private bool _isJustPlaced;
|
|
|
|
// managers
|
|
private RayInteractor rayInteractor;
|
|
private SnapSystem snappingSystem;
|
|
private Environment environment;
|
|
private CommandHandler commandHandler;
|
|
private SocketIterator socketIterator;
|
|
private BaseInputProvider inputProvider;
|
|
|
|
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();
|
|
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(OnRotate);
|
|
inputProvider.OnCancel.AddListener(ClearGhostData);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (_ctx == null || !_ctx.IsActive) return;
|
|
|
|
UpdateSurfaceTracking();
|
|
|
|
snappingSystem.UpdateSockets(_ctx.SurfacePoint, _ctx.GhostTransform);
|
|
|
|
SocketPoint targetSocket = snappingSystem.GetClosestSocket();
|
|
SocketPoint mySocket = socketIterator.GetCurrentActiveSocket();
|
|
bool compatible = targetSocket != null &&
|
|
(mySocket == null || targetSocket.CanAccept(mySocket));
|
|
|
|
_ctx.SetFrameState(
|
|
_ctx.SurfacePoint,
|
|
_ctx.SurfaceNormal,
|
|
mySocket,
|
|
compatible ? targetSocket : null);
|
|
|
|
// Build movement context and run strategy
|
|
var movCtx = BuildMovementContext();
|
|
_ctx.CurrentStrategy?.UpdateMovement(ref movCtx);
|
|
_ctx.SetRotationAxis(movCtx.RotationAxis);
|
|
|
|
// Validate and give feedback
|
|
_ctx.IsValidPlacement = _ctx.Validator?.IsValid(_ctx) ?? true;
|
|
if (_ctx.IsValidPlacement)
|
|
_ctx.VisualFeedback?.OnValid(_ctx);
|
|
else
|
|
_ctx.VisualFeedback?.OnInvalid(_ctx);
|
|
}
|
|
|
|
|
|
public void setDrag(bool drag) => _isDragged = drag;
|
|
|
|
private void OnPlace()
|
|
{
|
|
if (InputModeManager.Is(InputMode.UI)) return;
|
|
if (_ctx == null || !_ctx.IsActive || !_ctx.IsValidPlacement) return;
|
|
if (_isJustPlaced) return;
|
|
|
|
snappingSystem.Clear();
|
|
_ctx.PlacementHandler?.Commit(_ctx);
|
|
|
|
_isJustPlaced = true;
|
|
StartCoroutine(ResetPlaced());
|
|
ClearGhostDataInternal(false);
|
|
}
|
|
|
|
private void OnPlaceWithDragging()
|
|
{
|
|
if (_isDragged) OnPlace();
|
|
}
|
|
|
|
private void OnRotate()
|
|
{
|
|
_ctx.RotationHandler?.performRotation(_ctx);
|
|
}
|
|
|
|
|
|
public void SetupBlock(BlockData blockData = null)
|
|
{
|
|
if (blockData == null && _ctx?.GhostObject != null)
|
|
{
|
|
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
|
InputModeManager.Push(InputMode.ObjectDragging);
|
|
_ctx.GhostObject.SetActive(true);
|
|
return;
|
|
}
|
|
|
|
if (_ctx?.GhostObject != null)
|
|
Destroy(_ctx.GhostObject);
|
|
|
|
if (blockData == null) { _ctx = null; return; }
|
|
|
|
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
|
InputModeManager.Push(InputMode.ObjectDragging);
|
|
|
|
GameObject prefab = blockData.blockPrefab;
|
|
GameObject ghost = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation);
|
|
SetLayerRecursively(ghost, LayerMask.NameToLayer("Ignore Raycast"));
|
|
|
|
_ctx = BuildContext(ghost, blockData, false, null, Vector3.zero, Quaternion.identity);
|
|
_ctx.PlacementHandler = new NewBlockPlacementHandler(commandHandler);
|
|
_ctx.CurrentStrategy = new FreeMoveStrategy();
|
|
}
|
|
|
|
public void SetupExistingBlock(GameObject sourceObject, IGhostMovementStrategy strategy = null)
|
|
{
|
|
if (sourceObject == null) return;
|
|
|
|
if (_ctx?.GhostObject != null && !_ctx.IsEditingExisting)
|
|
Destroy(_ctx.GhostObject);
|
|
|
|
sourceObject.SetActive(false);
|
|
GameObject ghost = sourceObject;
|
|
|
|
// Disconnect all sockets so it can freely snap to new locations while moving
|
|
Block block = ghost.GetComponent<Block>();
|
|
if (block != null)
|
|
{
|
|
SocketManagerChecker checker = FindObjectOfType<SocketManagerChecker>();
|
|
if (checker != null) checker.DisconnectAllSockets(block);
|
|
}
|
|
|
|
ghost.SetActive(true);
|
|
SetLayerRecursively(ghost, LayerMask.NameToLayer("Ignore Raycast"));
|
|
|
|
_ctx = BuildContext(
|
|
ghost,
|
|
sourceObject.GetComponent<Block>()?.getBlockData(),
|
|
true,
|
|
sourceObject,
|
|
sourceObject.transform.position,
|
|
sourceObject.transform.rotation);
|
|
|
|
_ctx.PlacementHandler = new ExistingBlockPlacementHandler(commandHandler);
|
|
_ctx.CurrentStrategy = strategy ?? new FreeMoveStrategy();
|
|
|
|
if (!InputModeManager.Has(InputMode.ObjectDragging))
|
|
InputModeManager.Push(InputMode.ObjectDragging);
|
|
}
|
|
|
|
public void SetMovementStrategy(IGhostMovementStrategy strategy)
|
|
{
|
|
if (_ctx != null) _ctx.CurrentStrategy = strategy ?? new FreeMoveStrategy();
|
|
}
|
|
|
|
public bool HasActiveGhost() => _ctx != null && _ctx.IsActive;
|
|
public bool isGhost(GameObject obj) => _ctx?.GhostObject == obj;
|
|
public Transform GhostTransform => _ctx?.GhostTransform;
|
|
|
|
public void ClearGhost()
|
|
{
|
|
if (_ctx?.GhostObject != null) _ctx.GhostObject.SetActive(false);
|
|
}
|
|
|
|
public void ClearGhostData()
|
|
{
|
|
if (InputModeManager.Is(InputMode.UI)) return;
|
|
ClearGhostDataInternal(true);
|
|
}
|
|
|
|
private void ClearGhostDataInternal(bool isCancel)
|
|
{
|
|
if (InputModeManager.Has(InputMode.ObjectDragging))
|
|
InputModeManager.Pop(InputMode.ObjectDragging);
|
|
|
|
if (_ctx != null)
|
|
{
|
|
if (isCancel && _ctx.IsEditingExisting)
|
|
_ctx.PlacementHandler?.Cancel(_ctx);
|
|
else if (!_ctx.IsEditingExisting && _ctx.GhostObject != null)
|
|
Destroy(_ctx.GhostObject);
|
|
}
|
|
|
|
_ctx = null;
|
|
|
|
snappingSystem?.Clear();
|
|
socketIterator?.ClearActiveContainer();
|
|
onClearEvent.Invoke();
|
|
}
|
|
|
|
public void onClearAddListener(UnityAction action) => onClearEvent.AddListener(action);
|
|
public void setupBlock(BlockData blockData = null) => SetupBlock(blockData);
|
|
public void clearGhost() => ClearGhost();
|
|
public void clearGhostData() => ClearGhostData();
|
|
|
|
/// <summary>
|
|
/// Commits the current ghost unconditionally (no validity check).
|
|
/// Mirrors GhostManager.ForcePlace() for editor/test tooling.
|
|
/// </summary>
|
|
public void ForceCommit()
|
|
{
|
|
if (_ctx == null || !_ctx.IsActive) return;
|
|
snappingSystem.Clear();
|
|
_ctx.PlacementHandler?.Commit(_ctx);
|
|
ClearGhostDataInternal(false);
|
|
}
|
|
|
|
public void TryCommitOrCancel()
|
|
{
|
|
if (_ctx == null || !_ctx.IsActive) return;
|
|
|
|
if (_ctx.IsValidPlacement)
|
|
{
|
|
snappingSystem.Clear();
|
|
_ctx.PlacementHandler?.Commit(_ctx);
|
|
ClearGhostDataInternal(false);
|
|
}
|
|
else
|
|
{
|
|
ClearGhostDataInternal(true);
|
|
}
|
|
}
|
|
|
|
|
|
private BlockEditContext BuildContext(
|
|
GameObject ghost, BlockData blockData,
|
|
bool isExisting, GameObject source,
|
|
Vector3 originalPos, Quaternion originalRot)
|
|
{
|
|
var ctx = new BlockEditContext
|
|
{
|
|
GhostObject = ghost,
|
|
BlockData = blockData,
|
|
IsEditingExisting = isExisting,
|
|
SourceObject = source,
|
|
OriginalPosition = originalPos,
|
|
OriginalRotation = originalRot,
|
|
Validator = new DefaultBlockValidator(),
|
|
VisualFeedback = new DefaultVisualFeedback(),
|
|
SnapBehavior = new DefaultSnapBehavior(),
|
|
RotationHandler = new DefaultRotationHandler()
|
|
};
|
|
|
|
GrabInteractable grab = ghost.GetComponent<GrabInteractable>();
|
|
if (grab != null)
|
|
{
|
|
ctx.AlignToSurfaceNormal = grab.alignToSurfaceNormal;
|
|
ctx.SnapRotation = grab.snapRotation;
|
|
ctx.FollowSpeed = grab.followSpeed;
|
|
ctx.HeightOffset = grab.heightOffset;
|
|
ctx.ValidTint = grab.validTint;
|
|
ctx.InvalidTint = grab.invalidTint;
|
|
ctx.AttachPoint = grab.GetAttachPoint();
|
|
if (!isExisting) Destroy(grab);
|
|
}
|
|
else
|
|
{
|
|
ctx.AttachPoint = new GameObject("attachPoint").transform;
|
|
ctx.AttachPoint.SetParent(ghost.transform);
|
|
}
|
|
|
|
ctx.Block = ghost.GetComponent<Block>();
|
|
ctx.JointBlock = ghost.GetComponent<JointBlock>();
|
|
ctx.Container = ghost.GetComponent<SocketContainer>();
|
|
|
|
if (ctx.Container != null && ctx.Container.GetSocketCount() > 0)
|
|
socketIterator.SetActiveContainer(ctx.Container);
|
|
else
|
|
socketIterator.ClearActiveContainer();
|
|
|
|
ctx.GhostRenderers = ghost.GetComponentsInChildren<Renderer>().Where(x => x.gameObject.layer != LayerMask.NameToLayer("Gizmos")).ToArray();
|
|
foreach (var r in ctx.GhostRenderers)
|
|
foreach (var mat in r.materials)
|
|
{
|
|
if (mat.HasProperty("_Color"))
|
|
{
|
|
mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 0.5f);
|
|
}
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
private GhostMovementContext BuildMovementContext() => new GhostMovementContext(
|
|
ghostTransform: _ctx.GhostTransform,
|
|
surfacePoint: _ctx.SurfacePoint,
|
|
surfaceNormal: _ctx.SurfaceNormal,
|
|
mySocket: _ctx.MySocket,
|
|
targetSocket: _ctx.TargetSocket,
|
|
attachPoint: _ctx.AttachPoint,
|
|
followSpeed: _ctx.FollowSpeed,
|
|
heightOffset: _ctx.HeightOffset,
|
|
alignToSurfaceNormal: _ctx.AlignToSurfaceNormal,
|
|
snapRotation: _ctx.SnapRotation);
|
|
|
|
private void UpdateSurfaceTracking()
|
|
{
|
|
if (rayInteractor.GetHitInfo(out RaycastHit hitInfo))
|
|
_ctx.SetFrameState(hitInfo.point, hitInfo.normal.normalized, _ctx.MySocket, _ctx.TargetSocket);
|
|
else
|
|
_ctx.SetFrameState(_ctx.SurfacePoint, Vector3.up, _ctx.MySocket, _ctx.TargetSocket);
|
|
}
|
|
|
|
private IEnumerator ResetPlaced()
|
|
{
|
|
yield return new WaitForSeconds(0.1f);
|
|
_isJustPlaced = false;
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
private 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 void SetLayerRecursively(GameObject obj, int layer)
|
|
{
|
|
SetLayerRecursively(obj, layer, x =>
|
|
{
|
|
return x.gameObject.layer != LayerMask.NameToLayer("Gizmos");
|
|
});
|
|
}
|
|
|
|
|
|
} |