Files
AppleHillsProduction/Assets/Scripts/UI/Tracking/OffScreenPin.cs

238 lines
8.4 KiB
C#
Raw Normal View History

using UnityEngine;
using UnityEngine.UI;
using AppleHills.Core;
namespace UI.Tracking
{
/// <summary>
/// UI pin that displays on screen edges pointing to off-screen targets.
/// Consists of a static icon in the center and a rotatable frame that points toward the target.
/// Optionally displays distance to target if enabled.
/// Updates every frame for smooth tracking.
/// </summary>
public class OffScreenPin : MonoBehaviour
{
[Header("References")]
[Tooltip("The image component that displays the target's icon (static, in center)")]
[SerializeField] private Image iconImage;
[Tooltip("The image component that rotates to point toward the target (default: pointing downward)")]
[SerializeField] private Image frameImage;
[Tooltip("Optional: Text component to display distance to target")]
[SerializeField] private TMPro.TextMeshProUGUI distanceText;
private RectTransform _rectTransform;
private TrackableTarget _target;
private bool _isInitialized;
private float _screenPadding;
private bool _trackDistance;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
}
private void Update()
{
// Update position and rotation every frame for smooth tracking
if (_isInitialized && _target != null)
{
Camera mainCamera = QuickAccess.Instance?.MainCamera;
if (mainCamera != null)
{
UpdatePositionAndRotation(mainCamera, _screenPadding);
}
// Update distance display if enabled
if (_trackDistance && distanceText != null)
{
UpdateDistanceDisplay();
}
}
}
/// <summary>
/// Initialize the pin with a target reference and set the icon
/// </summary>
public void Initialize(TrackableTarget target, float screenPadding)
{
_target = target;
_isInitialized = true;
_screenPadding = screenPadding;
_trackDistance = target.TrackDistance;
// Set the icon sprite if available
if (iconImage != null && target.Icon != null)
{
iconImage.sprite = target.Icon;
iconImage.enabled = true;
}
else if (iconImage != null)
{
iconImage.enabled = false;
}
// Configure distance text
if (distanceText != null)
{
distanceText.gameObject.SetActive(_trackDistance);
}
}
/// <summary>
/// Update the pin's position and rotation based on the target's world position.
/// Called by the manager each update cycle.
/// </summary>
public void UpdatePositionAndRotation(Camera cam, float screenPadding)
{
if (!_isInitialized || _target == null)
return;
// Get target position in screen space
Vector3 targetScreenPos = cam.WorldToScreenPoint(_target.WorldPosition);
// Calculate direction from screen center to target
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
Vector2 targetScreenPos2D = new Vector2(targetScreenPos.x, targetScreenPos.y);
Vector2 directionToTarget = (targetScreenPos2D - screenCenter).normalized;
// Calculate screen bounds with padding (inset from edges)
float minX = screenPadding;
float maxX = Screen.width - screenPadding;
float minY = screenPadding;
float maxY = Screen.height - screenPadding;
// Find intersection point with screen bounds
Vector2 intersectionPoint = CalculateScreenEdgeIntersection(
screenCenter,
directionToTarget,
minX, maxX, minY, maxY
);
// Offset the intersection point slightly toward the center to ensure pin is fully visible
Vector2 offsetTowardCenter = -directionToTarget * screenPadding * 0.5f;
Vector2 finalPosition = intersectionPoint + offsetTowardCenter;
// Update pin position
_rectTransform.position = finalPosition;
// Update frame rotation to point toward target
// Frame's default orientation points downward (0 degrees in UI space)
// In UI space: 0° = down, 90° = right, 180° = up, 270° = left
// Atan2(y, x) gives angle from right (+X axis), so we need to adjust
float angle = Mathf.Atan2(directionToTarget.y, directionToTarget.x) * Mathf.Rad2Deg;
// Add 90 to convert from "right is 0°" to align with down-pointing sprite
angle = angle + 90f;
if (frameImage != null)
{
frameImage.rectTransform.localRotation = Quaternion.Euler(0, 0, angle);
}
}
/// <summary>
/// Update the distance display text
/// </summary>
private void UpdateDistanceDisplay()
{
// Get distance source (typically the player)
if (TrackingDistanceSource.Instance == null)
{
distanceText.text = "???";
return;
}
// Calculate distance between source and target
float distance = Vector3.Distance(TrackingDistanceSource.Instance.WorldPosition, _target.WorldPosition);
// Format distance nicely (meters with 1 decimal place)
distanceText.text = $"{distance:F1}m";
}
/// <summary>
/// Calculate the intersection point of a ray from center in given direction with screen bounds
/// </summary>
private Vector2 CalculateScreenEdgeIntersection(
Vector2 center,
Vector2 direction,
float minX, float maxX, float minY, float maxY)
{
// Calculate intersection with each edge and find the closest one
float tMin = float.MaxValue;
Vector2 intersection = center;
// Check intersection with right edge (x = maxX)
if (direction.x > 0.001f)
{
float t = (maxX - center.x) / direction.x;
float y = center.y + t * direction.y;
if (y >= minY && y <= maxY && t < tMin)
{
tMin = t;
intersection = new Vector2(maxX, y);
}
}
// Check intersection with left edge (x = minX)
if (direction.x < -0.001f)
{
float t = (minX - center.x) / direction.x;
float y = center.y + t * direction.y;
if (y >= minY && y <= maxY && t < tMin)
{
tMin = t;
intersection = new Vector2(minX, y);
}
}
// Check intersection with top edge (y = maxY)
if (direction.y > 0.001f)
{
float t = (maxY - center.y) / direction.y;
float x = center.x + t * direction.x;
if (x >= minX && x <= maxX && t < tMin)
{
tMin = t;
intersection = new Vector2(x, maxY);
}
}
// Check intersection with bottom edge (y = minY)
if (direction.y < -0.001f)
{
float t = (minY - center.y) / direction.y;
float x = center.x + t * direction.x;
if (x >= minX && x <= maxX && t < tMin)
{
tMin = t;
intersection = new Vector2(x, minY);
}
}
return intersection;
}
/// <summary>
/// Reset the pin for pooling reuse
/// </summary>
public void ResetPin()
{
_target = null;
_isInitialized = false;
if (iconImage != null)
{
iconImage.sprite = null;
iconImage.enabled = false;
}
}
/// <summary>
/// Get the current target (for null checking)
/// </summary>
public TrackableTarget Target => _target;
}
}