chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
351
Assets/Scripts/Managers/MinimapMarker.cs
Normal file
351
Assets/Scripts/Managers/MinimapMarker.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class MinimapMarker : MonoBehaviour
|
||||
{
|
||||
[Header("Marker Settings")]
|
||||
[SerializeField] private float heightOffset = 5f; // Height above character for minimap visibility
|
||||
[SerializeField] private float markerSize = 6f; // 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.white; // 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<TeamMember>();
|
||||
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<Collider>();
|
||||
if (col != null) Destroy(col);
|
||||
|
||||
// Configure layer
|
||||
ConfigureLayer();
|
||||
|
||||
// Get renderer and create material
|
||||
markerRenderer = markerObject.GetComponent<MeshRenderer>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distance from point P to line segment AB
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force update the marker color (call after team changes)
|
||||
/// </summary>
|
||||
public void RefreshMarker()
|
||||
{
|
||||
myTeamTag = gameObject.tag;
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set marker visibility
|
||||
/// </summary>
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
if (markerObject != null)
|
||||
{
|
||||
markerObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static method to update all markers when player changes
|
||||
/// </summary>
|
||||
public static void RefreshAllMarkers()
|
||||
{
|
||||
foreach (var marker in FindObjectsOfType<MinimapMarker>())
|
||||
{
|
||||
marker.RefreshMarker();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user