using UnityEngine; /// /// A single radar blip spawned by . Each frame it orients itself so its forward (+Z, where /// the "Point" marker sits) faces the direction from the reference camera to a world-space target, visualising /// the bearing camera -> target. Tracks continuously (the camera turns / the target moves) and self-destructs /// once its lifetime elapses - or earlier if its target is destroyed. The point's base colour and size encode /// the two things the flat radar disc can't show via position alone: tints it by /// how far the target sits ahead of / behind the camera, and its scale shrinks with camera -> target distance. /// /// On top of the radar-wide fade, each blip fades in when it spawns and fades out at the end of its life (or when /// its world target vanishes) via its own factor, so blips appear/disappear smoothly instead /// of popping. Fade-in and fade-out speeds are independent; the factor persists across so a /// refreshed blip is pulled back to full opacity rather than flickering to zero first. /// [DisallowMultipleComponent] public sealed class RadarTarget : MonoBehaviour { public Renderer pointRenderer; [Tooltip("Point tint by bearing: sampled at 0 when the target is dead ahead of the camera, at 1 when directly behind. Flip the keys for the reverse.")] [SerializeField] Gradient forwardGradient; [Tooltip("Point transform scale at the near (x) and far (y) end of pointDistanceRange.")] [SerializeField] Vector2 pointDistanceScale; [Tooltip("camera -> target distance mapped onto pointDistanceScale: x = near distance, y = far distance.")] [SerializeField] Vector2 pointDistanceRange; [Header("Fade")] [Tooltip("Per-blip fade-in speed in alpha units per second when the blip appears. e.g. 6 = ~0.17s. 0 = appear instantly.")] [SerializeField] float fadeInSpeed = 6f; [Tooltip("Per-blip fade-out speed in alpha units per second at the end of life / when the target vanishes. e.g. 4 = ~0.25s. 0 = vanish instantly.")] [SerializeField] float fadeOutSpeed = 4f; private Transform _target; private Camera _camera; private float _lifetime; private float _age; private bool _initialised; private float _fade; // per-blip 0..1 ramp, multiplied on top of the radar-wide fade; persists across Refresh private MaterialPropertyBlock _mpb; private Color _pointColor = Color.white; // last gradient-evaluated point colour; its alpha is scaled by the radar fade private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); public Transform Target => _target; public float RemainingLifetime => Mathf.Max(0f, _lifetime - _age); public void Init(Transform target, Camera referenceCamera, float lifetime) { _target = target; _camera = referenceCamera; _lifetime = lifetime; _age = 0f; _initialised = true; Aim(); PushPointColor(); } /// Restarts this blip's countdown to seconds. Used when a ping arrives /// for a target this radar already tracks: the existing blip is refreshed instead of stacking a duplicate. public void Refresh(float lifetime) { _lifetime = lifetime; _age = 0f; } // LateUpdate so the aim uses the camera's final position/rotation for this frame. private void LateUpdate() { if (!_initialised) return; _age += Time.deltaTime; // Fade out at the very end of life, or immediately if the world target vanished (e.g. a caught hider) // instead of dangling. The fade-out window equals the time to ramp 1 -> 0 at fadeOutSpeed, so the point // reaches full transparency right as the lifetime elapses; then the blip destroys itself. float fadeOutWindow = fadeOutSpeed > 0f ? 1f / fadeOutSpeed : 0f; bool fadingOut = !_target || RemainingLifetime <= fadeOutWindow; float goal = fadingOut ? 0f : 1f; float speed = Mathf.Max(0f, fadingOut ? fadeOutSpeed : fadeInSpeed); _fade = Mathf.MoveTowards(_fade, goal, speed * Time.deltaTime); if (fadingOut && (speed <= 0f || _fade <= 0.0001f)) { Destroy(gameObject); return; } Aim(); // keeps last orientation/colour if the target is gone (Aim early-returns) PushPointColor(); // every frame so the alpha tracks both the radar fade and this blip's own fade } private void Aim() { if (!_target || !_camera) return; Vector3 dir = _target.position - _camera.transform.position; float sqr = dir.sqrMagnitude; if (sqr < 1e-8f) return; // camera sitting on the target -> keep the last orientation/colour/scale transform.rotation = Quaternion.LookRotation(dir); float distance = Mathf.Sqrt(sqr); ApplyDistanceScale(distance); ApplyForwardColor(dir / distance); } // Shrinks the blip with distance: pointDistanceRange (near, far) remapped onto pointDistanceScale (near, far), clamped. private void ApplyDistanceScale(float distance) { if (!pointRenderer) return; float t = Mathf.InverseLerp(pointDistanceRange.x, pointDistanceRange.y, distance); float s = Mathf.Lerp(pointDistanceScale.x, pointDistanceScale.y, t); pointRenderer.transform.localScale = new Vector3(s, s, s); } // Tints the point's base colour by how far the target sits ahead of (0) vs directly behind (1) the camera. // Only caches the colour here; PushPointColor writes it (with the fade applied) so the alpha can track the // radar fade every frame independent of when the bearing last changed. private void ApplyForwardColor(Vector3 dirNormalized) { if (forwardGradient == null) return; float t = Vector3.Angle(_camera.transform.forward, dirNormalized) / 180f; _pointColor = forwardGradient.Evaluate(t); } // Writes the point's _BaseColor with its alpha scaled by both the radar's shared fade factor (so blips fade in // step with the disc) and this blip's own fade (so it eases in on spawn and out at the end of its life). private void PushPointColor() { if (!pointRenderer) return; Color c = _pointColor; c.a *= (Radar.Instance ? Radar.Instance.CurrentFade : 1f) * _fade; _mpb ??= new MaterialPropertyBlock(); pointRenderer.GetPropertyBlock(_mpb); _mpb.SetColor(BaseColorId, c); pointRenderer.SetPropertyBlock(_mpb); } }