331 lines
11 KiB
C#
331 lines
11 KiB
C#
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<RectTransform>();
|
|
if (panelRectTransform != null) {
|
|
Vector2 currentSize = panelRectTransform.sizeDelta;
|
|
if (currentSize.x != value) {
|
|
panelRectTransform.sizeDelta = new Vector2(value, currentSize.y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Awake () {
|
|
canvas = GetComponent<Canvas>();
|
|
line = GetComponentInChildren<Image>();
|
|
text = GetComponentInChildren<TextMeshProUGUI>();
|
|
panel = transform.GetChild(0).GetComponentInChildren<RectTransform>();
|
|
canvasGroup = GetComponent<CanvasGroup>();
|
|
if (zTestModeID == 0) {
|
|
zTestModeID = Shader.PropertyToID("_ZTestMode");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures the label Canvas render mode for screen space or world space rendering.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the label to the pool
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show the label
|
|
/// </summary>
|
|
public virtual void Show () {
|
|
isVisible = true;
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying) {
|
|
HighlightLabelPoolManager.Refresh();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide the label
|
|
/// </summary>
|
|
public virtual void Hide () {
|
|
if (this == null) return;
|
|
gameObject.SetActive(false);
|
|
isVisible = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide & destroy the label
|
|
/// </summary>
|
|
public virtual void Release () {
|
|
if (this == null) return;
|
|
Hide();
|
|
if (isPooled) {
|
|
ReturnToPool();
|
|
}
|
|
}
|
|
}
|
|
}
|