using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using UnityEngine.EventSystems; using UnityEngine.InputSystem; namespace MedievalShopkeeper { /// /// Player Controller designed for a Medieval Shopkeeper game. /// Supports PC (Left-Click to Move, Right-Click to Interact) and Mobile (Tap to Move / Tap Interactable to Approach & Interact). /// Uses NavMeshAgent for shop navigation and Unity's New Input System. /// [RequireComponent(typeof(NavMeshAgent))] [SelectionBase] public class PlayerController : MonoBehaviour { [Header("References")] [Tooltip("The camera used for raycasting screen clicks. Defaults to Camera.main.")] [SerializeField] private Camera playerCamera; [Tooltip("NavMeshAgent component. If unassigned, will be retrieved on Awake.")] [SerializeField] private NavMeshAgent agent; [Tooltip("Optional Animator component for walking and carrying animations.")] [SerializeField] private Animator animator; [Header("Movement Settings")] [Tooltip("Standard walking speed of the shopkeeper.")] [SerializeField] private float walkSpeed = 4.0f; [Tooltip("Turning speed in degrees per second.")] [SerializeField] private float turnSpeed = 1080f; [Tooltip("If true, turns on the spot when changing directions sharply instead of making wide circular arcs.")] [SerializeField] private bool turnInPlace = true; [Tooltip("Turning/angular speed in degrees per second.")] [SerializeField] private float agentAngularSpeed = 720f; [Tooltip("Acceleration of the agent for crisp starting and stopping.")] [SerializeField] private float agentAcceleration = 24f; [Tooltip("Speed multiplier applied when carrying stock boxes, crates, or heavy items.")] [Range(0.1f, 1f)] [SerializeField] private float carrySpeedMultiplier = 0.8f; [Tooltip("Distance threshold to consider the destination reached.")] [SerializeField] private float destinationThreshold = 0.35f; [Tooltip("Shop floor plane Y-level for raycast fallback.")] [SerializeField] private float floorYLevel = 0f; [Tooltip("Layers representing walkable ground/floor in the shop.")] [SerializeField] private LayerMask groundLayers = ~0; [Header("Interaction Settings")] [Tooltip("Layers representing interactable objects (shelves, stock crates, NPCs, counters).")] [SerializeField] private LayerMask interactableLayers = ~0; [Tooltip("Default interaction range if the target does not specify one.")] [SerializeField] private float defaultInteractionDistance = 2.0f; [Tooltip("On mobile / touch: tapping an interactable automatically moves to it and interacts.")] [SerializeField] private bool smartTouchInteraction = true; [Tooltip("On PC: allow left-click on an interactable to also trigger interaction.")] [SerializeField] private bool leftClickAlsoInteracts = false; [Header("Shopkeeper Carry System")] [Tooltip("Transform socket (e.g. hands or chest) where carried items, boxes, or crates will be attached.")] [SerializeField] private Transform carrySocket; [Header("Optional Input Actions (New Input System)")] [Tooltip("Optional: Action for movement click/tap. If unassigned, defaults to Mouse Left-Click / Touch.")] [SerializeField] private InputActionReference moveAction; [Tooltip("Optional: Action for interaction click. If unassigned, defaults to Mouse Right-Click.")] [SerializeField] private InputActionReference interactAction; [Tooltip("Optional: Action for pointer position. If unassigned, reads Mouse/Touch position.")] [SerializeField] private InputActionReference pointerPositionAction; [Header("Visual Feedback (Optional)")] [Tooltip("Optional prefab instantiated or moved to show the click/destination point.")] [SerializeField] private GameObject destinationMarkerPrefab; [Header("Debug")] [Tooltip("Output detailed logs comparing clicked destination vs NavMesh destination.")] [SerializeField] private bool enableDebugLogs = true; [Header("Animation Parameters")] [SerializeField] private string speedAnimParam = "Speed"; [SerializeField] private string isMovingAnimParam = "IsMoving"; [SerializeField] private string isCarryingAnimParam = "IsCarrying"; // Runtime state private IInteractable pendingTarget; private IInteractable currentHovered; private GameObject destinationMarkerInstance; private GameObject currentCarriedItem; private Vector3 lastClickedDestination; private Vector3 lastNavMeshDestination; private bool hasActiveMoveCommand; private int speedHash; private int isMovingHash; private int isCarryingHash; // Public properties public NavMeshAgent Agent => agent; public IInteractable PendingTarget => pendingTarget; public IInteractable CurrentHovered => currentHovered; public GameObject CurrentCarriedItem => currentCarriedItem; public bool IsCarryingItem => currentCarriedItem != null; public Vector3 LastClickedDestination => lastClickedDestination; public Vector3 LastNavMeshDestination => lastNavMeshDestination; // Events public event Action OnDestinationSet; public event Action OnInteractionTargetSet; public event Action OnInteracted; public event Action OnHoverChanged; public event Action OnItemPickedUp; public event Action OnItemDropped; private void Awake() { if (agent == null) { agent = GetComponent(); } FindCamera(); // Ensure the camera is statically positioned overhead with TopDownCamera if (playerCamera != null) { TopDownCamera topDownCam = playerCamera.GetComponent(); if (topDownCam == null) { topDownCam = playerCamera.gameObject.AddComponent(); } topDownCam.ApplyCameraTransform(); } if (animator == null) { animator = GetComponentInChildren(); } if (animator != null) { animator.applyRootMotion = false; // Prevent animations from fighting NavMesh movement } // Check for offset visual models which cause orbiting foreach (var r in GetComponentsInChildren()) { Vector3 localPos = r.transform.localPosition; localPos.y = 0; // Ignore vertical offset if (localPos.sqrMagnitude > 0.25f) // If offset by more than 0.5m on X/Z { Debug.LogWarning($"[PlayerController] WARNING: Visual model '{r.gameObject.name}' is offset from the Player center by {localPos}. This will cause the character to orbit around the pivot point instead of rotating in place! Please center the model in the prefab."); } } // Configure agent for responsive movement without circular orbiting agent.speed = walkSpeed; agent.angularSpeed = agentAngularSpeed; agent.acceleration = agentAcceleration; agent.autoBraking = true; agent.stoppingDistance = destinationThreshold; agent.updateRotation = false; // We handle rotation to eliminate wide circular loops // Cache animation hashes speedHash = Animator.StringToHash(speedAnimParam); isMovingHash = Animator.StringToHash(isMovingAnimParam); isCarryingHash = Animator.StringToHash(isCarryingAnimParam); // Instantiate destination marker or create default click disc if (destinationMarkerPrefab != null) { destinationMarkerInstance = Instantiate(destinationMarkerPrefab); foreach (var col in destinationMarkerInstance.GetComponentsInChildren()) { col.enabled = false; } destinationMarkerInstance.SetActive(false); } else { destinationMarkerInstance = GameObject.CreatePrimitive(PrimitiveType.Cylinder); destinationMarkerInstance.name = "ClickDestinationMarker"; destinationMarkerInstance.transform.localScale = new Vector3(0.5f, 0.01f, 0.5f); var col = destinationMarkerInstance.GetComponent(); if (col != null) Destroy(col); var rend = destinationMarkerInstance.GetComponent(); if (rend != null) { rend.material.color = new Color(0f, 0.85f, 1f, 0.8f); } destinationMarkerInstance.SetActive(false); } } private void Start() { // Ensure the agent is cleanly snapped onto the NavMesh if placed slightly above or below if (!agent.isOnNavMesh) { if (NavMesh.SamplePosition(transform.position, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas)) { agent.Warp(navHit.position); if (enableDebugLogs) { Debug.Log($"[PlayerController] Agent snapped to NavMesh at: {navHit.position}"); } } else { Debug.LogWarning("[PlayerController] Player is not placed on a valid NavMesh! Please ensure the floor has a baked NavMesh.", this); } } } private void OnEnable() { moveAction?.action?.Enable(); interactAction?.action?.Enable(); pointerPositionAction?.action?.Enable(); } private void OnDisable() { moveAction?.action?.Disable(); interactAction?.action?.Disable(); pointerPositionAction?.action?.Disable(); if (currentHovered != null) { currentHovered.OnHoverExit(this); currentHovered = null; } } private void Update() { // Suspend normal player interaction and movement if building mode is active if (BuildManager.Instance != null && BuildManager.Instance.IsBuilding) { ClearHover(); // Remove UI prompts return; // Let BuildManager handle input } HandleHover(); HandleInput(); HandleDestinationArrival(); HandlePendingInteraction(); UpdateRotation(); UpdateAnimation(); } private void FindCamera() { if (playerCamera == null) { playerCamera = Camera.main; if (playerCamera == null) { playerCamera = FindAnyObjectByType(); } } } #region Input Handling private void HandleInput() { Vector2 screenPos = GetPointerPosition(); // Ignore input if pointer is over UI elements if (IsPointerOverUI(screenPos)) { if (WasMovePressed() || WasInteractPressed()) { if (enableDebugLogs) { Debug.LogWarning($"[PlayerController] Click at screenPos {screenPos} was BLOCKED by UI."); } } return; } bool isTouchDevice = IsTouchscreenActive(); // 1. Primary input (Left Click on PC / Screen Tap on Mobile) if (WasMovePressed()) { if (enableDebugLogs) { Debug.Log($"[PlayerController] CLICK DETECTED at screen: {screenPos}"); } if (isTouchDevice && smartTouchInteraction) { if (TryRaycastInteractable(screenPos, out IInteractable touchedInteractable)) { CommandInteract(touchedInteractable); return; } } else if (leftClickAlsoInteracts) { if (TryRaycastInteractable(screenPos, out IInteractable clickedInteractable)) { CommandInteract(clickedInteractable); return; } } // Move to ground point if (TryRaycastGround(screenPos, out Vector3 groundPoint)) { CommandMove(groundPoint); } else { if (enableDebugLogs) { Debug.LogWarning($"[PlayerController] Left Click at screenPos {screenPos} did NOT hit any ground or NavMesh."); } } } // 2. Secondary input (Right Click on PC) if (WasInteractPressed()) { if (TryRaycastInteractable(screenPos, out IInteractable interactable)) { CommandInteract(interactable); } else { if (enableDebugLogs) { Debug.Log($"[PlayerController] Right Click at screenPos {screenPos} did NOT hit any IInteractable."); } } } } private Vector2 GetPointerPosition() { if (pointerPositionAction != null && pointerPositionAction.action != null && pointerPositionAction.action.enabled) { return pointerPositionAction.action.ReadValue(); } if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed) { return Touchscreen.current.primaryTouch.position.ReadValue(); } if (Mouse.current != null) { return Mouse.current.position.ReadValue(); } if (Pointer.current != null) { return Pointer.current.position.ReadValue(); } return Vector2.zero; } private bool WasMovePressed() { if (moveAction != null && moveAction.action != null) { return moveAction.action.WasPressedThisFrame(); } if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame) { return true; } if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame) { return true; } return false; } private bool WasInteractPressed() { if (interactAction != null && interactAction.action != null) { return interactAction.action.WasPressedThisFrame(); } if (Mouse.current != null && Mouse.current.rightButton.wasPressedThisFrame) { return true; } return false; } private bool IsTouchscreenActive() { return Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed; } private bool IsPointerOverUI(Vector2 screenPosition) { if (EventSystem.current == null) return false; if (EventSystem.current.IsPointerOverGameObject()) return true; if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed) { int touchId = Touchscreen.current.primaryTouch.touchId.ReadValue(); if (EventSystem.current.IsPointerOverGameObject(touchId)) return true; } PointerEventData eventData = new PointerEventData(EventSystem.current) { position = screenPosition }; var results = new List(); EventSystem.current.RaycastAll(eventData, results); return results.Count > 0; } #endregion #region Raycasting private bool TryRaycastGround(Vector2 screenPosition, out Vector3 hitPoint) { hitPoint = Vector3.zero; FindCamera(); if (playerCamera == null) return false; Ray ray = playerCamera.ScreenPointToRay(screenPosition); // 1. Raycast against physics colliders, excluding the player character RaycastHit[] hits = Physics.RaycastAll(ray, 300f, groundLayers, QueryTriggerInteraction.Ignore); if (hits.Length > 0) { Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); foreach (var hit in hits) { if (hit.collider.transform.root == transform.root) continue; // Sample onto NavMesh if (NavMesh.SamplePosition(hit.point, out NavMeshHit navHit, 3.0f, NavMesh.AllAreas)) { hitPoint = navHit.position; return true; } hitPoint = hit.point; return true; } } // 2. Mathematical Plane Fallback: If no collider hit, intersect with shop floor Y plane Plane floorPlane = new Plane(Vector3.up, new Vector3(0f, floorYLevel, 0f)); if (floorPlane.Raycast(ray, out float enterDist)) { Vector3 planePoint = ray.GetPoint(enterDist); if (NavMesh.SamplePosition(planePoint, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas)) { hitPoint = navHit.position; return true; } hitPoint = planePoint; return true; } return false; } private bool TryRaycastInteractable(Vector2 screenPosition, out IInteractable interactable) { interactable = null; FindCamera(); if (playerCamera == null) return false; Ray ray = playerCamera.ScreenPointToRay(screenPosition); RaycastHit[] hits = Physics.RaycastAll(ray, 300f, interactableLayers, QueryTriggerInteraction.Collide); if (hits.Length > 0) { Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); foreach (var hit in hits) { if (hit.collider.transform.root == transform.root) continue; var found = hit.collider.GetComponentInParent(); if (found != null && found.CanInteract(this)) { interactable = found; return true; } } } return false; } #endregion #region Movement & Interaction Execution /// /// Commands the shopkeeper to walk to a position on the floor, canceling any active interaction target. /// public void CommandMove(Vector3 destination) { ClearPendingTarget(); if (!agent.isOnNavMesh) { if (NavMesh.SamplePosition(transform.position, out NavMeshHit navHit, 5.0f, NavMesh.AllAreas)) { agent.Warp(navHit.position); } else { Debug.LogWarning("[PlayerController] Cannot move: Agent is not on NavMesh!", this); return; } } lastClickedDestination = destination; agent.stoppingDistance = destinationThreshold; agent.isStopped = false; agent.velocity = Vector3.zero; // Kill residual forward momentum to prevent circular turn arcs bool setSuccess = agent.SetDestination(destination); lastNavMeshDestination = agent.destination; hasActiveMoveCommand = true; if (enableDebugLogs) { float dist = Vector3.Distance(transform.position, destination); Debug.Log($"[PlayerController] CLICKED: {destination} --> NAVMESH: {agent.destination} (Player at {transform.position}, Dist: {dist:F2}m, Success: {setSuccess}, PathStatus: {agent.pathStatus})"); } ShowDestinationMarker(destination); OnDestinationSet?.Invoke(destination); } /// /// Commands the shopkeeper to approach an interactable (shelf, crate, NPC) and interact upon arrival. /// If already within range, interacts immediately. /// public void CommandInteract(IInteractable target) { if (target == null || !target.CanInteract(this)) return; float distance = GetDistanceToInteractable(target); float interactionDistance = target.InteractionDistance > 0 ? target.InteractionDistance : defaultInteractionDistance; if (enableDebugLogs) { Debug.Log($"[PlayerController] INTERACT COMMAND: Target: '{target.InteractionPrompt}' at {target.InteractionTransform.position} | Current Distance: {distance:F2}m (Allowed: {interactionDistance:F2}m)"); } if (distance <= interactionDistance) { ClearPendingTarget(); agent.ResetPath(); agent.velocity = Vector3.zero; FaceTarget(target.InteractionTransform.position); ExecuteInteraction(target); } else { pendingTarget = target; hasActiveMoveCommand = false; lastClickedDestination = target.InteractionTransform.position; agent.stoppingDistance = Mathf.Max(0.5f, interactionDistance * 0.85f); agent.isStopped = false; agent.SetDestination(target.InteractionTransform.position); lastNavMeshDestination = agent.destination; ShowDestinationMarker(target.InteractionTransform.position); OnInteractionTargetSet?.Invoke(target); } } /// /// Handles stopping cleanly when reaching ground move destinations to prevent orbiting/circling. /// private void HandleDestinationArrival() { if (pendingTarget != null) return; if (!hasActiveMoveCommand) return; if (!agent.pathPending && agent.hasPath) { Vector3 flatPos = new Vector3(transform.position.x, 0, transform.position.z); Vector3 flatDest = new Vector3(agent.destination.x, 0, agent.destination.z); float flatDist = Vector3.Distance(flatPos, flatDest); // Normal arrival: within stopping distance or remaining distance if (flatDist <= agent.stoppingDistance || agent.remainingDistance <= agent.stoppingDistance) { if (enableDebugLogs) { Debug.Log($"[PlayerController] ARRIVED AT DESTINATION: Position: {transform.position} (Dist: {flatDist:F2}m)"); } hasActiveMoveCommand = false; agent.ResetPath(); agent.velocity = Vector3.zero; HideDestinationMarker(); } // Anti-orbiting safeguard: if agent is close and moving away from destination else if (flatDist < 0.8f && Vector3.Dot(agent.velocity.normalized, (flatDest - flatPos).normalized) < 0f) { if (enableDebugLogs) { Debug.Log($"[PlayerController] OVERSHOOT PREVENTED (STOPPED): Position: {transform.position} (Dist: {flatDist:F2}m)"); } hasActiveMoveCommand = false; agent.ResetPath(); agent.velocity = Vector3.zero; HideDestinationMarker(); } } } private void HandlePendingInteraction() { if (pendingTarget == null) return; if (!pendingTarget.CanInteract(this)) { ClearPendingTarget(); return; } float interactionDistance = pendingTarget.InteractionDistance > 0 ? pendingTarget.InteractionDistance : defaultInteractionDistance; float distance = GetDistanceToInteractable(pendingTarget); if (distance <= interactionDistance || (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)) { IInteractable targetToInteract = pendingTarget; ClearPendingTarget(); agent.ResetPath(); agent.velocity = Vector3.zero; FaceTarget(targetToInteract.InteractionTransform.position); ExecuteInteraction(targetToInteract); } } private void ExecuteInteraction(IInteractable target) { HideDestinationMarker(); target.Interact(this); OnInteracted?.Invoke(target); } public void ClearPendingTarget() { pendingTarget = null; } private float GetDistanceToInteractable(IInteractable target) { Vector3 targetPos = target.InteractionTransform.position; Vector3 playerPos = transform.position; targetPos.y = playerPos.y; return Vector3.Distance(playerPos, targetPos); } private void UpdateRotation() { if (agent == null) return; Vector3 targetDirection = Vector3.zero; if (pendingTarget != null) { targetDirection = pendingTarget.InteractionTransform.position - transform.position; } else if (hasActiveMoveCommand && agent.hasPath) { targetDirection = agent.steeringTarget - transform.position; } targetDirection.y = 0; if (targetDirection.sqrMagnitude > 0.001f) { Quaternion targetRot = Quaternion.LookRotation(targetDirection.normalized); float angleDiff = Quaternion.Angle(transform.rotation, targetRot); // Strict Turn-in-place: if angle is large, stop all movement until facing the target if (turnInPlace && angleDiff > 10f) { transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, turnSpeed * Time.deltaTime); agent.velocity = Vector3.zero; // Completely halt forward momentum while turning! agent.speed = 0f; } else { transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, turnSpeed * Time.deltaTime); float baseSpeed = IsCarryingItem ? (walkSpeed * carrySpeedMultiplier) : walkSpeed; agent.speed = baseSpeed; } } } private void FaceTarget(Vector3 targetPosition) { Vector3 direction = (targetPosition - transform.position); direction.y = 0; if (direction.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.LookRotation(direction); } } #endregion #region Hover Handling private void HandleHover() { if (Mouse.current == null && Pointer.current == null) return; Vector2 screenPos = GetPointerPosition(); if (IsPointerOverUI(screenPos)) { ClearHover(); return; } if (TryRaycastInteractable(screenPos, out IInteractable hitInteractable) && hitInteractable.CanInteract(this)) { if (currentHovered != hitInteractable) { ClearHover(); currentHovered = hitInteractable; currentHovered.OnHoverEnter(this); OnHoverChanged?.Invoke(currentHovered); } } else { ClearHover(); } } private void ClearHover() { if (currentHovered != null) { currentHovered.OnHoverExit(this); currentHovered = null; OnHoverChanged?.Invoke(null); } } #endregion #region Shopkeeper Carry System /// /// Attaches an item, delivery crate, or stock box into the shopkeeper's carry socket. /// public bool CarryItem(GameObject item) { if (item == null || IsCarryingItem) return false; currentCarriedItem = item; Transform targetParent = carrySocket != null ? carrySocket : transform; item.transform.SetParent(targetParent); item.transform.localPosition = Vector3.zero; item.transform.localRotation = Quaternion.identity; if (item.TryGetComponent(out Rigidbody rb)) { rb.isKinematic = true; } if (item.TryGetComponent(out Collider col)) { col.enabled = false; } UpdateMovementSpeed(); OnItemPickedUp?.Invoke(item); return true; } /// /// Drops the currently carried item onto the ground in front of the player. /// public GameObject DropCarriedItem(Vector3? dropPosition = null) { if (!IsCarryingItem) return null; GameObject dropped = currentCarriedItem; currentCarriedItem = null; dropped.transform.SetParent(null); Vector3 targetDrop = dropPosition ?? (transform.position + transform.forward * 0.8f + Vector3.up * 0.2f); dropped.transform.position = targetDrop; if (dropped.TryGetComponent(out Collider col)) { col.enabled = true; } if (dropped.TryGetComponent(out Rigidbody rb)) { rb.isKinematic = false; } UpdateMovementSpeed(); OnItemDropped?.Invoke(dropped); return dropped; } /// /// Removes the carried item reference without placing it in the world (e.g. placed onto a shelf). /// public void ClearCarriedItem() { currentCarriedItem = null; UpdateMovementSpeed(); } private void UpdateMovementSpeed() { float speed = walkSpeed; if (IsCarryingItem) { speed *= carrySpeedMultiplier; } agent.speed = speed; } #endregion #region Visuals & Animation private void ShowDestinationMarker(Vector3 position) { if (destinationMarkerInstance != null) { destinationMarkerInstance.transform.position = position + Vector3.up * 0.05f; destinationMarkerInstance.SetActive(true); } } private void HideDestinationMarker() { if (destinationMarkerInstance != null) { destinationMarkerInstance.SetActive(false); } } private void UpdateAnimation() { if (animator == null) return; float currentSpeed = agent.velocity.magnitude; bool isMoving = agent.velocity.sqrMagnitude > 0.05f && !agent.isStopped && agent.hasPath; animator.SetFloat(speedHash, currentSpeed); animator.SetBool(isMovingHash, isMoving); animator.SetBool(isCarryingHash, IsCarryingItem); } #endregion #region Editor Gizmos private void OnDrawGizmos() { // Draw Clicked Point in bright RED if (lastClickedDestination != Vector3.zero) { Gizmos.color = Color.red; Gizmos.DrawSphere(lastClickedDestination, 0.25f); Gizmos.DrawLine(transform.position, lastClickedDestination); } // Draw NavMesh Agent Destination in bright GREEN if (agent != null && (hasActiveMoveCommand || agent.hasPath)) { Gizmos.color = Color.green; Gizmos.DrawWireSphere(agent.destination, agent.stoppingDistance); Gizmos.DrawLine(transform.position, agent.destination); } } private void OnDrawGizmosSelected() { if (agent != null && agent.hasPath) { Gizmos.color = Color.green; Vector3 prev = transform.position; foreach (Vector3 corner in agent.path.corners) { Gizmos.DrawLine(prev, corner); Gizmos.DrawSphere(corner, 0.15f); prev = corner; } } if (pendingTarget != null && pendingTarget.InteractionTransform != null) { Gizmos.color = Color.cyan; Gizmos.DrawLine(transform.position, pendingTarget.InteractionTransform.position); Gizmos.DrawWireSphere(pendingTarget.InteractionTransform.position, pendingTarget.InteractionDistance); } if (carrySocket != null) { Gizmos.color = Color.magenta; Gizmos.DrawWireCube(carrySocket.position, Vector3.one * 0.2f); } } #endregion } }