Search Unity

Finding what letter/word I clicked

Discussion in 'UGUI & TextMesh Pro' started by The-Oddler, Sep 14, 2014.

  1. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    I'm trying out the new Unity UI and I'm trying to figure out, when clicking text, what letter in that text I clicked. I'm implementing IPointerDownHandler in a script attached to the same object as the text to catch the click on my text. This works, but now I need to figure out what letter was clicked.

    I first figure out the local position of the click, relative to the text. And then I check my chick against the character vertices. This however doesn't work, and I'm not sure what's wrong.

    My code:

    Code (CSharp):
    1.   public void OnPointerDown (PointerEventData eventData) {
    2.      eventData.Use();
    3.      GetWord(eventData.pressEventCamera.ScreenPointToRay(eventData.position));
    4.    }
    5.    
    6.    void GetWord(Ray ray) {
    7.      Ray localRay = new Ray(
    8.        transform.InverseTransformPoint(ray.origin),
    9.        transform.InverseTransformDirection(ray.direction));
    10.      
    11.      Vector3 localClickPos =
    12.        localRay.origin +
    13.        localRay.direction / localRay.direction.z * (transform.localPosition.z - localRay.origin.z);
    14.      
    15.      Debug.DrawRay(transform.TransformPoint(localClickPos), Vector3.up/10, Color.red, 2.0f);
    16.      
    17.      var textGen = _text.cachedTextGenerator;
    18.      for (int i = 0; i < textGen.characterCount; ++i) {
    19.        if (localClickPos.x >= textGen.verts[i].position.x &&
    20.         localClickPos.x <= textGen.verts[i+3].position.x &&
    21.         localClickPos.y >= textGen.verts[i].position.y &&
    22.         localClickPos.y <= textGen.verts[i+3].position.y
    23.         ) {
    24.          Debug.Log(_text.text.Substring(i,10));
    25.       }
    26.      }
    27.    }
    The Debug.Log output I get is wrong. I can't find a patterns in the returned text.


    Any idea what's going wrong? Or perhaps a better way of doing it?
     
  2. Nanity

    Nanity

    Joined:
    Jul 6, 2012
    Posts:
    148
    Better solution would be to add an EventTrigger to all (text) objects that contain a "clickable text" how you call it.

    The OnPointerClick() delegate can then call your function while specifying itself as parameter (drag and drop the gameobject into the parameter field):
    Code (csharp):
    1. public void submitTextFunction (UnityEngine.UI.Text ui_text_element)
    2. {
    3.   Debug.Log(ui_text_element.text);
    4. }
    Edit: Missread your question, maybe you want to split up your text into seperate text objects.
     
  3. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    I want to avoid this since there is a lot of text and every single word has to be clickable. I thought about splitting up the text in code, but then I still need to be able to get the position of the letter properly.

    Thanks for the help anyway.
     
  4. Breyer

    Breyer

    Joined:
    Nov 10, 2012
    Posts:
    412
  5. Nanity

    Nanity

    Joined:
    Jul 6, 2012
    Posts:
    148
    Do you use world space or ui space for the text panel?

    If you use UI space, why you convert eventData.position to a ray and then calculate the localClickPos again?

    Shouldn't localClickPos equal eventData.position since you dont use z at all?
     
  6. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    Oh, the canvas with the text is placed in the world. So I use worldSpace. The eventData.position gives me the position of the cursor on screen, so I have to convert that to local space for the text.
     
  7. Nanity

    Nanity

    Joined:
    Jul 6, 2012
    Posts:
    148
    Aaaah ok, though I never believed there could be a 3D interative text wall game. Sounds pretty unique and innovative

    But vertices means I'm out. Never got a foot in the mesh door, sorry. I had an idea about getting the word by the text cursor, but it seems there's little to no documentation about it. This is the best result:

    http://docs.unity3d.com/ScriptReference/UICharInfo-cursorPos.html
     
  8. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    Hehe, I'm just trying to build a prototype at the moment, to see if it works, but this problem is really bogging my results down. Though the cursosPos indeed gets me the closest. It gives it local left position, and the width. Though the upper pos and height are still illusive.

    Any ideas for that, or perhaps someone else got any idea?

    Thanks!

    Edit: I just found UILineInfo which seems interesting, playing around with that now.
    Edit2: The UILineInfo got me the height. But now I still need to find the top.
    What I currently have, I'm drawing gizmos to see if I got the position/size of the letters right.


    The red is what I get when using cursosPos and UILineInfo. I tried moving it down with half a UILineInfo.height (or 0.4/0.6 * height) but that doesn't work for all texture sizes.
    The white (only on the first few letters) is what I get when I draw the vertices.

    The code:

    Code (CSharp):
    1. void OnDrawGizmos() {
    2.         var text = GetComponent<Text>();
    3.         var textGen = text.cachedTextGenerator;
    4.         for (int i = 0; i < textGen.characterCount; ++i) {
    5.             Vector2 locUpperLeft = new Vector2(textGen.verts[i].position.x, textGen.verts[i].position.y);
    6.             Vector2 locBottomRight = new Vector2(textGen.verts[i+1].position.x, textGen.verts[i+1].position.y);
    7.  
    8.             Vector3 worldUpperLeft = transform.TransformPoint(locUpperLeft);
    9.             Vector3 worldBottomRight = transform.TransformPoint(locBottomRight);
    10.  
    11.             Vector3 mid = (worldUpperLeft + worldBottomRight) / 2.0f;
    12.             Vector3 size = worldBottomRight - worldUpperLeft;
    13.            
    14.             Gizmos.DrawWireCube(mid, size);
    15.         }
    16.        
    17.         // newest try, with the cursorPos
    18.         Gizmos.color = Color.red;
    19.         var chars = text.cachedTextGenerator.characters;
    20.         var lines = text.cachedTextGenerator.lines;
    21.         for (int i = 0; i < textGen.characterCount; ++i)
    22.         {
    23.             var line = lines.First(lineInfo => i >= lineInfo.startCharIdx);
    24.  
    25.             Vector2 locUpperLeft = chars[i].cursorPos - new Vector2(0, 0); ;
    26.             Vector2 locBottomRight = new Vector2(locUpperLeft.x + chars[i].charWidth, locUpperLeft.y + line.height);
    27.  
    28.             Vector3 worldUpperLeft = transform.TransformPoint(locUpperLeft);
    29.             Vector3 worldBottomRight = transform.TransformPoint(locBottomRight);
    30.  
    31.             Vector3 mid = (worldUpperLeft + worldBottomRight) / 2.0f;
    32.             Vector3 size = worldBottomRight - worldUpperLeft;
    33.  
    34.             Gizmos.DrawWireCube(mid, size);
    35.         }
    36.     }
     
    Last edited: Sep 16, 2014
  9. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    Ok, I'm so f-ing stupid. I just found the problem and I can now draw proper boxes around each letter.
    The problem was that I didn't multiply my index with 4 to get the proper vertex.

    The fixed code:
    Code (CSharp):
    1.     void OnDrawGizmos() {
    2.         var text = GetComponent<Text>();
    3.         var textGen = text.cachedTextGenerator;
    4.         var prevMatrix = Gizmos.matrix;
    5.         Gizmos.matrix = transform.localToWorldMatrix;
    6.         for (int i = 0; i < textGen.characterCount; ++i) {
    7.             Vector2 locUpperLeft = new Vector2(textGen.verts[i * 4].position.x, textGen.verts[i * 4].position.y);
    8.             Vector2 locBottomRight = new Vector2(textGen.verts[i * 4 + 2].position.x, textGen.verts[i * 4 + 2].position.y);
    9.  
    10.             Vector3 mid = (locUpperLeft + locBottomRight) / 2.0f;
    11.             Vector3 size = locBottomRight - locUpperLeft;
    12.            
    13.             Gizmos.DrawWireCube(mid, size);
    14.         }
    15.         Gizmos.matrix = prevMatrix;
    16.     }
    as you can see, I now use "i * 4" instead of just "i". This was such a stupid bug I just didn't see it...

    Thanks for all the help!
     
  10. LessThanEpic

    LessThanEpic

    Joined:
    Aug 22, 2014
    Posts:
    13
    In case anyone else finds it helpful I put together The Oddler's solution (with a few minor refactors) and figured I'd post the full MonoBehaviour here.
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.EventSystems;
    3. using UnityEngine.UI;
    4.  
    5. public class ClickableText : MonoBehaviour, IPointerDownHandler
    6. {
    7.     private Text _text;
    8.  
    9.     void Start()
    10.     {
    11.         _text = GetComponent<Text>();
    12.     }
    13.  
    14.     void OnDrawGizmos()
    15.     {
    16.         var text = GetComponent<Text>();
    17.         var textGen = text.cachedTextGenerator;
    18.         var prevMatrix = Gizmos.matrix;
    19.         Gizmos.matrix = transform.localToWorldMatrix;
    20.         for (int i = 0; i < textGen.characterCount; ++i)
    21.         {
    22.             Vector2 locUpperLeft = new Vector2(textGen.verts[i * 4].position.x, textGen.verts[i * 4].position.y);
    23.             Vector2 locBottomRight = new Vector2(textGen.verts[i * 4 + 2].position.x, textGen.verts[i * 4 + 2].position.y);
    24.  
    25.             Vector3 mid = (locUpperLeft + locBottomRight) / 2.0f;
    26.             Vector3 size = locBottomRight - locUpperLeft;
    27.  
    28.             Gizmos.DrawWireCube(mid, size);
    29.         }
    30.         Gizmos.matrix = prevMatrix;
    31.     }
    32.  
    33.  
    34.     public void OnPointerDown(PointerEventData eventData)
    35.     {
    36.         eventData.Use();
    37.         int index = GetIndexOfClick(eventData.pressEventCamera.ScreenPointToRay(eventData.position));
    38.         if(index != -1) Debug.Log(GetWordAtIndex(index));
    39.     }
    40.  
    41.     int GetIndexOfClick(Ray ray)
    42.     {
    43.         Ray localRay = new Ray(
    44.           transform.InverseTransformPoint(ray.origin),
    45.           transform.InverseTransformDirection(ray.direction));
    46.  
    47.         Vector3 localClickPos =
    48.           localRay.origin +
    49.           localRay.direction / localRay.direction.z * (transform.localPosition.z - localRay.origin.z);
    50.  
    51.         Debug.DrawRay(transform.TransformPoint(localClickPos), Vector3.up / 10, Color.red, 2.0f);
    52.  
    53.         var textGen = _text.cachedTextGenerator;
    54.         for (int i = 0; i < textGen.characterCount; ++i)
    55.         {
    56.             Vector2 locUpperLeft = new Vector2(textGen.verts[i * 4].position.x, textGen.verts[i * 4].position.y);
    57.             Vector2 locBottomRight = new Vector2(textGen.verts[i * 4 + 2].position.x, textGen.verts[i * 4 + 2].position.y);
    58.  
    59.             if (localClickPos.x >= locUpperLeft.x &&
    60.              localClickPos.x <= locBottomRight.x &&
    61.              localClickPos.y <= locUpperLeft.y &&
    62.              localClickPos.y >= locBottomRight.y
    63.              )
    64.             {
    65.                 return i;
    66.             }
    67.         }
    68.  
    69.         return -1;
    70.     }
    71.  
    72.     string GetWordAtIndex(int index)
    73.     {
    74.         int begIndex = -1;
    75.         int marker = index;
    76.         while (begIndex == -1)
    77.         {
    78.             marker--;
    79.             if (marker < 0)
    80.             {
    81.                 begIndex = 0;
    82.             }
    83.             else if (!char.IsLetter(_text.text[marker]))
    84.             {
    85.                 begIndex = marker;
    86.             }
    87.         }
    88.  
    89.         int lastIndex = -1;
    90.         marker = index;
    91.         while (lastIndex == -1)
    92.         {
    93.             marker++;
    94.             if (marker > _text.text.Length - 1)
    95.             {
    96.                 lastIndex = _text.text.Length - 1;
    97.             }
    98.             else if (!char.IsLetter(_text.text[marker]))
    99.             {
    100.                 lastIndex = marker;
    101.             }
    102.         }
    103.  
    104.         return _text.text.Substring(begIndex, lastIndex - begIndex);
    105.     }
    106.  
    107. }
    108.  
     
    Carychao, sexual_loogie and eses like this.
  11. sexual_loogie

    sexual_loogie

    Joined:
    Jul 28, 2015
    Posts:
    2
    Thanks SO much guys, solves a lot of problems for me, that would have been quite the nightmare to try to figure out. kudos @The-Oddler and @LessThanEpic
     
    kor_Leone likes this.
  12. Dibbie

    Dibbie

    Joined:
    Sep 2, 2014
    Posts:
    42
    Late to the party - but this REALLY helped me out in a project, using The-Oddler's code, I was able to do links and email parsing in a UI Text - I also used Text Mesh Pro (https://www.assetstore.unity3d.com/en/#!/content/84126) as an alternative which is a little more complex to understand, but once you do, it becomes easy - I created an example with both, so anyone else in the future that may be looking for this alternative, here is the UnityPackage for anyone to check out =) - its not 100% perfect, and can certainly be modified better, and is commented well enough that it should be easy for any intermediate programmer to understand and modify it as they need to for their given project.

    Awesome work Oddler and thank you for sharing this great solution!
     
    Last edited: Jan 31, 2019
  13. The-Oddler

    The-Oddler

    Joined:
    Nov 26, 2010
    Posts:
    133
    Haha, happy to help :D
     
  14. mazumdarshouvik

    mazumdarshouvik

    Joined:
    Jan 31, 2019
    Posts:
    2

    Hi I am looking for the solution that uses Text Mesh Pro. Can you please share the code. This is bit urgent :)
     
  15. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    See example 12a included in the TMP Examples & Extras.
     
  16. samuelykc

    samuelykc

    Joined:
    Sep 6, 2017
    Posts:
    1
    Should the code on line 96 reads as the follow?
    1. lastIndex = _text.text.Length;
    Since it seems to me that "lastIndex" would refer to a char to be truncated away, if the index of the last char (Length - 1) is used when reaching the end of the text, we would actually dropped the last char.


    Anyway, the script is extremely helpful! Thanks!