Skip to content

Instantly share code, notes, and snippets.

@Rovsau
Created April 16, 2023 19:53
Show Gist options
  • Save Rovsau/4a863e0bd0c20b920bab8ce520df7a59 to your computer and use it in GitHub Desktop.
Save Rovsau/4a863e0bd0c20b920bab8ce520df7a59 to your computer and use it in GitHub Desktop.
Radar which tracks any world object with a custom Component

ComponentRadar tracks all objects with a RadarComponent.

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Rovsau.Unity.ComponentRadar
{
    // ComponentRadar tracks all objects with a RadarComponent.

    // Execution order:  Must run after all units have moved.
    // Execution order:  Must run before RadarComponent. 

    // In its current form, there is no way to disable components that are far out of range. 
    // It should probably be adapted when in a proper gameplay context. 

    // Radar Size is currently determined by the RectTransform size, not the Image size itself.

    public class ComponentRadar : MonoBehaviour
    {
        public static GameObject __RadarTargetIcon;
        public static RectTransform __RadarIconContainer;
        private static Dictionary<int, RadarComponent> __trackers = new Dictionary<int, RadarComponent>();

        [Header("Config")]
        [SerializeField] private LayerMask _radarLayers;
        [SerializeField] private float _range = 20f;

        [Tooltip("The additional range from which the icon will be visibly lingering at the outermost border.")]
        [SerializeField] private float _rangeFalloff = 10f;
        [SerializeField] private bool _applyRotation = true;
        [SerializeField, Range(0f, 2f)] private float _updateFrequency = 0f;

        [Header("Radar Icon Parent Container")]
        [SerializeField] private RectTransform _radarIconContainer;

        [Header("Prefab")]
        [SerializeField] private GameObject _radarTargetIconPrefab;

        private Transform _player;
        private float _radarSize;
        private float _radarScale;

        private float _timer;
        private float _rangeSquared;

        private void Awake()
        {
            _player = transform;
            _radarSize = _radarIconContainer.rect.width * 0.5f;
            _radarScale = _radarSize / _range;

            float rangeWithFalloff = _range + _rangeFalloff;

            _rangeSquared = rangeWithFalloff * rangeWithFalloff;
            
            __RadarTargetIcon = _radarTargetIconPrefab;
            __RadarIconContainer = _radarIconContainer;
        }

        // Should be FixedUpdate in a Multiplayer setting. 
        private void LateUpdate()
        {
            _timer += Time.deltaTime;
            if (_timer >= _updateFrequency)
            {
                _timer -= _updateFrequency;
                UpdateRadar();
            }
        }

        private void UpdateRadar()
        {
            foreach (RadarComponent tracker in __trackers.Values)
            {
                Vector3 p = _player.position;
                Vector3 t = tracker.Target.position;
                float x = p.x - t.x;
                float y = p.y - t.y;
                float z = p.z - t.z;

                // Compare Euclidean distance.
                // Same relative ordering as actual distance.
                if (x * x + y * y + z * z > _rangeSquared)
                {
                    // Out of range. 
                    tracker.RadarIcon.gameObject.SetActive(false);
                }
                else
                {
                    // If icon was disabled, and component is active, set icon active. 
                    if (!tracker.RadarIcon.gameObject.activeInHierarchy && tracker.enabled) tracker.RadarIcon.gameObject.SetActive(true);
                    tracker.RadarIcon.localPosition = GetIconLocation(tracker.Target);
                }
            }
        }

        private Vector2 GetIconLocation(Transform target)
        {
            // Get vector distance on a 2D plane (XZ). 
            Vector3 distance = target.position - _player.position;
            Vector2 location = new Vector2(distance.x, distance.z) * _radarScale;

            if (_applyRotation)
            {
                // Project player position on to an XZ plane. 
                Vector3 playerForwardDirectionXZ = Vector3.ProjectOnPlane(_player.forward, Vector3.up);
                Quaternion rotation = Quaternion.LookRotation(playerForwardDirectionXZ);
                
                // Flip Y axis. 
                rotation.eulerAngles = new Vector3(rotation.eulerAngles.x, -rotation.eulerAngles.y, rotation.eulerAngles.z);

                // Rotate in 3D space. 
                Vector3 rotatedIconLocation = rotation * new Vector3(location.x, 0, location.y);

                // Convert from 3D to 2D (XZ)
                location = new Vector2(rotatedIconLocation.x, rotatedIconLocation.z);
            }

            // Keep within max radius. 
            // If falloff is zero, this line can be commented/disabled.
            location = Vector2.ClampMagnitude(location, _radarSize);

            return location;
        }

        public static void Add(int instanceID, RadarComponent radarComponent)
        {
            __trackers.Add(instanceID, radarComponent);
        }

        public static void Remove(int instanceID)
        {
            __trackers.Remove(instanceID);
        }
    }
}
using UnityEngine;

namespace Rovsau.Unity.ComponentRadar
{
    // Execution order:  Must run after ComponentRadar. 
    [DefaultExecutionOrder(1)]
    [DisallowMultipleComponent]
    public class RadarComponent : MonoBehaviour
    {
        public int InstanceID { get; private set; }
        public Transform Target { get; private set; }
        public RectTransform RadarIcon { get; private set; }

        private void Awake()
        {
            InstanceID = GetInstanceID();
            Target = transform;
            RadarIcon = Instantiate(ComponentRadar.__RadarTargetIcon, ComponentRadar.__RadarIconContainer).GetComponent<RectTransform>();
        }

        private void OnEnable()
        {
            ComponentRadar.Add(InstanceID, this);
        }

        private void OnDisable()
        {
            ComponentRadar.Remove(InstanceID);
        }

        private void OnDestroy()
        {
            if (RadarIcon) Destroy(RadarIcon.gameObject);
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment