334 lines
12 KiB
C#
334 lines
12 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<RectTransform> indicatorPool = new List<RectTransform>();
|
|
private List<Image> indicatorImages = new List<Image>();
|
|
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<MinimapManager>();
|
|
if (mm != null)
|
|
{
|
|
minimapCamera = mm.GetComponent<Camera>();
|
|
}
|
|
|
|
// Auto-find minimap rect if not assigned
|
|
if (minimapRect == null)
|
|
{
|
|
minimapRect = GetComponent<RectTransform>();
|
|
|
|
// Try parent if this component doesn't have RectTransform
|
|
if (minimapRect == null)
|
|
{
|
|
minimapRect = GetComponentInParent<RectTransform>();
|
|
}
|
|
}
|
|
|
|
// 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<RectTransform>();
|
|
rect.sizeDelta = new Vector2(indicatorSize, indicatorSize);
|
|
rect.localScale = Vector3.one;
|
|
|
|
Image img = indicatorObj.AddComponent<Image>();
|
|
img.sprite = arrowSprite;
|
|
img.color = enemyColor;
|
|
img.raycastTarget = false;
|
|
|
|
// Add outline for better visibility
|
|
Outline outline = indicatorObj.AddComponent<Outline>();
|
|
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<CashBag>.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<TeamVault>();
|
|
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<CashSystemManager>();
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates and attaches an edge indicator to a minimap UI element
|
|
/// </summary>
|
|
public static MinimapEdgeIndicator AttachTo(RectTransform minimapUI)
|
|
{
|
|
if (minimapUI == null) return null;
|
|
|
|
MinimapEdgeIndicator existing = minimapUI.GetComponent<MinimapEdgeIndicator>();
|
|
if (existing != null) return existing;
|
|
|
|
MinimapEdgeIndicator indicator = minimapUI.gameObject.AddComponent<MinimapEdgeIndicator>();
|
|
indicator.minimapRect = minimapUI;
|
|
|
|
Debug.Log("[MinimapEdgeIndicator] Attached to " + minimapUI.name);
|
|
return indicator;
|
|
}
|
|
}
|