238 lines
8.4 KiB
C#
238 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|
|
|