Advanced Player Name Tag for Third Person Characters

Hi Guys,

Just wanted to share a more advanced solution that i added into my Virtuoso Life Reality game recently for displaying name tags over third person players.

The problem is not so trivial and usually requires some additional features to make it useful for proper game play like :

  1. Hiding name tags when the camera no longer views the player.
  2. Changing the color, font size of the name tag
  3. Keeping the name tag as part of the character prefab for better design
  4. Ensuring that the name tag does not flicker or jerk while the avatar is moving, jumping.
  5. Handling characters which change their capsule collider heights during playing different animations such as crouching.
  6. Automatic Offset Y handling of the name tag for different scene ratios and distances
  7. A simple approach to synchronizing name tags across a UNET network.

The code below accomplishes all this. Simply add it to your player prefab, assign the GUI Default Skin.

using UnityEngine;
using UnityEngine.Networking;

namespace VGS.VLR.Core
{
    public class VGS_VLRPlayerTag : NetworkBehaviour
    {
        [Header("Visual Settings")]
        public GUISkin guiSkin;
        public Color color = Color.white;
        public int fontSize = 14;

        [Header("Automatic Offset Y")]
        public float maxCameraDistance = 8f;                // Maximum camera distance
        public float minCamDistanceOffsetY = 20f;           // Minimum Offset Y applied when camera distance at minimum from player
        public float maxCamDistanceOffsetY = -30f;          // Maximum Offset Y applied when camera distance at maximum from player
        private float offsetX = 0;
        private float offsetY = 0;

        private float boxW = 15f;
        private Vector2 boxPosition;

        private CapsuleCollider _playerCollider = null;

        // On the server/host set the player Name
        [SyncVar(hook = "OnPlayerName")]
        public string playerName;

        void Start()
        {
            _playerCollider = GetComponent<CapsuleCollider>();
        }

        /// <summary>
        /// Sync handler for setting player name
        /// </summary>
        /// <param name="name"></param>
        void OnPlayerName(string name)
        {
            playerName = name;
        }

        /// <summary>
        /// Set the player name
        /// </summary>
        /// <param name="name"></param>
        public void SetPlayerName(string name)
        {
            playerName = name;
        }

        /// <summary>
        /// Main name tag rendering loop
        /// </summary>
        void OnGUI()
        {
            // Calculate the name tag position based on the height of the players capsule collider
            Vector3 nameTagPosition = new Vector3(transform.position.x, transform.position.y + _playerCollider.height * 1.1f, transform.position.z);

            // Calculate the world to viewport point in relation to the camera
            Vector3 vpPos = Camera.main.WorldToViewportPoint(nameTagPosition);

            // Only render when the camera see's the player on the view frustrum
            if (vpPos.x > 0 && vpPos.x < 1 && vpPos.y > 0 && vpPos.y < 1 && vpPos.z > 0)
            {
                // Calculate name tag box position from world to screen coordinates
                boxPosition = Camera.main.WorldToScreenPoint(nameTagPosition);
                boxPosition.y = Screen.height - boxPosition.y;
                boxPosition.x -= boxW;

                // Dyanmic name tag size calculation
                GUI.skin = guiSkin;
                Vector2 content = guiSkin.box.CalcSize(new GUIContent(playerName));
                guiSkin.box.fontSize = fontSize;
                GUI.contentColor = color;

                // Automatic Offset Y Calculation
                float camDist = Vector3.Distance(Camera.main.transform.position, nameTagPosition);
                offsetY = ScaleValueFloat(camDist, 0f, maxCameraDistance, minCamDistanceOffsetY, maxCamDistanceOffsetY);

                // Center the position of the name tag
                Rect rectPos = new Rect(boxPosition.x - content.x / 2 * offsetX, boxPosition.y + offsetY, content.x, content.y);

                // Display the name tag
                GUI.Box(rectPos, playerName);
            }
        }

        /// <summary>
        /// Scales one range of values to another range of values.
        /// This function can handle inverted ScaleMin and ScaleMax as well.
        /// </summary>
        /// <param name="fValue"></param>
        /// <param name="fInputMin"></param>
        /// <param name="fInputMax"></param>
        /// <param name="fScaleMin"></param>
        /// <param name="fScaleMax"></param>
        /// <returns></returns>
        public float ScaleValueFloat(float fValue, float fInputMin, float fInputMax, float fScaleMin, float fScaleMax)
        {
            float fVal = 0;

            //Inputs
            float fInputRange = fInputMax - fInputMin;

            //Scale
            float fScaleRange = fScaleMax - fScaleMin;

            //Rate Per Scale
            float fRate = fScaleRange / fInputRange;

            //Output
            if (fValue < fInputMin)
            {
                fValue = fInputMin;
            }
            if (fValue > fInputMax)
            {
                fValue = fInputMax;
            }
            float fOut1 = (fValue - fInputMin) * fRate;
            float fOut2 = fOut1 + fScaleMin;
            fVal = fOut2;

            return fVal;
        }
    }
}
2 Likes

Very concise and elegant Mark.

Still works :slight_smile:

I have an update on my original tag which does not create any garbage

Note this version uses group, alias and player name rows

using UnityEngine;

namespace VGS.Network.Client
{
    public class VGS_PlayerTag: MonoBehaviour
    {
        [Header("Main Settings")]
        public Transform target;
        public bool visible = true;

        [Header("Tag Display Properties")]
        public bool groupTagEnable = true;
        [SerializeField]
        private string _groupTagName = string.Empty;
        [SerializeField]
        private Color _groupTagColor = Color.green;
        [SerializeField]
        private int _groupTagSize = 22;

        public bool aliasTagEnable = true;
        [SerializeField]
        private string _aliasTagName = string.Empty;
        [SerializeField]
        private Color _aliasTagColor = Color.yellow;
        [SerializeField]
        private int _aliasTagSize = 30;

        public bool playerTagEnable = true;
        [SerializeField]
        private string _playerTagName = string.Empty;
        [SerializeField]
        private Color _playerTagColor = Color.white;
        [SerializeField]
        private int _playerTagSize = 22;


        [Header("Visual Settings")]
        public GUISkin guiSkin;
        public Vector2 minFontZoomScale = new Vector2(1f, 1.5f);
        public Vector2 maxFontZoomScale = new Vector2(0.5f, 20f);
        public float maxZoomHeightOffset = 1.8f;


        // Privates
        private string _tag = string.Empty;
        private string _tagGroup = string.Empty;
        private string _tagAlias = string.Empty;
        private string _tagPlayer = string.Empty;
        private Vector2 _tagContent = new Vector2();

        private void Start()
        {
            guiSkin.box.richText = true;
          
        }

        public void SetGroupName(string name)
        {
            _groupTagName = name;
            _tagGroup = string.Format("<color=#{0}><size={1}>{2}</size></color>", ColorUtility.ToHtmlStringRGB(_groupTagColor), _groupTagSize, _groupTagName);
            BuildTag();
        }

        public void SetAliasName(string name)
        {
            _aliasTagName = name;
            _tagAlias =string.Format("<color=#{0}><size={1}><b>{2}</b></size></color>", ColorUtility.ToHtmlStringRGB(_aliasTagColor), _aliasTagSize, _aliasTagName);
            BuildTag();
        }

        public void SetPlayerName(string name)
        {
            _playerTagName = name;
            _tagPlayer = string.Format("<color=#{0}><size={1}><i>{2}</i></size></color>", ColorUtility.ToHtmlStringRGB(_playerTagColor), _playerTagSize, _playerTagName);
            BuildTag();
        }

        public void BuildTag()
        {
            _tag = string.Empty;

            if (groupTagEnable)
                _tag += _tagGroup;

            if (aliasTagEnable)
                _tag += _tag.ToString() == string.Empty ? _tagAlias : "\n" + _tagAlias;

            if (playerTagEnable)
                _tag += _tag.ToString() == string.Empty ? _tagPlayer : "\n" + _tagPlayer;

            if (_tag.ToString() != string.Empty)
            {
                _tagContent = guiSkin.box.CalcSize(new GUIContent(_tag.ToString()));
            }
        }

        /// <summary>
        /// Main name tag rendering loop
        /// Zero garbage collection
        /// </summary>
        void OnGUI()
        {
            // Tag not visible simply return
            if (!visible || _tag.Length == 0)
                return;

            // Calculate the world to viewport point in relation to the camera
            Vector3 vpPos = Camera.main.WorldToViewportPoint(target.position);

            // Only render when the camera see's the player on the view frustrum
            if (vpPos.x > 0 && vpPos.x < 1 && vpPos.y > 0 && vpPos.y < 1 && vpPos.z > 0)
            {
                Vector3 boxPosition = Camera.main.WorldToScreenPoint(new Vector3(target.position.x, target.position.y + ScaleValueFloat(Vector3.Distance(Camera.main.transform.position, target.position), minFontZoomScale.y, maxFontZoomScale.y, 0, maxZoomHeightOffset), target.position.z));
                GUI.skin = guiSkin;
                GUI.Box(new Rect(boxPosition.x - _tagContent.x / 2, Screen.height - boxPosition.y, _tagContent.x, _tagContent.y), _tag.ToString());
            }
        }

        /// <summary>
        /// Scales one range of values to another range of values.
        /// This function can handle inverted ScaleMin and ScaleMax as well.
        /// </summary>
        /// <param name="fValue"></param>
        /// <param name="fInputMin"></param>
        /// <param name="fInputMax"></param>
        /// <param name="fScaleMin"></param>
        /// <param name="fScaleMax"></param>
        /// <returns></returns>
        public float ScaleValueFloat(float fValue, float fInputMin, float fInputMax, float fScaleMin, float fScaleMax)
        {
            float fVal = 0;

            //Inputs
            float fInputRange = fInputMax - fInputMin;

            //Scale
            float fScaleRange = fScaleMax - fScaleMin;

            //Rate Per Scale
            float fRate = fScaleRange / fInputRange;

            //Output
            if (fValue < fInputMin)
            {
                fValue = fInputMin;
            }
            if (fValue > fInputMax)
            {
                fValue = fInputMax;
            }
            float fOut1 = (fValue - fInputMin) * fRate;
            float fOut2 = fOut1 + fScaleMin;
            fVal = fOut2;

            return fVal;
        }
    }
}