using UnityEngine; using TMPro; public class RayCastSystem : MonoBehaviour { [Header("Layer Settings")] public LayerMask interactableLayers; [Header("Highlight Settings")] public Color highlightColor = new Color(1f, 1f, 0f, 0.35f); public float padding = 1.02f; private Camera mainCamera; private GameObject currentSelection; private GameObject outlineObject; private MeshFilter outlineFilter; private MeshRenderer outlineRenderer; private Rigidbody currentPickedRB = null; [Header("UI Elements")] public TMP_Text itemCountText; void Start() { mainCamera = Camera.main; CreateOutlineObject(); if(itemCountText) itemCountText.gameObject.SetActive(false); } void CreateOutlineObject() { outlineObject = new GameObject("SelectionHighlight"); outlineFilter = outlineObject.AddComponent(); outlineRenderer = outlineObject.AddComponent(); Material ghostMat = new Material(Shader.Find("Standard")); // Optimize Material: Enable GPU Instancing ghostMat.enableInstancing = true; ghostMat.SetFloat("_Mode", 3); // Transparent ghostMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); ghostMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); ghostMat.SetInt("_ZWrite", 0); ghostMat.DisableKeyword("_ALPHATEST_ON"); ghostMat.EnableKeyword("_ALPHABLEND_ON"); ghostMat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); ghostMat.renderQueue = 3000; ghostMat.color = highlightColor; ghostMat.EnableKeyword("_EMISSION"); ghostMat.SetColor("_EmissionColor", highlightColor * 0.5f); outlineRenderer.material = ghostMat; outlineObject.SetActive(false); // OPTIMIZATION: Do not parent to the player. // Keeping it in the root ensures Scale calculations are always 1:1 with the target. outlineObject.transform.SetParent(null); } void Update() { RaycastHit hit; // Perform Raycast if (Physics.Raycast(mainCamera.transform.position, mainCamera.transform.forward, out hit, 100f, interactableLayers)) { GameObject hitObject = hit.collider.gameObject; // Only run expensive logic if the selection ACTUALLY changed if (currentSelection != hitObject) { currentSelection = hitObject; UpdateOutline(currentSelection); UpdateUI(currentSelection); // Separated UI logic for cleanliness } // Keep outline snapped if (currentSelection != null && outlineObject.activeSelf) { FollowTarget(currentSelection); } } else { if (currentSelection != null) ClearSelection(); } } void UpdateUI(GameObject target) { if (itemCountText == null) return; Shelf shelf = target.GetComponent(); if (shelf != null) { // PERFORMANCE FIX: // Instead of getting a whole dictionary, we just ask for the count directly. // You need to add 'public int ItemCount' or similar to your Shelf script. itemCountText.text = $"Items: {shelf.GetTotalItemCount()}"; itemCountText.gameObject.SetActive(true); } else { itemCountText.gameObject.SetActive(false); } } void UpdateOutline(GameObject target) { if(currentPickedRB != null) { outlineObject.SetActive(false); return; } MeshFilter targetMeshFilter = target.GetComponent(); if (targetMeshFilter == null) targetMeshFilter = target.GetComponentInChildren(); if (targetMeshFilter == null) { outlineObject.SetActive(false); return; } // Lightweight: Just swapping the reference, not creating new meshes outlineFilter.sharedMesh = targetMeshFilter.sharedMesh; FollowTarget(target); outlineObject.SetActive(true); } void FollowTarget(GameObject target) { outlineObject.transform.position = target.transform.position; outlineObject.transform.rotation = target.transform.rotation; // This math is safer now that outlineObject is not a child of the Player outlineObject.transform.localScale = target.transform.localScale * padding; } void ClearSelection() { currentSelection = null; outlineObject.SetActive(false); if (itemCountText) itemCountText.gameObject.SetActive(false); } public void SetCurrentPickedRB(Rigidbody rb) { currentPickedRB = rb; if (currentPickedRB != null) ClearSelection(); } }