Friday, June 15, 2018

How to create Linked Text in Unity with Textmesh Pro

What I was trying to achieve was a block of UI text with links that I could click with the mouse to cause something to happen. In Unity this doesn't happen out of the box. However there is a package called TextMesh Pro which with a bit of effort achieved what I was after.

Here is the link: https://assetstore.unity.com/packages/essentials/beta-projects/textmesh-pro-84126

Unfortunately the documentation is a bit sketchy, some out-of-date, and difficult to find for the newer version.

So to get it to work...
  1. Update to Unity 5.6 or later.
  2. Download and install the package.
  3. From GameObject > UI add a TextMeshPro-Text to the canvas.
  4. Make sure the scene has an Event System object.
  5. Add a script to TextMesh Pro Text object based on TMP_Text_Selector_B.cs 
  6. Links in the text need to be tagged like: Some text <link="link_name">this text has a link</link> and this is boring text.
  7. In TMP_TextInfoDebugTool.cs comment out the contents of method OnDrawGizmos() as this causes errors in the editor.
  8. There is a sample scene called: 12 - Link Example with links that work.
  9. If using the color tag (which works a little different to to the Text color tag) make sure that the base text color is white. Color tags are in the form: <#40A0FF>This is a color tag - just specify the color in the tag.</color>
  10. While I could get the mouseovers to work using the new TMP Text Event Handler script I couldn't get it to register mouse clicks so I used the old TMP_Text_Selector_B version instead.
This is my Selector script based on TMP_Text_Selector_B
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using TMPro;

// Disabled warning due to SetVertices being deprecated 
// until new release with SetMesh() is available.
#pragma warning disable 0618

public class TextOutputTextSelector : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler, IPointerUpHandler
{
 public RectTransform TextPopup_Prefab_01;

 private RectTransform m_TextPopup_RectTransform;
 private TextMeshProUGUI m_TextPopup_TMPComponent;

 private const string k_LinkText = "You have selected link <#ffff00>";
 private const string k_WordText = "Word Index: <#ffff00>";


 private TextMeshProUGUI m_TextMeshPro;
 private Canvas m_Canvas;
 private Camera m_Camera;

 // Flags
 private bool isHoveringObject;
 private int m_selectedLink = -1;
 private int m_selectedWord = -1;
 private int m_lastIndex = -1;

 private Matrix4x4 m_matrix;

 private TMP_MeshInfo[] m_cachedMeshInfoVertexData;

 // ----------------------------------------------------------------------------
 public void Awake()
 {
  m_TextMeshPro = gameObject.GetComponent<TextMeshProUGUI>();
  m_Canvas = gameObject.GetComponentInParent<Canvas>();

  // Get a reference to the camera if Canvas Render Mode is not ScreenSpace Overlay.
  if (m_Canvas.renderMode == RenderMode.ScreenSpaceOverlay)
   m_Camera = null;
  else
   m_Camera = m_Canvas.worldCamera; 
 }

 // ----------------------------------------------------------------------------
 public void OnEnable()
 {

  // Subscribe to event fired when text object has been regenerated.
  TMPro_EventManager.TEXT_CHANGED_EVENT.Add(ON_TEXT_CHANGED);
 }
  
 // ----------------------------------------------------------------------------
 public void OnDisable()
 {
  // UnSubscribe to event fired when text object has been regenerated.
  TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(ON_TEXT_CHANGED);
 }

 // ----------------------------------------------------------------------------
 public void ON_TEXT_CHANGED(Object obj)
 {

  if (obj == m_TextMeshPro)
  {
   // Update cached vertex data.
   m_cachedMeshInfoVertexData = m_TextMeshPro.textInfo.CopyMeshInfoVertexData();
  }
 }

 // ----------------------------------------------------------------------------
 public void LateUpdate()
 {

  if (isHoveringObject)
  {
   // LINKS 
   // Check if mouse intersects with any links.
   int linkIndex = TMP_TextUtilities.FindIntersectingLink(m_TextMeshPro, Input.mousePosition, m_Camera);

   // Clear previous link selection if one existed.
   if ((linkIndex == -1 && m_selectedLink != -1) || linkIndex != m_selectedLink)
   {
    m_selectedLink = -1;
   }

   // Handle new Link selection.
   if (linkIndex != -1 && linkIndex != m_selectedLink)
   {
    m_selectedLink = linkIndex;
    TMP_LinkInfo linkInfo = m_TextMeshPro.textInfo.linkInfo[linkIndex];

    Debug.Log("(Late Update) Selected Link Index: " + m_selectedLink);
    Debug.Log("*** (Late Update) Link ID: \"" + linkInfo.GetLinkID() + "\"   Link Text: \"" + linkInfo.GetLinkText() + "\""); 

    Vector3 worldPointInRectangle = Vector3.zero;
    RectTransformUtility.ScreenPointToWorldPointInRectangle(m_TextMeshPro.rectTransform, Input.mousePosition, m_Camera, out worldPointInRectangle);

    // change color of link (Doesn't quite work but leaving here for future use)
    // Iterate through each of the characters of the word.
    /*
    Debug.Log("First Character Index: " + linkInfo.linkTextfirstCharacterIndex);
    Debug.Log("Link Text Length: " + linkInfo.linkTextLength);

    for (int i = 0; i < linkInfo.linkTextLength; i++)
    {
     int characterIndex = linkInfo.linkTextfirstCharacterIndex + i;

     // Get the index of the material / sub text object used by this character.
     int meshIndex = m_TextMeshPro.textInfo.characterInfo[characterIndex].materialReferenceIndex;

     int vertexIndex = m_TextMeshPro.textInfo.characterInfo[characterIndex].vertexIndex;

     // Get a reference to the vertex color
     Color32[] vertexColors = m_TextMeshPro.textInfo.meshInfo[meshIndex].colors32;

     Color32 c = vertexColors[vertexIndex + 0].Tint(0.75f);

     vertexColors[vertexIndex + 0] = c;
     vertexColors[vertexIndex + 1] = c;
     vertexColors[vertexIndex + 2] = c;
     vertexColors[vertexIndex + 3] = c;
    }

    // Update Geometry
    m_TextMeshPro.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
    */
   }

  }
  else
  {
   // Restore any character that may have been modified
   if (m_lastIndex != -1)
   {
    RestoreCachedVertexAttributes(m_lastIndex);
    m_lastIndex = -1;
   }
  }
 }

 // ----------------------------------------------------------------------------
 public void OnPointerEnter(PointerEventData eventData)
 {
  isHoveringObject = true;
 }

 // ----------------------------------------------------------------------------
 public void OnPointerExit(PointerEventData eventData)
 {
  //Debug.Log("OnPointerExit()");
  isHoveringObject = false;
 }

 // ----------------------------------------------------------------------------
 public void OnPointerClick(PointerEventData eventData)
 {
        // Check if Mouse intersects any words and if so assign a random color to that word.
        int linkIndex = TMP_TextUtilities.FindIntersectingLink(m_TextMeshPro, Input.mousePosition, m_Camera);
  if (linkIndex != -1)
  {
   //TMP_LinkInfo linkInfo = m_TextMeshPro.textInfo.linkInfo[linkIndex];
   Debug.Log("(On Pointer Click) - Selected Link ID: " + m_selectedLink);
   Debug.Log("Mouse position - X: " + Input.mousePosition.x + ", Y: " + Input.mousePosition.y);
  }
 }

 // ----------------------------------------------------------------------------
 public void OnPointerUp(PointerEventData eventData)
 {
  //Debug.Log("OnPointerUp()");
 }

 // ----------------------------------------------------------------------------
 void RestoreCachedVertexAttributes(int index)
 {
  if (index == -1 || index > m_TextMeshPro.textInfo.characterCount - 1) return;

  // Get the index of the material / sub text object used by this character.
  int materialIndex = m_TextMeshPro.textInfo.characterInfo[index].materialReferenceIndex;

  // Get the index of the first vertex of the selected character.
  int vertexIndex = m_TextMeshPro.textInfo.characterInfo[index].vertexIndex;

  // Restore Vertices
  // Get a reference to the cached / original vertices.
  Vector3[] src_vertices = m_cachedMeshInfoVertexData[materialIndex].vertices;

  // Get a reference to the vertices that we need to replace.
  Vector3[] dst_vertices = m_TextMeshPro.textInfo.meshInfo[materialIndex].vertices;

  // Restore / Copy vertices from source to destination
  dst_vertices[vertexIndex + 0] = src_vertices[vertexIndex + 0];
  dst_vertices[vertexIndex + 1] = src_vertices[vertexIndex + 1];
  dst_vertices[vertexIndex + 2] = src_vertices[vertexIndex + 2];
  dst_vertices[vertexIndex + 3] = src_vertices[vertexIndex + 3];

  // Restore Vertex Colors
  // Get a reference to the vertex colors we need to replace.
  Color32[] dst_colors = m_TextMeshPro.textInfo.meshInfo[materialIndex].colors32;

  // Get a reference to the cached / original vertex colors.
  Color32[] src_colors = m_cachedMeshInfoVertexData[materialIndex].colors32;

  // Copy the vertex colors from source to destination.
  dst_colors[vertexIndex + 0] = src_colors[vertexIndex + 0];
  dst_colors[vertexIndex + 1] = src_colors[vertexIndex + 1];
  dst_colors[vertexIndex + 2] = src_colors[vertexIndex + 2];
  dst_colors[vertexIndex + 3] = src_colors[vertexIndex + 3];

  // Restore UV0S
  // UVS0
  Vector2[] src_uv0s = m_cachedMeshInfoVertexData[materialIndex].uvs0;
  Vector2[] dst_uv0s = m_TextMeshPro.textInfo.meshInfo[materialIndex].uvs0;
  dst_uv0s[vertexIndex + 0] = src_uv0s[vertexIndex + 0];
  dst_uv0s[vertexIndex + 1] = src_uv0s[vertexIndex + 1];
  dst_uv0s[vertexIndex + 2] = src_uv0s[vertexIndex + 2];
  dst_uv0s[vertexIndex + 3] = src_uv0s[vertexIndex + 3];

  // UVS2
  Vector2[] src_uv2s = m_cachedMeshInfoVertexData[materialIndex].uvs2;
  Vector2[] dst_uv2s = m_TextMeshPro.textInfo.meshInfo[materialIndex].uvs2;
  dst_uv2s[vertexIndex + 0] = src_uv2s[vertexIndex + 0];
  dst_uv2s[vertexIndex + 1] = src_uv2s[vertexIndex + 1];
  dst_uv2s[vertexIndex + 2] = src_uv2s[vertexIndex + 2];
  dst_uv2s[vertexIndex + 3] = src_uv2s[vertexIndex + 3];


  // Restore last vertex attribute as we swapped it as well
  int lastIndex = (src_vertices.Length / 4 - 1) * 4;

  // Vertices
  dst_vertices[lastIndex + 0] = src_vertices[lastIndex + 0];
  dst_vertices[lastIndex + 1] = src_vertices[lastIndex + 1];
  dst_vertices[lastIndex + 2] = src_vertices[lastIndex + 2];
  dst_vertices[lastIndex + 3] = src_vertices[lastIndex + 3];

  // Vertex Colors
  src_colors = m_cachedMeshInfoVertexData[materialIndex].colors32;
  dst_colors = m_TextMeshPro.textInfo.meshInfo[materialIndex].colors32;
  dst_colors[lastIndex + 0] = src_colors[lastIndex + 0];
  dst_colors[lastIndex + 1] = src_colors[lastIndex + 1];
  dst_colors[lastIndex + 2] = src_colors[lastIndex + 2];
  dst_colors[lastIndex + 3] = src_colors[lastIndex + 3];

  // UVS0
  src_uv0s = m_cachedMeshInfoVertexData[materialIndex].uvs0;
  dst_uv0s = m_TextMeshPro.textInfo.meshInfo[materialIndex].uvs0;
  dst_uv0s[lastIndex + 0] = src_uv0s[lastIndex + 0];
  dst_uv0s[lastIndex + 1] = src_uv0s[lastIndex + 1];
  dst_uv0s[lastIndex + 2] = src_uv0s[lastIndex + 2];
  dst_uv0s[lastIndex + 3] = src_uv0s[lastIndex + 3];

  // UVS2
  src_uv2s = m_cachedMeshInfoVertexData[materialIndex].uvs2;
  dst_uv2s = m_TextMeshPro.textInfo.meshInfo[materialIndex].uvs2;
  dst_uv2s[lastIndex + 0] = src_uv2s[lastIndex + 0];
  dst_uv2s[lastIndex + 1] = src_uv2s[lastIndex + 1];
  dst_uv2s[lastIndex + 2] = src_uv2s[lastIndex + 2];
  dst_uv2s[lastIndex + 3] = src_uv2s[lastIndex + 3];

  // Need to update the appropriate 
  m_TextMeshPro.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
 }
}

No comments:

Post a Comment