Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6be0de9216955b648a0d40547046583a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy 2 — axis-constrained translation via UI handle.
|
||||
/// Call BeginDrag() when the user grabs an axis arrow, passing the world-space
|
||||
/// axis direction and the hit point on that arrow.
|
||||
/// Call EndDrag() when the pointer is released.
|
||||
/// The ghost slides along the axis; rotation is frozen.
|
||||
/// Snapping still applies (position only).
|
||||
/// </summary>
|
||||
public class AxisMoveStrategy : IGhostMovementStrategy
|
||||
{
|
||||
private readonly Quaternion _lockedRotation;
|
||||
|
||||
// Drag state
|
||||
private bool _isDragging;
|
||||
private Vector3 _axisDirection; // world-space unit vector of the active axis
|
||||
private Vector3 _axisOrigin; // point on the axis at drag-begin (ghost position)
|
||||
private float _clampMin;
|
||||
private float _clampMax;
|
||||
private Camera _camera;
|
||||
private Vector3 _frozenPosition;
|
||||
private Vector2 _lastMousePos;
|
||||
private float _accumulated;
|
||||
|
||||
/// <param name="lockedRotation">Ghost rotation frozen for the entire interaction.</param>
|
||||
/// <param name="clampMin">Min distance from drag origin along the axis (negative = behind).</param>
|
||||
/// <param name="clampMax">Max distance from drag origin along the axis.</param>
|
||||
public AxisMoveStrategy(Quaternion lockedRotation, float clampMin = -1000f, float clampMax = 1000f)
|
||||
{
|
||||
_lockedRotation = lockedRotation;
|
||||
_clampMin = clampMin;
|
||||
_clampMax = clampMax;
|
||||
_camera = Camera.main;
|
||||
}
|
||||
|
||||
private float _dragOffsetT;
|
||||
|
||||
/// <summary>
|
||||
/// Call from your UI when the user begins dragging an axis arrow.
|
||||
/// </summary>
|
||||
/// <param name="axisDirection">World-space direction of the axis (e.g. ghost.transform.right).</param>
|
||||
/// <param name="ghostPosition">Ghost world position at the moment dragging begins.</param>
|
||||
public void BeginDrag(Vector3 axisDirection, Vector3 ghostPosition)
|
||||
{
|
||||
_camera = Camera.main; // Ensure camera reference is up-to-date
|
||||
_axisDirection = axisDirection.normalized;
|
||||
_axisOrigin = ghostPosition;
|
||||
_frozenPosition = ghostPosition;
|
||||
_accumulated = 0f;
|
||||
_isDragging = true;
|
||||
|
||||
if (_camera != null)
|
||||
{
|
||||
Ray mouseRay = _camera.ScreenPointToRay(Input.mousePosition);
|
||||
_dragOffsetT = CalculateT(mouseRay);
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateT(Ray mouseRay)
|
||||
{
|
||||
Vector3 w = mouseRay.origin - _axisOrigin;
|
||||
float b = Vector3.Dot(_axisDirection, mouseRay.direction);
|
||||
float d = Vector3.Dot(_axisDirection, w);
|
||||
float e = Vector3.Dot(mouseRay.direction, w);
|
||||
|
||||
float denominator = 1f - b * b;
|
||||
|
||||
if (Mathf.Abs(denominator) < 0.0001f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return (d - b * e) / denominator;
|
||||
}
|
||||
|
||||
/// <summary>Call from your UI when the pointer is released.</summary>
|
||||
public void EndDrag() => _isDragging = false;
|
||||
|
||||
public void UpdateMovement(ref GhostMovementContext ctx)
|
||||
{
|
||||
// Rotation always frozen
|
||||
ctx.GhostTransform.rotation = _lockedRotation;
|
||||
|
||||
if (!_isDragging)
|
||||
{
|
||||
ctx.RotationAxis = _axisDirection != Vector3.zero ? _axisDirection : Vector3.up;
|
||||
TrySocketSnap(ref ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_camera != null)
|
||||
{
|
||||
Ray mouseRay = _camera.ScreenPointToRay(Input.mousePosition);
|
||||
float currentT = CalculateT(mouseRay);
|
||||
float targetAccumulated = currentT - _dragOffsetT;
|
||||
_accumulated = Mathf.Clamp(targetAccumulated, _clampMin, _clampMax);
|
||||
}
|
||||
|
||||
// Only the axis component changes — all other axes stay frozen
|
||||
ctx.GhostTransform.position = _frozenPosition + _axisDirection * _accumulated;
|
||||
|
||||
ctx.RotationAxis = _axisDirection;
|
||||
|
||||
TrySocketSnap(ref ctx);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private bool TrySocketSnap(ref GhostMovementContext ctx)
|
||||
{
|
||||
SocketPoint[] mySockets = ctx.GhostTransform.GetComponentsInChildren<SocketPoint>();
|
||||
if (mySockets.Length == 0) return false;
|
||||
|
||||
SocketPoint bestMySocket = null;
|
||||
SocketPoint bestTargetSocket = null;
|
||||
float bestDist = 0.5f;
|
||||
|
||||
foreach (var mySocket in mySockets)
|
||||
{
|
||||
Collider[] hits = Physics.OverlapSphere(mySocket.transform.position, bestDist, Physics.AllLayers, QueryTriggerInteraction.Collide);
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.transform.root == ctx.GhostTransform.root) continue;
|
||||
|
||||
SocketPoint[] targetSockets = hit.transform.root.GetComponentsInChildren<SocketPoint>();
|
||||
foreach (var target in targetSockets)
|
||||
{
|
||||
if (target.IsOccupied()) continue;
|
||||
if (!target.CanAccept(mySocket)) continue;
|
||||
|
||||
float dist = Vector3.Distance(mySocket.transform.position, target.transform.position);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestMySocket = mySocket;
|
||||
bestTargetSocket = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMySocket != null && bestTargetSocket != null)
|
||||
{
|
||||
Vector3 desiredPosition = bestTargetSocket.transform.position - (bestMySocket.transform.position - ctx.GhostTransform.position);
|
||||
|
||||
if (_isDragging && _axisDirection != Vector3.zero)
|
||||
{
|
||||
Vector3 toDesired = desiredPosition - _axisOrigin;
|
||||
float scalar = Vector3.Dot(toDesired, _axisDirection);
|
||||
scalar = Mathf.Clamp(scalar, _clampMin, _clampMax);
|
||||
|
||||
ctx.GhostTransform.position = _axisOrigin + _axisDirection * scalar;
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.GhostTransform.position = desiredPosition;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d40b6e6599b5054b8a3f72a417bfa87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class AxisRotateStrategy : IGhostMovementStrategy
|
||||
{
|
||||
public bool OverridesSurfaceTracking => true;
|
||||
|
||||
public enum LocalAxis { Up, Forward, Right }
|
||||
|
||||
private readonly LocalAxis _localAxis;
|
||||
private readonly Vector3 _lockedPosition;
|
||||
|
||||
private bool _isRotating;
|
||||
private float _accumulatedDegrees;
|
||||
private Vector2 _lastMousePos;
|
||||
|
||||
private Quaternion _unsnappedRot;
|
||||
private Vector3 _unsnappedPos;
|
||||
private bool _wasSnapped;
|
||||
|
||||
public float Sensitivity = 0.4f;
|
||||
public float SnapAngle = 15f;
|
||||
public bool EnableSnapping = false;
|
||||
|
||||
/// <param name="localAxis">Local axis of the ghost to rotate around.</param>
|
||||
/// <param name="lockedPosition">Ghost world position frozen for the entire interaction.</param>
|
||||
public AxisRotateStrategy(LocalAxis localAxis, Vector3 lockedPosition)
|
||||
{
|
||||
_localAxis = localAxis;
|
||||
_lockedPosition = lockedPosition;
|
||||
}
|
||||
|
||||
public void BeginRotate()
|
||||
{
|
||||
_lastMousePos = Input.mousePosition;
|
||||
_accumulatedDegrees = 0f;
|
||||
_isRotating = true;
|
||||
_wasSnapped = false;
|
||||
}
|
||||
|
||||
public void EndRotate() => _isRotating = false;
|
||||
|
||||
public void UpdateMovement(ref GhostMovementContext ctx)
|
||||
{
|
||||
// 1. Revert previous frame's snap so we rotate from the true axial position
|
||||
if (_wasSnapped)
|
||||
{
|
||||
ctx.GhostTransform.position = _unsnappedPos;
|
||||
ctx.GhostTransform.rotation = _unsnappedRot;
|
||||
_wasSnapped = false;
|
||||
}
|
||||
|
||||
// Read the axis from the ghost's current orientation every frame
|
||||
// so it stays local regardless of how the ghost has been rotated
|
||||
Vector3 worldAxis = GetWorldAxis(ctx.GhostTransform);
|
||||
|
||||
if (_isRotating)
|
||||
{
|
||||
Vector2 mouseDelta = (Vector2)Input.mousePosition - _lastMousePos;
|
||||
_lastMousePos = Input.mousePosition;
|
||||
|
||||
if (mouseDelta.sqrMagnitude > 0.001f)
|
||||
{
|
||||
// Project the world axis into screen space to get its 2D direction
|
||||
Vector3 axisScreenStart = Camera.main.WorldToScreenPoint(_lockedPosition);
|
||||
Vector3 axisScreenEnd = Camera.main.WorldToScreenPoint(_lockedPosition + worldAxis);
|
||||
Vector2 axisScreen = (axisScreenEnd - axisScreenStart).normalized;
|
||||
|
||||
// The rotation direction is the perpendicular to the screen-space axis
|
||||
// Dot the mouse delta against that perpendicular to get signed rotation
|
||||
Vector2 perpendicular = new Vector2(-axisScreen.y, axisScreen.x);
|
||||
float signedDelta = Vector2.Dot(mouseDelta, perpendicular);
|
||||
|
||||
if (ctx.SnapRotation || EnableSnapping)
|
||||
{
|
||||
_accumulatedDegrees += signedDelta * Sensitivity;
|
||||
|
||||
float sign = Mathf.Sign(_accumulatedDegrees);
|
||||
while (Mathf.Abs(_accumulatedDegrees) >= SnapAngle)
|
||||
{
|
||||
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, SnapAngle * sign);
|
||||
_accumulatedDegrees -= SnapAngle * sign;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float pendingDegrees = signedDelta * Sensitivity;
|
||||
if (pendingDegrees != 0f)
|
||||
{
|
||||
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, pendingDegrees);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the pure axial rotation state
|
||||
_unsnappedPos = ctx.GhostTransform.position;
|
||||
_unsnappedRot = ctx.GhostTransform.rotation;
|
||||
|
||||
// Attempt to snap to nearby sockets visually
|
||||
if (TrySocketSnap(ref ctx))
|
||||
{
|
||||
_wasSnapped = true;
|
||||
}
|
||||
|
||||
ctx.RotationAxis = worldAxis;
|
||||
}
|
||||
|
||||
private bool TrySocketSnap(ref GhostMovementContext ctx)
|
||||
{
|
||||
SocketPoint[] mySockets = ctx.GhostTransform.GetComponentsInChildren<SocketPoint>();
|
||||
if (mySockets.Length == 0) return false;
|
||||
|
||||
SocketPoint bestMySocket = null;
|
||||
SocketPoint bestTargetSocket = null;
|
||||
float bestDist = 0.5f;
|
||||
|
||||
foreach (var mySocket in mySockets)
|
||||
{
|
||||
Collider[] hits = Physics.OverlapSphere(mySocket.transform.position, bestDist, Physics.AllLayers, QueryTriggerInteraction.Collide);
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
if (hit.transform.root == ctx.GhostTransform.root) continue;
|
||||
|
||||
SocketPoint[] targetSockets = hit.transform.root.GetComponentsInChildren<SocketPoint>();
|
||||
foreach (var target in targetSockets)
|
||||
{
|
||||
if (target.IsOccupied()) continue;
|
||||
if (!target.CanAccept(mySocket)) continue;
|
||||
|
||||
float dist = Vector3.Distance(mySocket.transform.position, target.transform.position);
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestMySocket = mySocket;
|
||||
bestTargetSocket = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMySocket != null && bestTargetSocket != null)
|
||||
{
|
||||
// Align the rotations exactly like FreeMoveStrategy
|
||||
Vector3 myNormal = bestMySocket.GetNormal().normalized;
|
||||
Vector3 myTargetNormal = -bestTargetSocket.GetNormal().normalized;
|
||||
|
||||
Quaternion primaryRot = Quaternion.FromToRotation(myNormal, myTargetNormal);
|
||||
ctx.GhostTransform.rotation = primaryRot * ctx.GhostTransform.rotation;
|
||||
|
||||
Vector3 mySecondary = Mathf.Abs(Vector3.Dot(bestMySocket.transform.up, myTargetNormal)) < 0.9f
|
||||
? bestMySocket.transform.up
|
||||
: bestMySocket.transform.forward;
|
||||
|
||||
Vector3 targetSecondary = Mathf.Abs(Vector3.Dot(bestTargetSocket.transform.up, -myTargetNormal)) < 0.9f
|
||||
? bestTargetSocket.transform.up
|
||||
: bestTargetSocket.transform.forward;
|
||||
|
||||
Vector3 projectedMySecondary = Vector3.ProjectOnPlane(mySecondary, myTargetNormal).normalized;
|
||||
Vector3 projectedTargetSecondary = Vector3.ProjectOnPlane(targetSecondary, myTargetNormal).normalized;
|
||||
|
||||
if (projectedMySecondary != Vector3.zero && projectedTargetSecondary != Vector3.zero)
|
||||
{
|
||||
float angle = Vector3.SignedAngle(projectedMySecondary, projectedTargetSecondary, myTargetNormal);
|
||||
float correction = angle - Mathf.Round(angle / 45f) * 45f;
|
||||
ctx.GhostTransform.rotation = Quaternion.AngleAxis(correction, myTargetNormal) * ctx.GhostTransform.rotation;
|
||||
}
|
||||
|
||||
// Translate to connect the sockets perfectly
|
||||
Vector3 shift = bestTargetSocket.transform.position - bestMySocket.transform.position;
|
||||
ctx.GhostTransform.position += shift;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Re-evaluated every frame from the ghost's current transform
|
||||
private Vector3 GetWorldAxis(Transform t) => _localAxis switch
|
||||
{
|
||||
LocalAxis.Up => t.up,
|
||||
LocalAxis.Forward => t.forward,
|
||||
LocalAxis.Right => t.right,
|
||||
_ => t.up,
|
||||
};
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6129be9a4c7dac4bb0a5c345336cbcb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Strategy — free surface tracking.
|
||||
/// Ghost follows the ray hit point, aligns to the surface normal,
|
||||
/// and socket-snaps when a compatible socket is nearby.
|
||||
/// Rotation input rotates around the surface normal (or socket forward when snapped).
|
||||
/// </summary>
|
||||
public class FreeMoveStrategy : IGhostMovementStrategy
|
||||
{
|
||||
public void UpdateMovement(ref GhostMovementContext ctx)
|
||||
{
|
||||
bool snapped = TrySocketSnap(ref ctx);
|
||||
|
||||
if (!snapped)
|
||||
{
|
||||
if (ctx.AlignToSurfaceNormal)
|
||||
AlignToSurfaceNormal(ref ctx);
|
||||
|
||||
MoveAlongSurface(ref ctx);
|
||||
}
|
||||
|
||||
// Rotation axis: socket forward when snapped, surface normal otherwise
|
||||
ctx.RotationAxis = (snapped && ctx.MySocket != null)
|
||||
? ctx.MySocket.transform.forward
|
||||
: ctx.SurfaceNormal;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static bool TrySocketSnap(ref GhostMovementContext ctx)
|
||||
{
|
||||
if (ctx.TargetSocket == null) return false;
|
||||
bool compatible = ctx.MySocket == null || ctx.TargetSocket.CanAccept(ctx.MySocket);
|
||||
if (!compatible) return false;
|
||||
|
||||
PerformSocketSnap(ref ctx);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PerformSocketSnap(ref GhostMovementContext ctx)
|
||||
{
|
||||
Vector3 targetNormal = ctx.TargetSocket.GetNormal().normalized;
|
||||
|
||||
if (ctx.MySocket != null)
|
||||
{
|
||||
// 1. Primary Alignment: Align the exact socket normals
|
||||
Vector3 myNormal = ctx.MySocket.GetNormal().normalized;
|
||||
Vector3 myTargetNormal = -ctx.TargetSocket.GetNormal().normalized;
|
||||
|
||||
Quaternion primaryRot = Quaternion.FromToRotation(myNormal, myTargetNormal);
|
||||
ctx.GhostTransform.rotation = primaryRot * ctx.GhostTransform.rotation;
|
||||
|
||||
// 2. Secondary Alignment: Align the 'Up' (or Forward) vectors to prevent random twisting/rolling
|
||||
Vector3 mySecondary = Mathf.Abs(Vector3.Dot(ctx.MySocket.transform.up, myTargetNormal)) < 0.9f
|
||||
? ctx.MySocket.transform.up
|
||||
: ctx.MySocket.transform.forward;
|
||||
|
||||
Vector3 targetSecondary = Mathf.Abs(Vector3.Dot(ctx.TargetSocket.transform.up, -myTargetNormal)) < 0.9f
|
||||
? ctx.TargetSocket.transform.up
|
||||
: ctx.TargetSocket.transform.forward;
|
||||
|
||||
// Project secondary vectors onto the flat plane between the sockets
|
||||
Vector3 projectedMySecondary = Vector3.ProjectOnPlane(mySecondary, myTargetNormal).normalized;
|
||||
Vector3 projectedTargetSecondary = Vector3.ProjectOnPlane(targetSecondary, myTargetNormal).normalized;
|
||||
|
||||
if (projectedMySecondary != Vector3.zero && projectedTargetSecondary != Vector3.zero)
|
||||
{
|
||||
// Find the twist angle needed to align the secondary axes
|
||||
float angle = Vector3.SignedAngle(projectedMySecondary, projectedTargetSecondary, myTargetNormal);
|
||||
// Snap to the nearest 45 degrees to allow manual 'R' rotations to persist!
|
||||
float correction = angle - Mathf.Round(angle / 45f) * 45f;
|
||||
// Rotate the ghost around its own pivot
|
||||
ctx.GhostTransform.rotation = Quaternion.AngleAxis(correction, myTargetNormal) * ctx.GhostTransform.rotation;
|
||||
}
|
||||
|
||||
// 3. Translate the ghost so the sockets physically touch
|
||||
Vector3 shift = ctx.TargetSocket.transform.position - ctx.MySocket.transform.position;
|
||||
ctx.GhostTransform.position += shift;
|
||||
}
|
||||
else
|
||||
{
|
||||
Transform pivot = ctx.AttachPoint != null
|
||||
? ctx.AttachPoint
|
||||
: ctx.GhostTransform;
|
||||
Vector3 pivotOffset = pivot.position - ctx.GhostTransform.position;
|
||||
ctx.GhostTransform.position = ctx.TargetSocket.transform.position - pivotOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AlignToSurfaceNormal(ref GhostMovementContext ctx)
|
||||
{
|
||||
Transform pivot = GetCurrentPivot(ref ctx);
|
||||
Quaternion target = Quaternion.FromToRotation(pivot.up, ctx.SurfaceNormal)
|
||||
* ctx.GhostTransform.rotation;
|
||||
|
||||
if (ctx.SnapRotation)
|
||||
target = SnapToNearestRightAngle(target);
|
||||
|
||||
ctx.GhostTransform.rotation = target;
|
||||
}
|
||||
|
||||
private static void MoveAlongSurface(ref GhostMovementContext ctx)
|
||||
{
|
||||
Transform pivot = GetCurrentPivot(ref ctx);
|
||||
Vector3 pivotOffset = pivot.position - ctx.GhostTransform.position;
|
||||
Vector3 targetPos = ctx.SurfacePoint - pivotOffset;
|
||||
|
||||
if (ctx.HeightOffset != 0f)
|
||||
targetPos += ctx.SurfaceNormal * ctx.HeightOffset;
|
||||
|
||||
ctx.GhostTransform.position = Vector3.Lerp(
|
||||
ctx.GhostTransform.position,
|
||||
targetPos,
|
||||
Time.deltaTime * ctx.FollowSpeed);
|
||||
}
|
||||
|
||||
private static Transform GetCurrentPivot(ref GhostMovementContext ctx)
|
||||
{
|
||||
return ctx.AttachPoint != null ? ctx.AttachPoint : ctx.GhostTransform;
|
||||
}
|
||||
|
||||
private static Quaternion SnapToNearestRightAngle(Quaternion q)
|
||||
{
|
||||
Vector3 e = q.eulerAngles;
|
||||
e.x = Mathf.Round(e.x / 90f) * 90f;
|
||||
e.y = Mathf.Round(e.y / 90f) * 90f;
|
||||
e.z = Mathf.Round(e.z / 90f) * 90f;
|
||||
return Quaternion.Euler(e);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5046d2417e8edc54f981d84ddaa61413
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
|
||||
public class GhostStrategyHandler : MonoBehaviour
|
||||
{
|
||||
private static GhostStrategyHandler instance;
|
||||
public static GhostStrategyHandler getInstance() => instance;
|
||||
|
||||
private GhostManagerV2 ghostManager;
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null) instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
ghostManager = GhostManagerV2.getInstance();
|
||||
}
|
||||
|
||||
// MoveAxis strategy
|
||||
|
||||
public void StartDraggingRight()
|
||||
{
|
||||
var MoveStategy = new AxisMoveStrategy(Quaternion.identity);
|
||||
MoveStategy.BeginDrag(Vector3.right, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(MoveStategy);
|
||||
}
|
||||
|
||||
public void StartDraggingUp()
|
||||
{
|
||||
var MoveStategy = new AxisMoveStrategy(Quaternion.identity);
|
||||
MoveStategy.BeginDrag(Vector3.up, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(MoveStategy);
|
||||
}
|
||||
|
||||
public void StartDraggingForward()
|
||||
{
|
||||
var MoveStategy = new AxisMoveStrategy(Quaternion.identity);
|
||||
MoveStategy.BeginDrag(Vector3.forward, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(MoveStategy);
|
||||
}
|
||||
|
||||
public void EndDragging()
|
||||
{
|
||||
ghostManager.SetMovementStrategy(new FreeMoveStrategy());
|
||||
}
|
||||
|
||||
// RotateAxis strategy
|
||||
|
||||
public AxisRotateStrategy StartRotatingRight()
|
||||
{
|
||||
var RotateStrategy = new AxisRotateStrategy(AxisRotateStrategy.LocalAxis.Right, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(RotateStrategy);
|
||||
return RotateStrategy;
|
||||
}
|
||||
|
||||
public AxisRotateStrategy StartRotatingUp()
|
||||
{
|
||||
var RotateStrategy = new AxisRotateStrategy(AxisRotateStrategy.LocalAxis.Up, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(RotateStrategy);
|
||||
return RotateStrategy;
|
||||
}
|
||||
|
||||
public AxisRotateStrategy StartRotatingForward()
|
||||
{
|
||||
var RotateStrategy = new AxisRotateStrategy(AxisRotateStrategy.LocalAxis.Forward, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(RotateStrategy);
|
||||
return RotateStrategy;
|
||||
}
|
||||
|
||||
public void StartRotation()
|
||||
{
|
||||
var RotateStrategy = new AxisRotateStrategy(AxisRotateStrategy.LocalAxis.Right, Vector3.zero);
|
||||
ghostManager.SetMovementStrategy(RotateStrategy);
|
||||
return;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 033a7779738092141b18bb78e68e0c21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class GhostStrategyTester : MonoBehaviour
|
||||
{
|
||||
[Header("Assign the scene object to edit")]
|
||||
public GameObject TestTarget;
|
||||
|
||||
[Header("Move settings")]
|
||||
public float ClampMin = -5f;
|
||||
public float ClampMax = 5f;
|
||||
|
||||
[Header("Rotation settings")]
|
||||
public float RotateSensitivity = 0.4f;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private enum Mode { None, AxisMove, AxisRotate }
|
||||
private Mode _mode = Mode.None;
|
||||
|
||||
private AxisMoveStrategy _moveStrat;
|
||||
private AxisRotateStrategy _rotStrat;
|
||||
|
||||
private bool _isEditing;
|
||||
private bool _buttonHeld;
|
||||
private int _activeAxisIndex = -1;
|
||||
private float _lastMouseX;
|
||||
private Vector3 _editingOrigin;
|
||||
|
||||
private static readonly Color[] AxisColors =
|
||||
{
|
||||
new Color(0.9f, 0.25f, 0.25f),
|
||||
new Color(0.25f, 0.85f, 0.25f),
|
||||
new Color(0.25f, 0.45f, 0.95f),
|
||||
};
|
||||
private static readonly string[] AxisLabels = { "X", "Y", "Z" };
|
||||
|
||||
private static readonly Vector3[] LocalMoveAxes =
|
||||
{
|
||||
Vector3.right,
|
||||
Vector3.up,
|
||||
Vector3.forward,
|
||||
};
|
||||
|
||||
private static readonly AxisRotateStrategy.LocalAxis[] RotateAxes =
|
||||
{
|
||||
AxisRotateStrategy.LocalAxis.Right,
|
||||
AxisRotateStrategy.LocalAxis.Up,
|
||||
AxisRotateStrategy.LocalAxis.Forward,
|
||||
};
|
||||
|
||||
private const float BtnW = 96f;
|
||||
private const float BtnH = 36f;
|
||||
private const float PadX = 12f;
|
||||
private const float PadY = 8f;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void Start()
|
||||
{
|
||||
GhostManagerV2.getInstance().onClearAddListener(() =>
|
||||
{
|
||||
_isEditing = false;
|
||||
_buttonHeld = false;
|
||||
_activeAxisIndex = -1;
|
||||
_moveStrat = null;
|
||||
_rotStrat = null;
|
||||
});
|
||||
}
|
||||
|
||||
// private void Update()
|
||||
// {
|
||||
// if (!_buttonHeld || _activeAxisIndex < 0) return;
|
||||
|
||||
// if (_mode == Mode.AxisRotate && _rotStrat != null)
|
||||
// {
|
||||
// float delta = Input.mousePosition.x - _lastMouseX;
|
||||
// if (Mathf.Abs(delta) > 0.01f)
|
||||
// _rotStrat.Rotate(delta * RotateSensitivity);
|
||||
// }
|
||||
|
||||
// _lastMouseX = Input.mousePosition.x;
|
||||
// }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GUI
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (TestTarget == null)
|
||||
{
|
||||
GUI.Label(new Rect(PadX, PadX, 300, 24), "Assign TestTarget in the Inspector.");
|
||||
return;
|
||||
}
|
||||
|
||||
float y = PadX;
|
||||
|
||||
// ── Mode buttons ───────────────────────────────────────────────
|
||||
DrawModeButton("Move", Mode.AxisMove, new Rect(PadX, y, BtnW, BtnH));
|
||||
DrawModeButton("Rotate", Mode.AxisRotate, new Rect(PadX + BtnW + PadY, y, BtnW, BtnH));
|
||||
y += BtnH + PadY;
|
||||
|
||||
if (_mode == Mode.None)
|
||||
{
|
||||
GUI.Label(new Rect(PadX, y, 300, 24), "Select a mode above.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Axis buttons ───────────────────────────────────────────────
|
||||
string hint = _mode == Mode.AxisMove
|
||||
? "Hold to drag along axis:"
|
||||
: "Hold and drag left/right to rotate:";
|
||||
GUI.Label(new Rect(PadX, y, 350, 20), hint);
|
||||
y += 22f;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
Rect btnRect = new Rect(PadX + i * (BtnW + PadY), y, BtnW, BtnH);
|
||||
DrawAxisButton(i, btnRect);
|
||||
}
|
||||
|
||||
y += BtnH + PadY * 2;
|
||||
|
||||
// ── Commit / Cancel ────────────────────────────────────────────
|
||||
if (GUI.Button(new Rect(PadX, y, BtnW, BtnH), "Commit"))
|
||||
CommitEdit();
|
||||
|
||||
if (GUI.Button(new Rect(PadX + BtnW + PadY, y, BtnW, BtnH), "Cancel"))
|
||||
StopEditing();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void DrawAxisButton(int i, Rect rect)
|
||||
{
|
||||
bool held = _buttonHeld && _activeAxisIndex == i;
|
||||
|
||||
GUI.backgroundColor = held ? AxisColors[i] : AxisColors[i] * 0.55f;
|
||||
GUI.contentColor = Color.white;
|
||||
|
||||
bool pressed = GUI.RepeatButton(rect, $"{AxisLabels[i]} axis");
|
||||
|
||||
if (pressed && (!_buttonHeld || _activeAxisIndex != i))
|
||||
OnAxisButtonDown(i);
|
||||
|
||||
if (!pressed && _buttonHeld && _activeAxisIndex == i)
|
||||
OnAxisButtonUp();
|
||||
|
||||
GUI.backgroundColor = Color.white;
|
||||
GUI.contentColor = Color.white;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void OnAxisButtonDown(int axisIndex)
|
||||
{
|
||||
EnsureEditing();
|
||||
|
||||
_activeAxisIndex = axisIndex;
|
||||
_buttonHeld = true;
|
||||
_lastMouseX = Input.mousePosition.x;
|
||||
|
||||
if (_mode == Mode.AxisMove)
|
||||
{
|
||||
Transform ghost = GhostManagerV2.getInstance().GhostTransform;
|
||||
Vector3 worldAxis = ghost.TransformDirection(LocalMoveAxes[axisIndex]);
|
||||
|
||||
_moveStrat = new AxisMoveStrategy(ghost.rotation, ClampMin, ClampMax);
|
||||
_moveStrat.BeginDrag(worldAxis, ghost.position);
|
||||
GhostManagerV2.getInstance().SetMovementStrategy(_moveStrat);
|
||||
}
|
||||
else
|
||||
{
|
||||
_rotStrat = new AxisRotateStrategy(
|
||||
RotateAxes[axisIndex],
|
||||
GhostManagerV2.getInstance().GhostTransform.position);
|
||||
GhostManagerV2.getInstance().SetMovementStrategy(_rotStrat);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAxisButtonUp()
|
||||
{
|
||||
_buttonHeld = false;
|
||||
_activeAxisIndex = -1;
|
||||
|
||||
if (_mode == Mode.AxisMove)
|
||||
_moveStrat?.EndDrag();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void EnsureEditing()
|
||||
{
|
||||
if (_isEditing) return;
|
||||
_editingOrigin = TestTarget.transform.position;
|
||||
GhostManagerV2.getInstance().SetupExistingBlock(TestTarget, new FreeMoveStrategy());
|
||||
_isEditing = true;
|
||||
}
|
||||
|
||||
private void CommitEdit()
|
||||
{
|
||||
GhostManagerV2.getInstance().ForceCommit();
|
||||
ResetState();
|
||||
}
|
||||
|
||||
private void StopEditing()
|
||||
{
|
||||
GhostManagerV2.getInstance().ClearGhostData();
|
||||
ResetState();
|
||||
}
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
_mode = Mode.None;
|
||||
_activeAxisIndex = -1;
|
||||
_buttonHeld = false;
|
||||
_isEditing = false;
|
||||
_moveStrat = null;
|
||||
_rotStrat = null;
|
||||
}
|
||||
|
||||
private void DrawModeButton(string label, Mode target, Rect rect)
|
||||
{
|
||||
bool active = _mode == target;
|
||||
GUI.backgroundColor = active ? Color.white : new Color(0.55f, 0.55f, 0.55f);
|
||||
GUI.contentColor = active ? Color.black : Color.white;
|
||||
|
||||
if (GUI.Button(rect, label) && !active)
|
||||
{
|
||||
StopEditing();
|
||||
_mode = target;
|
||||
}
|
||||
|
||||
GUI.backgroundColor = Color.white;
|
||||
GUI.contentColor = Color.white;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e03daa9e654f8664f912d8ce018c0112
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d3beb871e533e146ad43a998f3415fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,356 @@
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 777683077db104c41a2050097c88d7b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
public class InteractionManager : MonoBehaviour
|
||||
{
|
||||
private static InteractionManager instance;
|
||||
|
||||
private CommandHandler commandHandler;
|
||||
private GhostManagerV2 ghostManager;
|
||||
private SelectionManager selectionManager;
|
||||
private UIManager uiManager;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null) instance = this;
|
||||
else Destroy(this.gameObject);
|
||||
}
|
||||
|
||||
public static InteractionManager getInstance()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
commandHandler = CommandHandler.getInstance();
|
||||
ghostManager = GhostManagerV2.getInstance();
|
||||
selectionManager = SelectionManager.getInstance();
|
||||
uiManager = UIManager.getInstance();
|
||||
|
||||
setupListeners();
|
||||
}
|
||||
|
||||
private void setupListeners()
|
||||
{
|
||||
uiManager.onSelectEvent.AddListener(blockData =>
|
||||
{
|
||||
ghostManager.setupBlock(blockData);
|
||||
});
|
||||
ghostManager.onClearAddListener(() =>
|
||||
{
|
||||
uiManager.deselectUIInteractable();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9623d7fa4d430ed488dab28c4425ca4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Manages selection state for placed interactable objects.
|
||||
/// Listens to RayInteractor.OnSelectEvent (which only fires when no ghost is active).
|
||||
/// Supports single and multi-select (Ctrl/Shift).
|
||||
/// </summary>
|
||||
public class SelectionManager : MonoBehaviour
|
||||
{
|
||||
private static SelectionManager instance;
|
||||
public static SelectionManager getInstance() => instance;
|
||||
|
||||
private RayInteractor rayInteractor;
|
||||
private BaseInputProvider inputProvider;
|
||||
|
||||
private readonly List<IInteractable> currentSelections = new List<IInteractable>();
|
||||
private bool multiSelectEnabled = false;
|
||||
|
||||
public UnityEvent<IInteractable> SelectionEvent;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Unity lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null) instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
rayInteractor = RayInteractor.getInstance();
|
||||
inputProvider = InputProviderManager.getInstance().getCurrentInputProvider();
|
||||
|
||||
inputProvider.OnEnableMultiSelect.AddListener(() => multiSelectEnabled = true);
|
||||
inputProvider.OnDisableMultiSelect.AddListener(() => multiSelectEnabled = false);
|
||||
|
||||
// RayInteractor only fires this when no ghost is active, so we can safely handle selection here
|
||||
rayInteractor.OnSelectEvent.AddListener(HandleSelect);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Selection logic
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void HandleSelect(IInteractable interactable)
|
||||
{
|
||||
if (InputModeManager.Is(InputMode.UI)) return;
|
||||
if (interactable == null)
|
||||
{
|
||||
SelectionEvent?.Invoke(interactable);
|
||||
// Clicked empty space – clear selection unless multi-select is held
|
||||
if (!multiSelectEnabled)
|
||||
ClearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (multiSelectEnabled)
|
||||
{
|
||||
// Toggle the clicked interactable
|
||||
if (currentSelections.Contains(interactable))
|
||||
{
|
||||
currentSelections.Remove(interactable);
|
||||
interactable.highLightOff();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSelections.Add(interactable);
|
||||
interactable.highLightOn();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Replace selection with just this interactable
|
||||
bool Exists = currentSelections.Contains(interactable);
|
||||
ClearSelection();
|
||||
if (!Exists) {
|
||||
currentSelections.Add(interactable);
|
||||
interactable.highLightOn();
|
||||
}
|
||||
}
|
||||
SelectionEvent?.Invoke(interactable);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public List<IInteractable> GetCurrentSelections() => currentSelections;
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
foreach (IInteractable selection in currentSelections)
|
||||
selection.highLightOff();
|
||||
|
||||
currentSelections.Clear();
|
||||
SelectionEvent?.Invoke(null);
|
||||
}
|
||||
|
||||
public bool HasOneSelection()
|
||||
{
|
||||
return !(currentSelections.Count > 1 || currentSelections.Count == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78d74bfe44748a14eb6f98e314f7b623
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
using UnityEngine;
|
||||
|
||||
public class SocketManagerChecker : MonoBehaviour
|
||||
{
|
||||
|
||||
private Environment environment;
|
||||
private GhostManagerV2 ghostManager;
|
||||
private RayInteractor rayInteractor;
|
||||
private JointPlacementHandler jointPlacementHandler;
|
||||
|
||||
[SerializeField] private float socketTolerance = 0.5f;
|
||||
private void Start()
|
||||
{
|
||||
environment = Environment.getInstance();
|
||||
ghostManager = GhostManagerV2.getInstance();
|
||||
rayInteractor = RayInteractor.getInstance();
|
||||
jointPlacementHandler = JointPlacementHandler.getInstance();
|
||||
|
||||
environment.onBlockAddedEvent.AddListener(OnAdd);
|
||||
environment.onBlockRemovedEvent.AddListener(OnRemove);
|
||||
environment.onBlockMovedEvt.AddListener(OnMove);
|
||||
}
|
||||
|
||||
private void OnMove(Block arg0, Vector3 oldPos, Quaternion oldRot)
|
||||
{
|
||||
// Re-evaluate connections after moving
|
||||
if (arg0.gameObject.GetComponent<JointBlock>())
|
||||
{
|
||||
OnJointAdded(arg0);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnBlockAdded(arg0);
|
||||
}
|
||||
}
|
||||
|
||||
public void DisconnectAllSockets(Block arg0)
|
||||
{
|
||||
SocketPoint[] socketPoints = arg0.GetComponentsInChildren<SocketPoint>();
|
||||
if (socketPoints.Length == 0) return;
|
||||
|
||||
List<GameObject> attachedObjects = new List<GameObject>();
|
||||
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
GameObject attachedObject = socketPoint.GetOccupyingObject();
|
||||
if (attachedObject != null && !attachedObjects.Contains(attachedObject))
|
||||
{
|
||||
attachedObjects.Add(attachedObject);
|
||||
}
|
||||
// Also check for joint objects
|
||||
GameObject jointObject = socketPoint.GetJointObject();
|
||||
if (jointObject != null && !attachedObjects.Contains(jointObject))
|
||||
{
|
||||
attachedObjects.Add(jointObject);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (GameObject attachedObject in attachedObjects)
|
||||
{
|
||||
SocketPoint[] attachedObjectSocketPoints = attachedObject.GetComponentsInChildren<SocketPoint>();
|
||||
foreach (SocketPoint attachedObjectSocketPoint in attachedObjectSocketPoints)
|
||||
{
|
||||
if (attachedObjectSocketPoint.GetOccupyingObject() == arg0.gameObject)
|
||||
{
|
||||
attachedObjectSocketPoint.Release();
|
||||
}
|
||||
if (attachedObjectSocketPoint.GetJointObject() == arg0.gameObject)
|
||||
{
|
||||
attachedObjectSocketPoint.ReleaseJoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release our own sockets
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
socketPoint.Release();
|
||||
socketPoint.ReleaseJoint();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemove(Block arg0)
|
||||
{
|
||||
if (arg0.gameObject.GetComponent<JointBlock>())
|
||||
{
|
||||
OnJointRemoved(arg0);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnBlockRemoved(arg0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAdd(Block arg0)
|
||||
{
|
||||
if (arg0.gameObject.GetComponent<JointBlock>())
|
||||
{
|
||||
OnJointAdded(arg0);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnBlockAdded(arg0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnBlockRemoved(Block arg0)
|
||||
{
|
||||
SocketPoint[] socketPoints = arg0.GetComponentsInChildren<SocketPoint>();
|
||||
if (socketPoints.Length == 0)
|
||||
return;
|
||||
|
||||
List<GameObject> attachedObjects = new List<GameObject>();
|
||||
|
||||
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
GameObject attachechedObject = socketPoint.GetOccupyingObject();
|
||||
if (attachechedObject != null)
|
||||
{
|
||||
if (!attachedObjects.Contains(attachechedObject))
|
||||
attachedObjects.Add(attachechedObject);
|
||||
}
|
||||
}
|
||||
// free all the sockets attached to the current block
|
||||
foreach (GameObject attachedObject in attachedObjects)
|
||||
{
|
||||
SocketPoint[] attachedObjectSocketPoints = attachedObject.GetComponentsInChildren<SocketPoint>();
|
||||
if (attachedObjectSocketPoints.Length == 0)
|
||||
continue;
|
||||
|
||||
foreach (SocketPoint attachedObjectSocketPoint in attachedObjectSocketPoints)
|
||||
{
|
||||
if (attachedObjectSocketPoint.GetOccupyingObject() == arg0.gameObject)
|
||||
{
|
||||
attachedObjectSocketPoint.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
environment.completeDestroyBlock(arg0);
|
||||
}
|
||||
|
||||
private void OnJointRemoved(Block arg0)
|
||||
{
|
||||
SocketPoint[] socketPoints = arg0.GetComponentsInChildren<SocketPoint>();
|
||||
if (socketPoints.Length == 0)
|
||||
return;
|
||||
|
||||
List<GameObject> attachedObjects = new List<GameObject>();
|
||||
|
||||
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
GameObject attachechedObject = socketPoint.GetOccupyingObject();
|
||||
if (attachechedObject != null)
|
||||
{
|
||||
if (!attachedObjects.Contains(attachechedObject))
|
||||
attachedObjects.Add(attachechedObject);
|
||||
}
|
||||
}
|
||||
// free all the sockets attached to the current block
|
||||
foreach (GameObject attachedObject in attachedObjects)
|
||||
{
|
||||
SocketPoint[] attachedObjectSocketPoints = attachedObject.GetComponentsInChildren<SocketPoint>();
|
||||
if (attachedObjectSocketPoints.Length == 0)
|
||||
continue;
|
||||
|
||||
foreach (SocketPoint attachedObjectSocketPoint in attachedObjectSocketPoints)
|
||||
{
|
||||
if (attachedObjectSocketPoint.GetJointObject() == arg0.gameObject)
|
||||
{
|
||||
attachedObjectSocketPoint.ReleaseJoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
environment.completeDestroyBlock(arg0);
|
||||
}
|
||||
|
||||
private void OnBlockAdded(Block arg0)
|
||||
{
|
||||
SocketPoint[] socketPoints = arg0.transform.GetComponentsInChildren<SocketPoint>();
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
SocketPoint closestSocket = getClosestSocketBy(socketPoint, socketTolerance);
|
||||
if (closestSocket != null)
|
||||
{
|
||||
GameObject closestSocketParent = closestSocket.transform.parent.gameObject;
|
||||
// check if the Block is JointBlock
|
||||
JointBlock jointBlock = arg0.GetComponent<JointBlock>();
|
||||
if (jointBlock != null)
|
||||
{
|
||||
GameObject occupyingObject = closestSocket.GetJointObject();
|
||||
if (occupyingObject == null)
|
||||
{
|
||||
socketPoint.SetJointOccupied(closestSocketParent);
|
||||
closestSocket.SetOccupied(arg0.gameObject);
|
||||
jointBlock.CreatePhysicsJoint(arg0.gameObject, closestSocket);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject occupyingObject = closestSocket.GetOccupyingObject();
|
||||
if (occupyingObject == null)
|
||||
{
|
||||
closestSocket.SetOccupied(arg0.gameObject);
|
||||
socketPoint.SetOccupied(closestSocketParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJointAdded(Block arg0)
|
||||
{
|
||||
SocketPoint[] socketPoints = arg0.transform.GetComponentsInChildren<SocketPoint>();
|
||||
foreach (SocketPoint socketPoint in socketPoints)
|
||||
{
|
||||
SocketPoint closestSocket = getClosestSocketBy(socketPoint, socketTolerance);
|
||||
if (closestSocket != null)
|
||||
{
|
||||
GameObject closestSocketParent = closestSocket.transform.parent.gameObject;
|
||||
|
||||
GameObject occupyingObject = closestSocket.GetJointObject();
|
||||
if (occupyingObject == null)
|
||||
{
|
||||
closestSocket.SetJointOccupied(arg0.gameObject);
|
||||
socketPoint.SetOccupied(closestSocketParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
// init Joint
|
||||
JointBlock joint = arg0.GetComponent<JointBlock>();
|
||||
if (joint != null && !joint.IsPlaced())
|
||||
{
|
||||
List<SocketPoint> sockets = jointPlacementHandler.FindTargetSocketsForJoint(
|
||||
joint, rayInteractor.GetHitPosition());
|
||||
joint.TryPlaceJoint(sockets);
|
||||
}
|
||||
}
|
||||
|
||||
private SocketPoint getClosestSocketBy(SocketPoint socketPoint, float tol)
|
||||
{
|
||||
Collider[] colliders = Physics.OverlapSphere(socketPoint.transform.position, tol, Physics.AllLayers, QueryTriggerInteraction.Collide);
|
||||
SocketPoint closestSocket = null;
|
||||
float closestDistance = Mathf.Infinity;
|
||||
foreach (Collider collider in colliders)
|
||||
{
|
||||
SocketPoint otherSocketPoint = collider.GetComponent<SocketPoint>();
|
||||
if (otherSocketPoint != null && otherSocketPoint != socketPoint)
|
||||
{
|
||||
if (collider.transform.parent == socketPoint.transform.parent ||
|
||||
ghostManager.isGhost(collider.transform.parent.gameObject) ||
|
||||
Vector3.Dot(socketPoint.GetNormal(), otherSocketPoint.GetNormal()) > 0.9f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float distance = Vector3.Distance(socketPoint.transform.position, otherSocketPoint.transform.position);
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
closestSocket = otherSocketPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closestDistance <= tol)
|
||||
return closestSocket;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15781e8c475ff6d4ebacedc8582953f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class UIManager : MonoBehaviour
|
||||
{
|
||||
private static UIManager instance;
|
||||
|
||||
private UIInteractable currentSelectedUIInteractable;
|
||||
private BlockData currentSelectedData;
|
||||
|
||||
|
||||
public UnityEvent<BlockData> onSelectEvent;
|
||||
|
||||
private CategoryData currentCategory;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null) instance = this;
|
||||
else Destroy(this.gameObject);
|
||||
}
|
||||
|
||||
public static UIManager getInstance() => instance;
|
||||
|
||||
// Selection
|
||||
public void selectUIInteractable(UIInteractable uiInteractable)
|
||||
{
|
||||
if (currentSelectedUIInteractable != null)
|
||||
currentSelectedUIInteractable.onSelectExit();
|
||||
|
||||
currentSelectedUIInteractable = uiInteractable;
|
||||
currentSelectedData = uiInteractable.getAssociatedBlockData();
|
||||
|
||||
onSelectEvent.Invoke(currentSelectedData);
|
||||
}
|
||||
|
||||
public void deselectUIInteractable()
|
||||
{
|
||||
if (currentSelectedUIInteractable != null)
|
||||
currentSelectedUIInteractable.onSelectExit();
|
||||
|
||||
currentSelectedUIInteractable = null;
|
||||
currentSelectedData = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3863eca36dc5dc41ab02d3ef6c5c6e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user