using UnityEngine; /// /// MinimapMarker - Creates a 3D marker visible to the minimap camera. /// This follows the same pattern as TeamIndicator but renders on a minimap-specific layer. /// Attach to character prefabs or add dynamically via MinimapMarkerManager. /// /// IMPORTANT: You MUST create a layer called "Minimap" in Unity Project Settings /// and configure your Main Camera to NOT render this layer (uncheck it in Culling Mask). /// public class MinimapMarker : MonoBehaviour { [Header("Marker Settings")] [SerializeField] private float heightOffset = 5f; // Height above character for minimap visibility [SerializeField] private float markerSize = 1f; // Size of marker in world units [Header("Team Colors")] [SerializeField] private Color allyColor = new Color(0.2f, 0.8f, 0.2f, 1f); // Green [SerializeField] private Color enemyColor = new Color(0.9f, 0.2f, 0.2f, 1f); // Red [SerializeField] private Color controlledColor = Color.green; // White (GTA V style) [Header("Layer Settings")] [SerializeField] private string minimapLayerName = "Minimap"; private GameObject markerObject; private MeshRenderer markerRenderer; private TeamMember teamMember; private string myTeamTag; private bool isCurrentlyControlled = false; private bool hasConfiguredLayer = false; // Cached textures to avoid regeneration private static Texture2D cachedCircleTexture; private static Texture2D cachedArrowTexture; // Static reference to current player team tag private static string currentPlayerTeamTag = "Player"; private bool _markerCreated; private void Start() { // Works in ALL modes (Cash System, Practice, etc.) teamMember = GetComponent(); myTeamTag = gameObject.tag; CreateMarker(); UpdateMarkerColor(); } private void LateUpdate() { if (markerObject == null) return; // Ensure layer is correctly set (handles late layer creation) if (!hasConfiguredLayer) { ConfigureLayer(); } // Check if this character is currently controlled bool wasControlled = isCurrentlyControlled; isCurrentlyControlled = teamMember != null && teamMember.IsPlayerControlled; if (isCurrentlyControlled) { currentPlayerTeamTag = myTeamTag; // PARENT to player so position + yaw follow exactly (no lag/jitter) if (markerObject.transform.parent != transform) markerObject.transform.SetParent(transform, false); // Local position: straight up; local rotation: flat quad facing up // The minimap camera rotates with the player, so the marker just // sits above the player and the camera handles heading visuals. markerObject.transform.localPosition = Vector3.up * heightOffset; markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); markerObject.transform.localScale = Vector3.one * (markerSize * 2f); } else { // AI / enemy markers: also parent to owner so they can't drift if (markerObject.transform.parent != transform) markerObject.transform.SetParent(transform, false); markerObject.transform.localPosition = Vector3.up * heightOffset; markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); markerObject.transform.localScale = Vector3.one * markerSize; } // Update color/shape if control status changed if (wasControlled != isCurrentlyControlled) { UpdateMarkerVisuals(); } markerObject.SetActive(true); } private void CreateMarker() { // Guard: only ever create ONE marker per component instance if (_markerCreated && markerObject != null) return; markerObject = GameObject.CreatePrimitive(PrimitiveType.Quad); markerObject.name = $"{gameObject.name}_MinimapMarker"; // Parent to owner immediately so it follows position + rotation markerObject.transform.SetParent(transform, false); markerObject.transform.localPosition = Vector3.up * heightOffset; markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); markerObject.transform.localScale = Vector3.one * markerSize; // Remove collider - we don't need it Collider col = markerObject.GetComponent(); if (col != null) Destroy(col); // Configure layer ConfigureLayer(); // Get renderer and create material markerRenderer = markerObject.GetComponent(); Material markerMaterial = new Material(Shader.Find("Sprites/Default")); if (markerMaterial != null) { markerRenderer.material = markerMaterial; } _markerCreated = true; Debug.Log($"[MinimapMarker] Created marker for {gameObject.name} on layer {markerObject.layer}"); } private void ConfigureLayer() { int minimapLayer = LayerMask.NameToLayer(minimapLayerName); if (minimapLayer == -1) { // Layer not found - this is a critical setup issue // Log error once and use a high layer number that's less likely to be rendered if (!hasConfiguredLayer) { Debug.LogError($"[MinimapMarker] CRITICAL: Layer '{minimapLayerName}' not found! " + "Please create this layer in Project Settings > Tags and Layers. " + "IMPORTANT: Set your Main Camera's Culling Mask to EXCLUDE the 'Minimap' layer!"); } // Use layer 31 as fallback (typically unused, less likely to show on main camera) markerObject.layer = 31; } else { markerObject.layer = minimapLayer; hasConfiguredLayer = true; } } private void UpdateMarkerVisuals() { if (markerRenderer == null) return; Color targetColor; Texture2D texture; // Currently controlled character if (isCurrentlyControlled) { targetColor = controlledColor; texture = GetArrowTexture(); // Directional arrow } // Determine if ally or enemy relative to player else if (myTeamTag == currentPlayerTeamTag) { targetColor = allyColor; texture = GetCircleTexture(); // Round circle } else { targetColor = enemyColor; texture = GetCircleTexture(); // Round circle } markerRenderer.material.color = targetColor; markerRenderer.material.mainTexture = texture; } // Get cached circle texture private Texture2D GetCircleTexture() { if (cachedCircleTexture == null) { cachedCircleTexture = GenerateCircleTexture(); } return cachedCircleTexture; } // Get cached arrow texture private Texture2D GetArrowTexture() { if (cachedArrowTexture == null) { cachedArrowTexture = GenerateArrowTexture(); } return cachedArrowTexture; } // Generate a clean solid dot — GTA V style (filled circle with soft edge) private Texture2D GenerateCircleTexture() { int size = 64; Texture2D tex = new Texture2D(size, size); Color[] colors = new Color[size * size]; float center = size / 2f; float radius = size / 2f - 4f; // Slightly smaller for clean edges for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { float dist = Vector2.Distance(new Vector2(x, y), new Vector2(center, center)); if (dist <= radius - 1.5f) { // Solid fill colors[y * size + x] = Color.white; } else if (dist <= radius + 1.5f) { // Anti-aliased edge float alpha = 1f - Mathf.Clamp01((dist - radius + 1.5f) / 3f); colors[y * size + x] = new Color(1, 1, 1, alpha); } else { colors[y * size + x] = Color.clear; } } } tex.SetPixels(colors); tex.Apply(); return tex; } // Generate a GTA V-style chevron (V-shape) pointing up // Sharp pointed front, open back — gives instant directional clarity private Texture2D GenerateArrowTexture() { int size = 64; Texture2D tex = new Texture2D(size, size); Color[] colors = new Color[size * size]; // Clear transparency for (int i = 0; i < colors.Length; i++) colors[i] = Color.clear; float cx = size / 2f; float cy = size / 2f; float thickness = 5f; // Chevron line thickness // Chevron is two angled lines meeting at a point (top center) // Left arm: from bottom-left to top-center // Right arm: from bottom-right to top-center Vector2 tip = new Vector2(cx, size * 0.85f); // Top tip Vector2 bottomLeft = new Vector2(size * 0.15f, size * 0.25f); // Bottom left Vector2 bottomRight = new Vector2(size * 0.85f, size * 0.25f); // Bottom right for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { Vector2 p = new Vector2(x, y); // Distance to left arm line segment float distLeft = DistanceToSegment(p, bottomLeft, tip); // Distance to right arm line segment float distRight = DistanceToSegment(p, bottomRight, tip); float minDist = Mathf.Min(distLeft, distRight); if (minDist <= thickness) { // Solid white with anti-aliased edges float alpha = 1f - Mathf.Clamp01((minDist - thickness + 1.5f) / 1.5f); alpha = Mathf.Max(alpha, 0f); if (minDist < thickness - 1f) alpha = 1f; colors[y * size + x] = new Color(1, 1, 1, alpha); } } } tex.SetPixels(colors); tex.Apply(); return tex; } /// /// Distance from point P to line segment AB /// private static float DistanceToSegment(Vector2 p, Vector2 a, Vector2 b) { Vector2 ab = b - a; float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / Vector2.Dot(ab, ab)); Vector2 closest = a + t * ab; return Vector2.Distance(p, closest); } private void UpdateMarkerColor() { UpdateMarkerVisuals(); } private void OnDestroy() { if (markerObject != null) { Destroy(markerObject); } } /// /// Force update the marker color (call after team changes) /// public void RefreshMarker() { myTeamTag = gameObject.tag; UpdateMarkerColor(); } /// /// Set marker visibility /// public void SetVisible(bool visible) { if (markerObject != null) { markerObject.SetActive(visible); } } /// /// Static method to update all markers when player changes /// public static void RefreshAllMarkers() { foreach (var marker in FindObjectsOfType()) { marker.RefreshMarker(); } } }