using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
///
/// MinimapEdgeIndicator - Shows arrows on the edge of the minimap UI
/// pointing toward off-screen entities (allies, enemies, cash bags, objectives).
///
/// Like games such as GTA, Fortnite, and PUBG - when entities are outside the
/// minimap view, arrows appear at the border pointing in their direction.
///
/// Attach to the minimap RawImage UI element, or let MinimapManager create it.
///
public class MinimapEdgeIndicator : MonoBehaviour
{
[Header("Settings")]
[SerializeField] private RectTransform minimapRect;
[SerializeField] private float edgePadding = 15f;
[SerializeField] private float indicatorSize = 60f;
[SerializeField] private float updateInterval = 0.1f;
[Header("Visibility")]
[SerializeField] private bool showAllies = true;
[SerializeField] private bool showEnemies = true;
[SerializeField] private bool showCashBags = true;
[SerializeField] private bool showVaults = true;
[Header("Colors")]
[SerializeField] private Color allyColor = new Color(0.2f, 0.8f, 0.2f, 1f);
[SerializeField] private Color enemyColor = new Color(1f, 0.2f, 0.2f, 1f);
[SerializeField] private Color cashBagColor = new Color(1f, 0.85f, 0f, 1f);
[SerializeField] private Color playerVaultColor = new Color(0.2f, 0.9f, 0.2f, 1f);
[SerializeField] private Color enemyVaultColor = new Color(0.9f, 0.2f, 0.2f, 1f);
// Pool of indicator objects
private List indicatorPool = new List();
private List indicatorImages = new List();
private int activeIndicatorCount = 0;
private Camera minimapCamera;
private Transform playerTransform;
private string playerTeamTag = "Player";
private float lastUpdateTime;
private Sprite arrowSprite;
private float playerHeadingDeg; // Cached player Y rotation for GTA V style
// Cached references for performance
private CashSystemManager cachedCSM;
private bool csmSearched = false;
private void Start()
{
// Find minimap camera
MinimapManager mm = FindObjectOfType();
if (mm != null)
{
minimapCamera = mm.GetComponent();
}
// Auto-find minimap rect if not assigned
if (minimapRect == null)
{
minimapRect = GetComponent();
// Try parent if this component doesn't have RectTransform
if (minimapRect == null)
{
minimapRect = GetComponentInParent();
}
}
// Create arrow sprite once
arrowSprite = CreateArrowSprite();
// Create initial pool
for (int i = 0; i < 15; i++)
{
CreateIndicator();
}
Debug.Log("[MinimapEdgeIndicator] Initialized - will show arrows for off-screen entities");
}
private void CreateIndicator()
{
GameObject indicatorObj = new GameObject("EdgeIndicator");
indicatorObj.transform.SetParent(transform, false);
RectTransform rect = indicatorObj.AddComponent();
rect.sizeDelta = new Vector2(indicatorSize, indicatorSize);
rect.localScale = Vector3.one;
Image img = indicatorObj.AddComponent();
img.sprite = arrowSprite;
img.color = enemyColor;
img.raycastTarget = false;
// Add outline for better visibility
Outline outline = indicatorObj.AddComponent();
outline.effectColor = new Color(0, 0, 0, 0.8f);
outline.effectDistance = new Vector2(1, 1);
indicatorObj.SetActive(false);
indicatorPool.Add(rect);
indicatorImages.Add(img);
}
private Sprite CreateArrowSprite()
{
int size = 48; // Higher resolution for sharper arrow
Texture2D tex = new Texture2D(size, size);
tex.filterMode = FilterMode.Bilinear;
Color[] colors = new Color[size * size];
for (int i = 0; i < colors.Length; i++) colors[i] = Color.clear;
Vector2 center = new Vector2(size / 2f, size / 2f);
// Draw a clean filled arrow/chevron pointing right
// (rotation applied at runtime to point toward off-screen targets)
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float nx = (x - center.x) / (size / 2f); // -1 to 1
float ny = (y - center.y) / (size / 2f); // -1 to 1
// Filled arrow pointing right: tip at nx=0.8, base fans from nx=-0.4
// The arrow body fills from base to tip
if (nx >= -0.4f && nx <= 0.8f)
{
float t = (nx + 0.4f) / 1.2f; // 0 at base, 1 at tip
float allowedY = 0.6f * (1f - t); // Max width at base, 0 at tip
if (Mathf.Abs(ny) <= allowedY)
{
// Anti-alias the edges
float edgeDist = allowedY - Mathf.Abs(ny);
float alpha = Mathf.Clamp01(edgeDist * 4f);
colors[y * size + x] = new Color(1, 1, 1, alpha);
}
}
}
}
tex.SetPixels(colors);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
private void Update()
{
if (Time.time - lastUpdateTime < updateInterval) return;
lastUpdateTime = Time.time;
UpdateIndicators();
}
private void UpdateIndicators()
{
// Find player
FindPlayer();
if (playerTransform == null || minimapCamera == null || minimapRect == null) return;
// Hide all first
activeIndicatorCount = 0;
for (int i = 0; i < indicatorPool.Count; i++)
{
indicatorPool[i].gameObject.SetActive(false);
}
Vector3 playerPos = playerTransform.position;
float viewSize = minimapCamera.orthographicSize;
// Cache player heading for GTA V rotating minimap
playerHeadingDeg = playerTransform.eulerAngles.y;
// Get minimap UI half-size
Vector2 halfSize = minimapRect.rect.size / 2f;
// Track all characters using static registry (zero-allocation, replaces FindObjectsOfType)
if (showAllies || showEnemies)
{
foreach (var member in TeamMember.AllMembers)
{
if (member == null || member.transform == playerTransform) continue;
bool isAlly = member.gameObject.CompareTag(playerTeamTag);
if (isAlly && showAllies)
{
ShowIndicatorIfOffscreen(member.transform.position, playerPos, viewSize, halfSize, allyColor);
}
else if (!isAlly && showEnemies)
{
ShowIndicatorIfOffscreen(member.transform.position, playerPos, viewSize, halfSize, enemyColor);
}
}
}
// Cash bags
if (showCashBags)
{
var cashBags = EntityRegistry.GetAll();
for (int i = 0; i < cashBags.Count; i++)
{
if (cashBags[i] == null) continue;
ShowIndicatorIfOffscreen(cashBags[i].transform.position, playerPos, viewSize, halfSize, cashBagColor);
}
}
// Vaults
if (showVaults)
{
TeamVault[] vaults = FindObjectsOfType();
foreach (var vault in vaults)
{
if (vault == null) continue;
Color vColor = vault.TeamTag == playerTeamTag ? playerVaultColor : enemyVaultColor;
ShowIndicatorIfOffscreen(vault.transform.position, playerPos, viewSize, halfSize, vColor);
}
}
}
private void ShowIndicatorIfOffscreen(Vector3 worldPos, Vector3 playerPos, float viewSize, Vector2 halfSize, Color color)
{
// Calculate relative position in world space
Vector3 relativePos = worldPos - playerPos;
Vector2 relativePos2D = new Vector2(relativePos.x, relativePos.z);
// GTA V STYLE: Rotate by negative player heading so edge positions
// are in "minimap space" (which rotates with the player).
// Without this, arrows point to wrong edges when the map is rotated.
float headingRad = -playerHeadingDeg * Mathf.Deg2Rad;
float cos = Mathf.Cos(headingRad);
float sin = Mathf.Sin(headingRad);
Vector2 rotated = new Vector2(
relativePos2D.x * cos - relativePos2D.y * sin,
relativePos2D.x * sin + relativePos2D.y * cos
);
// Convert world distance to minimap UI space
float worldToUIScale = halfSize.y / viewSize;
Vector2 minimapPos = rotated * worldToUIScale;
// Check if within minimap bounds
bool isWithinBounds = Mathf.Abs(minimapPos.x) <= halfSize.x * 0.85f &&
Mathf.Abs(minimapPos.y) <= halfSize.y * 0.85f;
if (isWithinBounds) return; // Already visible on minimap, don't show edge indicator
// Calculate angle for arrow rotation (in rotated/minimap space)
float angle = Mathf.Atan2(rotated.y, rotated.x) * Mathf.Rad2Deg;
// Clamp to edge of minimap
Vector2 normalizedDir = rotated.normalized;
// Find intersection with minimap edge
float maxX = halfSize.x - edgePadding;
float maxY = halfSize.y - edgePadding;
float tX = normalizedDir.x != 0 ? Mathf.Abs(maxX / normalizedDir.x) : float.MaxValue;
float tY = normalizedDir.y != 0 ? Mathf.Abs(maxY / normalizedDir.y) : float.MaxValue;
float t = Mathf.Min(tX, tY);
Vector2 clampedPos = normalizedDir * t;
// Get or create indicator
if (activeIndicatorCount >= indicatorPool.Count)
{
CreateIndicator();
}
RectTransform indicator = indicatorPool[activeIndicatorCount];
Image img = indicatorImages[activeIndicatorCount];
indicator.gameObject.SetActive(true);
indicator.sizeDelta = new Vector2(indicatorSize, indicatorSize); // Force size every frame
indicator.anchoredPosition = clampedPos;
indicator.localRotation = Quaternion.Euler(0, 0, angle);
img.color = color;
activeIndicatorCount++;
}
private void FindPlayer()
{
// Cache CashSystemManager lookup
if (!csmSearched)
{
cachedCSM = FindObjectOfType();
csmSearched = true;
}
// In Cash System mode, find controlled character
if (cachedCSM != null && cachedCSM.CurrentControlledCharacter != null)
{
playerTransform = cachedCSM.CurrentControlledCharacter.transform;
playerTeamTag = cachedCSM.CurrentControlledCharacter.tag;
return;
}
// Fallback to Player tag
if (playerTransform == null)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
playerTeamTag = "Player";
}
}
}
///
/// Creates and attaches an edge indicator to a minimap UI element
///
public static MinimapEdgeIndicator AttachTo(RectTransform minimapUI)
{
if (minimapUI == null) return null;
MinimapEdgeIndicator existing = minimapUI.GetComponent();
if (existing != null) return existing;
MinimapEdgeIndicator indicator = minimapUI.gameObject.AddComponent();
indicator.minimapRect = minimapUI;
Debug.Log("[MinimapEdgeIndicator] Attached to " + minimapUI.name);
return indicator;
}
}