Search Unity

Advanced Player Name Tag for Third Person Characters

Discussion in 'Scripting' started by GXMark, Jun 28, 2017.

  1. GXMark

    GXMark

    Joined:
    Oct 13, 2012
    Posts:
    514
    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.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3.  
    4. namespace VGS.VLR.Core
    5. {
    6.     public class VGS_VLRPlayerTag : NetworkBehaviour
    7.     {
    8.         [Header("Visual Settings")]
    9.         public GUISkin guiSkin;
    10.         public Color color = Color.white;
    11.         public int fontSize = 14;
    12.  
    13.         [Header("Automatic Offset Y")]
    14.         public float maxCameraDistance = 8f;                // Maximum camera distance
    15.         public float minCamDistanceOffsetY = 20f;           // Minimum Offset Y applied when camera distance at minimum from player
    16.         public float maxCamDistanceOffsetY = -30f;          // Maximum Offset Y applied when camera distance at maximum from player
    17.         private float offsetX = 0;
    18.         private float offsetY = 0;
    19.  
    20.         private float boxW = 15f;
    21.         private Vector2 boxPosition;
    22.  
    23.         private CapsuleCollider _playerCollider = null;
    24.  
    25.         // On the server/host set the player Name
    26.         [SyncVar(hook = "OnPlayerName")]
    27.         public string playerName;
    28.  
    29.         void Start()
    30.         {
    31.             _playerCollider = GetComponent<CapsuleCollider>();
    32.         }
    33.  
    34.         /// <summary>
    35.         /// Sync handler for setting player name
    36.         /// </summary>
    37.         /// <param name="name"></param>
    38.         void OnPlayerName(string name)
    39.         {
    40.             playerName = name;
    41.         }
    42.  
    43.         /// <summary>
    44.         /// Set the player name
    45.         /// </summary>
    46.         /// <param name="name"></param>
    47.         public void SetPlayerName(string name)
    48.         {
    49.             playerName = name;
    50.         }
    51.  
    52.         /// <summary>
    53.         /// Main name tag rendering loop
    54.         /// </summary>
    55.         void OnGUI()
    56.         {
    57.             // Calculate the name tag position based on the height of the players capsule collider
    58.             Vector3 nameTagPosition = new Vector3(transform.position.x, transform.position.y + _playerCollider.height * 1.1f, transform.position.z);
    59.  
    60.             // Calculate the world to viewport point in relation to the camera
    61.             Vector3 vpPos = Camera.main.WorldToViewportPoint(nameTagPosition);
    62.  
    63.             // Only render when the camera see's the player on the view frustrum
    64.             if (vpPos.x > 0 && vpPos.x < 1 && vpPos.y > 0 && vpPos.y < 1 && vpPos.z > 0)
    65.             {
    66.                 // Calculate name tag box position from world to screen coordinates
    67.                 boxPosition = Camera.main.WorldToScreenPoint(nameTagPosition);
    68.                 boxPosition.y = Screen.height - boxPosition.y;
    69.                 boxPosition.x -= boxW;
    70.  
    71.                 // Dyanmic name tag size calculation
    72.                 GUI.skin = guiSkin;
    73.                 Vector2 content = guiSkin.box.CalcSize(new GUIContent(playerName));
    74.                 guiSkin.box.fontSize = fontSize;
    75.                 GUI.contentColor = color;
    76.  
    77.                 // Automatic Offset Y Calculation
    78.                 float camDist = Vector3.Distance(Camera.main.transform.position, nameTagPosition);
    79.                 offsetY = ScaleValueFloat(camDist, 0f, maxCameraDistance, minCamDistanceOffsetY, maxCamDistanceOffsetY);
    80.  
    81.                 // Center the position of the name tag
    82.                 Rect rectPos = new Rect(boxPosition.x - content.x / 2 * offsetX, boxPosition.y + offsetY, content.x, content.y);
    83.  
    84.                 // Display the name tag
    85.                 GUI.Box(rectPos, playerName);
    86.             }
    87.         }
    88.  
    89.         /// <summary>
    90.         /// Scales one range of values to another range of values.
    91.         /// This function can handle inverted ScaleMin and ScaleMax as well.
    92.         /// </summary>
    93.         /// <param name="fValue"></param>
    94.         /// <param name="fInputMin"></param>
    95.         /// <param name="fInputMax"></param>
    96.         /// <param name="fScaleMin"></param>
    97.         /// <param name="fScaleMax"></param>
    98.         /// <returns></returns>
    99.         public float ScaleValueFloat(float fValue, float fInputMin, float fInputMax, float fScaleMin, float fScaleMax)
    100.         {
    101.             float fVal = 0;
    102.  
    103.             //Inputs
    104.             float fInputRange = fInputMax - fInputMin;
    105.  
    106.             //Scale
    107.             float fScaleRange = fScaleMax - fScaleMin;
    108.  
    109.             //Rate Per Scale
    110.             float fRate = fScaleRange / fInputRange;
    111.  
    112.             //Output
    113.             if (fValue < fInputMin)
    114.             {
    115.                 fValue = fInputMin;
    116.             }
    117.             if (fValue > fInputMax)
    118.             {
    119.                 fValue = fInputMax;
    120.             }
    121.             float fOut1 = (fValue - fInputMin) * fRate;
    122.             float fOut2 = fOut1 + fScaleMin;
    123.             fVal = fOut2;
    124.  
    125.             return fVal;
    126.         }
    127.     }
    128. }
     
    Zenithin and Laurence_B like this.
  2. RegBateman

    RegBateman

    Joined:
    Jun 28, 2017
    Posts:
    1
    Very concise and elegant Mark.
     
  3. Laurence_B

    Laurence_B

    Joined:
    Apr 10, 2017
    Posts:
    2
    Still works :)
     
  4. GXMark

    GXMark

    Joined:
    Oct 13, 2012
    Posts:
    514
    I have an update on my original tag which does not create any garbage

    Note this version uses group, alias and player name rows


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace VGS.Network.Client
    4. {
    5.     public class VGS_PlayerTag: MonoBehaviour
    6.     {
    7.         [Header("Main Settings")]
    8.         public Transform target;
    9.         public bool visible = true;
    10.  
    11.         [Header("Tag Display Properties")]
    12.         public bool groupTagEnable = true;
    13.         [SerializeField]
    14.         private string _groupTagName = string.Empty;
    15.         [SerializeField]
    16.         private Color _groupTagColor = Color.green;
    17.         [SerializeField]
    18.         private int _groupTagSize = 22;
    19.  
    20.         public bool aliasTagEnable = true;
    21.         [SerializeField]
    22.         private string _aliasTagName = string.Empty;
    23.         [SerializeField]
    24.         private Color _aliasTagColor = Color.yellow;
    25.         [SerializeField]
    26.         private int _aliasTagSize = 30;
    27.  
    28.         public bool playerTagEnable = true;
    29.         [SerializeField]
    30.         private string _playerTagName = string.Empty;
    31.         [SerializeField]
    32.         private Color _playerTagColor = Color.white;
    33.         [SerializeField]
    34.         private int _playerTagSize = 22;
    35.  
    36.  
    37.         [Header("Visual Settings")]
    38.         public GUISkin guiSkin;
    39.         public Vector2 minFontZoomScale = new Vector2(1f, 1.5f);
    40.         public Vector2 maxFontZoomScale = new Vector2(0.5f, 20f);
    41.         public float maxZoomHeightOffset = 1.8f;
    42.  
    43.  
    44.         // Privates
    45.         private string _tag = string.Empty;
    46.         private string _tagGroup = string.Empty;
    47.         private string _tagAlias = string.Empty;
    48.         private string _tagPlayer = string.Empty;
    49.         private Vector2 _tagContent = new Vector2();
    50.  
    51.         private void Start()
    52.         {
    53.             guiSkin.box.richText = true;
    54.          
    55.         }
    56.  
    57.         public void SetGroupName(string name)
    58.         {
    59.             _groupTagName = name;
    60.             _tagGroup = string.Format("<color=#{0}><size={1}>{2}</size></color>", ColorUtility.ToHtmlStringRGB(_groupTagColor), _groupTagSize, _groupTagName);
    61.             BuildTag();
    62.         }
    63.  
    64.         public void SetAliasName(string name)
    65.         {
    66.             _aliasTagName = name;
    67.             _tagAlias =string.Format("<color=#{0}><size={1}><b>{2}</b></size></color>", ColorUtility.ToHtmlStringRGB(_aliasTagColor), _aliasTagSize, _aliasTagName);
    68.             BuildTag();
    69.         }
    70.  
    71.         public void SetPlayerName(string name)
    72.         {
    73.             _playerTagName = name;
    74.             _tagPlayer = string.Format("<color=#{0}><size={1}><i>{2}</i></size></color>", ColorUtility.ToHtmlStringRGB(_playerTagColor), _playerTagSize, _playerTagName);
    75.             BuildTag();
    76.         }
    77.  
    78.         public void BuildTag()
    79.         {
    80.             _tag = string.Empty;
    81.  
    82.             if (groupTagEnable)
    83.                 _tag += _tagGroup;
    84.  
    85.             if (aliasTagEnable)
    86.                 _tag += _tag.ToString() == string.Empty ? _tagAlias : "\n" + _tagAlias;
    87.  
    88.             if (playerTagEnable)
    89.                 _tag += _tag.ToString() == string.Empty ? _tagPlayer : "\n" + _tagPlayer;
    90.  
    91.             if (_tag.ToString() != string.Empty)
    92.             {
    93.                 _tagContent = guiSkin.box.CalcSize(new GUIContent(_tag.ToString()));
    94.             }
    95.         }
    96.  
    97.         /// <summary>
    98.         /// Main name tag rendering loop
    99.         /// Zero garbage collection
    100.         /// </summary>
    101.         void OnGUI()
    102.         {
    103.             // Tag not visible simply return
    104.             if (!visible || _tag.Length == 0)
    105.                 return;
    106.  
    107.             // Calculate the world to viewport point in relation to the camera
    108.             Vector3 vpPos = Camera.main.WorldToViewportPoint(target.position);
    109.  
    110.             // Only render when the camera see's the player on the view frustrum
    111.             if (vpPos.x > 0 && vpPos.x < 1 && vpPos.y > 0 && vpPos.y < 1 && vpPos.z > 0)
    112.             {
    113.                 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));
    114.                 GUI.skin = guiSkin;
    115.                 GUI.Box(new Rect(boxPosition.x - _tagContent.x / 2, Screen.height - boxPosition.y, _tagContent.x, _tagContent.y), _tag.ToString());
    116.             }
    117.         }
    118.  
    119.         /// <summary>
    120.         /// Scales one range of values to another range of values.
    121.         /// This function can handle inverted ScaleMin and ScaleMax as well.
    122.         /// </summary>
    123.         /// <param name="fValue"></param>
    124.         /// <param name="fInputMin"></param>
    125.         /// <param name="fInputMax"></param>
    126.         /// <param name="fScaleMin"></param>
    127.         /// <param name="fScaleMax"></param>
    128.         /// <returns></returns>
    129.         public float ScaleValueFloat(float fValue, float fInputMin, float fInputMax, float fScaleMin, float fScaleMax)
    130.         {
    131.             float fVal = 0;
    132.  
    133.             //Inputs
    134.             float fInputRange = fInputMax - fInputMin;
    135.  
    136.             //Scale
    137.             float fScaleRange = fScaleMax - fScaleMin;
    138.  
    139.             //Rate Per Scale
    140.             float fRate = fScaleRange / fInputRange;
    141.  
    142.             //Output
    143.             if (fValue < fInputMin)
    144.             {
    145.                 fValue = fInputMin;
    146.             }
    147.             if (fValue > fInputMax)
    148.             {
    149.                 fValue = fInputMax;
    150.             }
    151.             float fOut1 = (fValue - fInputMin) * fRate;
    152.             float fOut2 = fOut1 + fScaleMin;
    153.             fVal = fOut2;
    154.  
    155.             return fVal;
    156.         }
    157.     }
    158. }