Files
2026-07-21 08:56:10 +03:00

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");
});
}
}