using System; using TMPro; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; namespace HighlightPlus { [ExecuteAlways] public partial class HighlightLabel : MonoBehaviour { public Camera cam; [NonSerialized] public Transform target; [NonSerialized] public Vector3 localPosition; [NonSerialized] public Vector3 worldOffset; [NonSerialized] public Vector2 viewportOffset; [NonSerialized] public bool isVisible; [NonSerialized] public LabelAlignment labelAlignment = LabelAlignment.Auto; [NonSerialized] public bool labelRelativeAlignment; [NonSerialized] public Transform labelAlignmentTransform; public GameObject labelPrefab; internal bool isPooled; [NonSerialized] public bool isWorldSpace; [NonSerialized] public float worldSpaceScale = 0.01f; static Material uiAlwaysOnTopMaterial; static int zTestModeID; Canvas canvas; Image line; TextMeshProUGUI text; RectTransform panel; CanvasGroup canvasGroup; [NonSerialized] public float labelMaxDistance = 250f; [NonSerialized] public float labelFadeStartDistance = 200f; [NonSerialized] public bool labelScaleByDistance; [NonSerialized] public float labelScaleMin = 1f; [NonSerialized] public float labelScaleMax = 1f; [NonSerialized] public float highlightAlpha = 1f; public virtual float alpha { get { return highlightAlpha; } set { highlightAlpha = value; } } public virtual Color textColor { get { return text?.color ?? Color.white; } set { if (text != null) { text.color = value; } } } public virtual string textLabel { get { return text?.text ?? ""; } set { if (text != null) { text.text = value; } } } public virtual float textSize { get { return text?.fontSize ?? 14; } set { if (text != null) { text.fontSize = value; } } } public virtual float width { get { return text?.rectTransform.sizeDelta.x ?? 200; } set { if (text == null) return; if (text.rectTransform != null) { Vector2 currentSize = text.rectTransform.sizeDelta; if (currentSize.x != value) { text.rectTransform.sizeDelta = new Vector2(value, currentSize.y); } } RectTransform panelRectTransform = panel.GetComponent(); if (panelRectTransform != null) { Vector2 currentSize = panelRectTransform.sizeDelta; if (currentSize.x != value) { panelRectTransform.sizeDelta = new Vector2(value, currentSize.y); } } } } void Awake () { canvas = GetComponent(); line = GetComponentInChildren(); text = GetComponentInChildren(); panel = transform.GetChild(0).GetComponentInChildren(); canvasGroup = GetComponent(); if (zTestModeID == 0) { zTestModeID = Shader.PropertyToID("_ZTestMode"); } } /// /// Configures the label Canvas render mode for screen space or world space rendering. /// public void ConfigureCanvas(bool worldSpace) { if (isWorldSpace == worldSpace) return; isWorldSpace = worldSpace; if (canvas == null) return; if (worldSpace) { canvas.renderMode = RenderMode.WorldSpace; canvas.worldCamera = cam; SetAlwaysOnTop(true); } else { canvas.renderMode = RenderMode.ScreenSpaceOverlay; SetAlwaysOnTop(false); } } void SetAlwaysOnTop(bool enabled) { // UI Image (the connecting line) if (line != null) { if (enabled) { if (uiAlwaysOnTopMaterial == null) { uiAlwaysOnTopMaterial = new Material(Shader.Find("UI/Default")); uiAlwaysOnTopMaterial.SetFloat(ShaderParams.ZTest, (float)CompareFunction.Always); } line.material = uiAlwaysOnTopMaterial; } else { line.material = null; } } // TextMeshPro text if (text != null) { text.fontMaterial.SetFloat(zTestModeID, (float)(enabled ? CompareFunction.Always : CompareFunction.LessEqual)); } } /// /// Return the label to the pool /// public virtual void ReturnToPool () { if (!isPooled) return; gameObject.SetActive(false); HighlightLabelPoolManager.ReturnToPool(this); } public virtual void SetPosition(Transform target, Vector3 localPosition, Vector3 worldOffset, Vector2 viewportOffset = default) { this.target = target; this.localPosition = localPosition; this.worldOffset = worldOffset; this.viewportOffset = viewportOffset; } public virtual void UpdatePosition () { if (panel == null || text == null) return; if (cam == null) { cam = Camera.main; if (cam == null) return; } if (target == null) return; Vector3 worldPosition = target.TransformPoint(localPosition); Vector3 worldWithOffset = worldPosition + worldOffset; // Distance-based visibility & scaling (common to both modes) float distanceToCam = Vector3.Distance(cam.transform.position, worldWithOffset); float visibleAlpha = 1f; if (labelMaxDistance > 0f && distanceToCam > labelMaxDistance) { if (canvasGroup != null) canvasGroup.alpha = 0f; return; } if (labelFadeStartDistance > 0f && labelMaxDistance > labelFadeStartDistance && distanceToCam > labelFadeStartDistance) { float t = (distanceToCam - labelFadeStartDistance) / (labelMaxDistance - labelFadeStartDistance); visibleAlpha = Mathf.Clamp01(1f - t); } if (canvasGroup != null) { canvasGroup.alpha = highlightAlpha * visibleAlpha; } if (isWorldSpace) { UpdatePositionWorldSpace(worldWithOffset, distanceToCam); } else { UpdatePositionScreenSpace(worldWithOffset, distanceToCam); } } void UpdatePositionWorldSpace(Vector3 worldWithOffset, float distanceToCam) { // Position canvas in world space transform.position = worldWithOffset; // Billboard: face camera transform.rotation = cam.transform.rotation; // Scale: base worldSpaceScale + optional distance scaling float scale = worldSpaceScale; if (labelScaleByDistance && labelMaxDistance > 0f) { float tScale = Mathf.Clamp01(distanceToCam / labelMaxDistance); scale *= Mathf.Lerp(labelScaleMax, labelScaleMin, tScale); } transform.localScale = new Vector3(scale, scale, scale); // Panel keeps identity transform in world space mode panel.localScale = Vector3.one; panel.localPosition = Vector3.zero; } void UpdatePositionScreenSpace(Vector3 worldWithOffset, float distanceToCam) { // If anchor point is outside of view, hide label without changing active state Vector3 vp = cam.WorldToViewportPoint(worldWithOffset); bool onScreen = vp.z > 0f && vp.x >= 0f && vp.x <= 1f && vp.y >= 0f && vp.y <= 1f; if (!onScreen) { if (canvasGroup != null) { canvasGroup.alpha = 0f; } return; } if (labelScaleByDistance && labelMaxDistance > 0f) { float tScale = Mathf.Clamp01(distanceToCam / labelMaxDistance); float scale = Mathf.Lerp(labelScaleMax, labelScaleMin, tScale); panel.localScale = new Vector3(scale, scale, 1f); } else { panel.localScale = Vector3.one; } Vector3 screenPosition = cam.WorldToScreenPoint(worldWithOffset); // Apply viewport offset in screen space (normalized -1 to 1 range) screenPosition.x += viewportOffset.x * Screen.width; screenPosition.y += viewportOffset.y * Screen.height; panel.position = screenPosition; bool flip = false; if (labelRelativeAlignment && labelAlignmentTransform != null) { Vector3 dirToCam = (cam.transform.position - labelAlignmentTransform.position).normalized; float dot = Vector3.Dot(labelAlignmentTransform.forward, dirToCam); flip = dot > 0; } if (labelAlignment == LabelAlignment.Left) { panel.pivot = flip ? Vector2.zero : Vector2.right; text.alignment = flip ? TextAlignmentOptions.BottomRight : TextAlignmentOptions.BottomLeft; } else if (labelAlignment == LabelAlignment.Right) { panel.pivot = flip ? Vector2.right : Vector2.zero; text.alignment = flip ? TextAlignmentOptions.BottomLeft : TextAlignmentOptions.BottomRight; } else if (labelAlignment == LabelAlignment.Auto && panel.position.x + width * 2f > cam.pixelWidth * 0.95f) { panel.pivot = Vector2.right; text.alignment = TextAlignmentOptions.BottomLeft; } else { panel.pivot = Vector2.zero; text.alignment = TextAlignmentOptions.BottomRight; } } /// /// Show the label /// public virtual void Show () { isVisible = true; #if UNITY_EDITOR if (!Application.isPlaying) { HighlightLabelPoolManager.Refresh(); } #endif } /// /// Hide the label /// public virtual void Hide () { if (this == null) return; gameObject.SetActive(false); isVisible = false; } /// /// Hide & destroy the label /// public virtual void Release () { if (this == null) return; Hide(); if (isPooled) { ReturnToPool(); } } } }