using System.Collections.Generic; using UnityEngine; /// /// Handles socket proximity detection, highlighting, and closest-socket tracking. /// /// Key fixes vs previous version: /// 1. Searches from col.transform.ROOT — so if OverlapSphere hits a child mesh /// collider, we still find SocketPoints that are siblings of that mesh. /// 2. Uses QueryTriggerInteraction.Collide — socket colliders are usually triggers; /// without this flag the overlap silently finds nothing when /// Physics.queriesHitTriggers is false in Project Settings. /// 3. Deduplicates across colliders that share a root, so a multi-collider block /// doesn't add its sockets multiple times. /// public class SnapSystem : MonoBehaviour { private static SnapSystem instance; public static SnapSystem getInstance() => instance; [SerializeField] private float socketDetectionRadius = 0.5f; private readonly List nearbySockets = new List(); private readonly HashSet visitedRoots = new HashSet(); private SocketPoint closestSocket; private void Awake() { if (instance == null) instance = this; else Destroy(gameObject); } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- /// /// Re-scans for sockets around . /// Call every frame while a ghost is being dragged. /// /// /// World-space ray hit point on the world surface — NOT the ghost's own position. /// /// /// The ghost's root transform. Sockets belonging to this hierarchy are ignored. /// public void UpdateSockets(Vector3 detectionPoint, Transform ghostRoot) { ClearHighlights(); nearbySockets.Clear(); visitedRoots.Clear(); closestSocket = null; // QueryTriggerInteraction.Collide ensures trigger colliders (typical for sockets) are found Collider[] colliders = Physics.OverlapSphere(detectionPoint, socketDetectionRadius, Physics.AllLayers, QueryTriggerInteraction.Collide); foreach (Collider col in colliders) { Transform root = col.transform.root; // Skip anything that belongs to the held ghost if (ghostRoot != null && root == ghostRoot) continue; // Don't process the same block twice (it may have multiple colliders) if (visitedRoots.Contains(root)) continue; visitedRoots.Add(root); // Search from root so we find ALL SocketPoints on the block, // regardless of which child collider was hit by the overlap. foreach (SocketPoint socket in root.GetComponentsInChildren()) { if (socket.IsOccupied()) continue; nearbySockets.Add(socket); socket.Highlight(HighlightState.Compatible); Debug.Log($"[SnappingSystem] Found socket: {socket.name} on {root.name}"); } } if (nearbySockets.Count > 0) { closestSocket = FindClosest(detectionPoint, nearbySockets); closestSocket?.Highlight(HighlightState.Active); Debug.Log($"[SnappingSystem] Closest socket: {closestSocket?.name}"); } } /// Resets all highlight states without clearing the list. public void ClearHighlights() { foreach (SocketPoint socket in nearbySockets) if (socket != null) socket.Highlight(HighlightState.Default); } /// Full reset — clears highlights and all cached data. public void Clear() { ClearHighlights(); nearbySockets.Clear(); visitedRoots.Clear(); closestSocket = null; } public List GetNearbySockets() => nearbySockets; public SocketPoint GetClosestSocket() => closestSocket; // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- private static SocketPoint FindClosest(Vector3 position, List sockets) { SocketPoint closest = null; float minDist = float.MaxValue; foreach (SocketPoint socket in sockets) { float dist = Vector3.Distance(position, socket.GetTransform().position); if (dist < minDist) { minDist = dist; closest = socket; } } return closest; } }