Add tracking pins for offscreen targets
This commit is contained in:
237
Assets/Scripts/UI/Tracking/OffScreenPin.cs
Normal file
237
Assets/Scripts/UI/Tracking/OffScreenPin.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user