using System.Collections.Generic; using UnityEngine; /// /// Sits on the "Radar" overlay object (a disc rendered by the Radar Camera in front of the main camera). /// Spawns "Target" blips - clones of the child "Target Template" - each of which continuously points from /// the reference camera toward a world-space transform (visualising the bearing camera -> target) and then /// destroys itself after a given lifetime. See for the per-blip behaviour. /// /// The whole radar overlay is only shown while it is useful: at least one live target, first-person is active, /// AND the match is in its Seek phase. Instead of snapping on/off it fades: a shared 0..1 factor () /// drives the alpha of every (transparent) radar renderer - the disc here, the blip points in - /// and scales the disc from up to 1. Fade-in and fade-out speeds are independent. The overlay /// camera is switched off only once fully faded out, so hidden blips keep counting down their lifetime meanwhile. /// [DisallowMultipleComponent] public sealed class Radar : MonoBehaviour { /// The scene's single radar overlay (lives in the persistent Main scene). Hunter clients spawn /// hider blips onto it from MatchManager's radar-ping sweep. public static Radar Instance { get; private set; } [Tooltip("Template each spawned Target is cloned from. Auto-resolves to the child \"Target Template\".")] [SerializeField] private Transform targetTemplate; [Tooltip("Camera the direction is measured FROM (camera -> target). Auto-resolves to the Camera this radar sits under.")] [SerializeField] private Camera referenceCamera; [Tooltip("Overlay camera that renders the radar. Its rendering is toggled off unless the radar should show " + "(at least one live target AND first-person active). Auto-resolves to the Camera this radar sits under.")] [SerializeField] private Camera radarCamera; [Tooltip("First-person rig. The radar is only shown while first-person is active. Empty = found automatically.")] [SerializeField] private FirstPersonController firstPerson; [Header("Fade")] [Tooltip("Fade-in speed in alpha units per second (0 -> 1). e.g. 5 = ~0.2s to fully appear.")] [SerializeField] private float fadeInSpeed = 5f; [Tooltip("Fade-out speed in alpha units per second (1 -> 0). e.g. 3 = ~0.33s to fully vanish.")] [SerializeField] private float fadeOutSpeed = 3f; [Tooltip("Disc scale multiplier while fully hidden (fade 0); grows to 1 as it fades in, and back down on fade-out.")] [SerializeField] private float hiddenScale = 0.8f; // Live blips, pruned as they self-destruct; the radar shows only while at least one is alive. private readonly List _blips = new List(); // Shared 0..1 visibility factor: drives disc/blip alpha and disc scale. Ramped in LateUpdate. private float _fade; /// Current 0..1 fade factor. Blips multiply their point alpha by this so they fade in step with the disc. public float CurrentFade => _fade; private Vector3 _baseScale; // authored disc scale, captured once; fade lerps hiddenScale..1 on top private Renderer[] _staticRenderers; // radar's own renderers (the disc); the template subtree is excluded private Color[] _staticBaseColors; // their full-opacity _BaseColor; alpha is scaled by _fade each frame private MaterialPropertyBlock _mpb; private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); private void Awake() { Instance = this; if (!targetTemplate) { var t = transform.Find("Target Template"); if (t) targetTemplate = t; } if (!referenceCamera) referenceCamera = GetComponentInParent(); if (!radarCamera) radarCamera = GetComponentInParent(); if (!firstPerson) firstPerson = FindFirstObjectByType(FindObjectsInactive.Include); _baseScale = transform.localScale; CacheStaticRenderers(); _fade = 0f; // hidden until a target exists, first-person is live AND the match is seeking ApplyFade(); } // Gathers the radar's own renderers (the disc) once, with their full-opacity _BaseColor, so their alpha can be // scaled by _fade each frame. The "Target Template" subtree is skipped: it is the blip prototype, and its clones // fade themselves via RadarTarget (which owns their per-blip colour). private void CacheStaticRenderers() { var all = GetComponentsInChildren(true); var renderers = new List(all.Length); var colors = new List(all.Length); foreach (var r in all) { if (targetTemplate && r.transform.IsChildOf(targetTemplate)) continue; renderers.Add(r); var mat = r.sharedMaterial; colors.Add(mat && mat.HasProperty(BaseColorId) ? mat.GetColor(BaseColorId) : Color.white); } _staticRenderers = renderers.ToArray(); _staticBaseColors = colors.ToArray(); } private void OnDestroy() { if (Instance == this) Instance = null; } /// /// Spawns a Target blip that each frame orients toward (from the reference /// camera) and destroys itself after seconds. Returns the spawned blip, or /// null if it could not be created. /// public RadarTarget Spawn(Transform target, float lifetime) { if (!target) { Debug.LogWarning("[Radar] Spawn called without a target transform.", this); return null; } if (!targetTemplate) { Debug.LogWarning("[Radar] No Target Template assigned or found.", this); return null; } // Already tracking this exact transform? Refresh its countdown instead of stacking a duplicate blip, // so a fresh ping (interval or manual) just resets the existing target's lifetime. for (int i = 0; i < _blips.Count; i++) { var existing = _blips[i]; if (existing && existing.Target == target) { existing.Refresh(lifetime); return existing; } } Transform clone = Instantiate(targetTemplate, targetTemplate.parent); clone.name = "Target"; clone.localPosition = targetTemplate.localPosition; clone.localScale = targetTemplate.localScale; clone.gameObject.SetActive(true); var blip = clone.GetComponent(); blip.Init(target, referenceCamera, lifetime); _blips.Add(blip); return blip; } // LateUpdate so the count reflects any blip that self-destructed this frame, in step with RadarTarget's aim. private void LateUpdate() { // Drop self-destructed blips (Destroy leaves a fake-null entry behind). for (int i = _blips.Count - 1; i >= 0; i--) if (!_blips[i]) _blips.RemoveAt(i); bool visible = _blips.Count > 0 && firstPerson && firstPerson.enabled && InSeekPhase(); // Ramp the shared factor toward shown/hidden with the matching speed, so the whole overlay fades // (alpha + scale) instead of snapping. A mid-fade visibility flip reverses smoothly. float target = visible ? 1f : 0f; float speed = Mathf.Max(0f, visible ? fadeInSpeed : fadeOutSpeed); _fade = Mathf.MoveTowards(_fade, target, speed * Time.deltaTime); ApplyFade(); } // The radar only makes sense during the seek phase (hunters chasing hiders). Blips only spawn then anyway, // but a blip's lifetime can outlast the phase edge, so gating here hides the overlay the instant Seek ends. private static bool InSeekPhase() { var m = MatchManager.Instance; return m && m.MatchActive && m.LocalSubPhase == MatchManager.MatchSubPhase.Seek; } // Applies the current _fade to the whole overlay: disc scale (hiddenScale..1), disc alpha (baseAlpha*fade), // and the overlay camera - switched off only once fully faded out, so blips keep ticking their lifetime while // hidden and no rendering happens for an invisible radar. Blips fade themselves via RadarTarget/CurrentFade. private void ApplyFade() { transform.localScale = _baseScale * Mathf.Lerp(hiddenScale, 1f, _fade); if (_staticRenderers != null) { _mpb ??= new MaterialPropertyBlock(); for (int i = 0; i < _staticRenderers.Length; i++) { var r = _staticRenderers[i]; if (!r) continue; Color c = _staticBaseColors[i]; c.a *= _fade; r.GetPropertyBlock(_mpb); _mpb.SetColor(BaseColorId, c); r.SetPropertyBlock(_mpb); } } bool render = _fade > 0.0001f; if (radarCamera && radarCamera.enabled != render) radarCamera.enabled = render; } }