Clicking on individual words in text area

I am looking to make each word in a text box clickable, similar to what children’s e-books do. Any way of doing this without having to make individual text objects?

An example of what i want to do is Dr. Seuss's ABC - YouTube however I have more text and in paragraph form (about 3-4 sentences per screen), so manual placement, although possible is not optimal especially if I have to do contestant text revisions. Wondering if there is a good way to do this.

Update 2/1

So I’ve been playing with this a bit more, but having issues with the positioning of the text. I am pretty certain that I have to do something with bounds, but can’t seem to get the correct combination to make for even spacing between words. Other then that it seems to work. Would like to eventually make this an editor script so it would lay it out on the screen as i type in the inspector.

Prefab is just a GameObject with a TextMesh, MeshRender and Collider (soon to change to Rect)
I also used this reference for unifycommunity.com

private var bodyTextArray = new Array();
var bodyText = "Default Text Goes Here";
var textPrefab : Transform;
var paragraphWidth : int;

function Start () {
bodyTextArray = bodyText.Split(" "[0]);
var lineWidth = 0;
for (var i = 0; i < bodyTextArray.length; i++){
	var go = Instantiate(textPrefab);
	go.GetComponent(TextMesh).text = bodyTextArray*;*
  •   go.transform.parent = transform;*
    
  •   //try and figure out where the next word should be placed, if line width reaches a certain width then I will move it don to the next line to make a paragraph.*
    
  •   lineWidth += go.renderer.bounds.size.x + go.renderer.bounds.extents.x;*
    
  •   //This is not working as expected*
    
  •   go.transform.position.x = lineWidth;*
    
  •   //This is temp while i Debug...*
    
  •   go.transform.position.y = i - .5;*
    
  •   //sync size of collider to Mesh*
    
  •   go.GetComponent(BoxCollider).size = go.renderer.bounds.size;*
    
  •   go.GetComponent(BoxCollider).center.x = go.renderer.bounds.size.x/2;*
    

_ go.GetComponent(BoxCollider).center.y = (go.renderer.bounds.size.y/2) * -1;_

_ Debug.Log(bodyTextArray*);_
_
} _
_
}_
function Update () {
_
if (Input.GetMouseButton(0)) {_
_
var ray: Ray = Camera.main.ScreenPointToRay(Input.mousePosition);_
_
var hit: RaycastHit;_
_
if (Physics.Raycast(ray, hit)) {_
_
if(hit.transform.tag == “Word”){_
_
//Add Audio Call here…_
_
Debug.Log(hit.transform.GetComponent(TextMesh).text);_
_
}_
_
} else {_
_
print(“Hit nothing”);_
_
}_
_
}_
_
}*_

Hmm sounds simple :slight_smile: since there is no 3d stuffs involved, you can go for perspective camera, it works fine (but doesnt matter anyway)

and you have asked for having a single text mesh and use it as different buttons, it is completly possible but you have one blocker. I assume that you want to change the color of the text whn pressed, it is not possible with that set up!! so it would be better to go with different text mesh for different text.

i assume that this app is for iOS device!! if so you can detect clicks using a rectangle rather than a collider (which most ppl go for). This would give you a really good performance boost. Like the below, but this would work only with orthographic camera for now(have to make a lot of changed for perspective)

var txt : TextMesh;
var boundingBox:Rect;
var isButton:boolean=true;

function Start()
{
	
	if(isButton)
	{
		var temp:Vector3=Camera.main.WorldToViewportPoint  (txt.renderer.bounds.min);
		
		boundingBox.x=temp.x;
		boundingBox.y=temp.y;
		boundingBox.width=txt.renderer.bounds.size.x*2/Screen.width;
		boundingBox.height=txt.renderer.bounds.size.y*2/Screen.height;
		
	}
//	print(temp);
}
function Update () {


	
}

function OnGUI()
{
	if(Event.current.type==EventType.MouseDown)
	{
		if(isButton)
		{
			
			var temp:Vector3=Camera.main.ScreenToViewportPoint(Input.mousePosition);
			print(""+temp);
			if(boundingBox.Contains(temp))
			{
				//action for button click...
				print("clicked "+gameObject.name);
			}
		}
	}
}

I hope the code is clear ?!.

I got the code to work. Turned out i didn’t specify the size as a float and it was being turned into a int.

private var bodyTextArray = new Array();
var bodyText : String;
var textPrefab : Transform;
var paragraphWidth : int;
var wordBuffer : float = .2;
var lineSpace : float = .5;
var textPlacer : Transform;

function Start () {
bodyTextArray = bodyText.Split(" "[0]);
var lastGOsize : float = 0.0;
var line : int = 0;

for (var i = 0; i < bodyTextArray.length; i++){
	var go : GameObject = Instantiate(textPrefab, textPlacer.position, Quaternion.identity);
	go.GetComponent(TextMesh).text = bodyTextArray*;*
  •   go.transform.parent = textPlacer.transform;*
    
  •   //This is not working as expected*
    
  •   go.transform.position.x = lastGOsize + wordBuffer;*
    
  •   go.transform.position.y = line;*
    
  •   //sync size of collider to Mesh*
    
  •   go.GetComponent(BoxCollider).size = go.renderer.bounds.size;*
    
  •   go.GetComponent(BoxCollider).center.x = go.renderer.bounds.size.x/2;*
    

_ go.GetComponent(BoxCollider).center.y = (go.renderer.bounds.size.y/2) * -1;_

  •   if(lastGOsize > 10){*
    
  •   	line -= lineSpace;*
    
  •   	lastGOsize = 0.0;*
    
  •   }else{*
    
  •   	lastGOsize = go.renderer.bounds.max.x;*
    
  •   }*
    
  • } *
    }
    function Update () {
  • if (Input.GetMouseButton(0)) {*
  •   var ray: Ray = Camera.main.ScreenPointToRay(Input.mousePosition);*
    
  •   var hit: RaycastHit;*
    
  •   if (Physics.Raycast(ray, hit)) {*
    
  •   	if(hit.transform.tag == "Word"){*
    
  •   		//Add Audio Call here....*
    
  •   		Debug.Log(hit.transform.GetComponent(TextMesh).text);*
    
  •   	}*
    
  •   } else {*
    
  •   	print("Hit nothing");*
    
  •   }*
    
  •  }*
    

}

Sorry i never got back to you with this.
I’ll try and explain my solution below. I ended up creating an editor script to do all the dirty manual work for me, so i can just enter the lines of text i’d like and then click a button and it generates all the text, colliders, spacing and object hierarchy.
I used colliders for my solution, I didn’t think of rects at the time, though im not too sure how that would work when you have text in 3D on angles and such, which my solution needed to do as well.
This is basically the breakdown though, you could change it easily to your own adaption:
A script called CreateTextField has the following:

  • A generic list to keep track of sentence lines.
  • A generic list to keep track of fonts used/needed.

Generic lists although slower than static arrays were chosen since it’s just an editor script and all this stuff will be created before compiling to be ready to use at run time.

An editor script shows a visual input to the user to add in their text and create new lines, choose their font etc.
Once the dialogue has been set up and the “create” button is pressed the following is completed:

  • First i created an empty gameObject to act as the parent and ‘controller’ of the actual dialogue. Just called “DialogueField”.
  • A for loop is run < the number of lines of text we need. When a new line is started, we create a new gameObject “Line_” + i.
  • For each line we iterate through the sentence and split it up when we find a space. (You could also define a custom split so that you can split your sentence into chunks instead of each and every word like we needed). All these words are saved into a temporary array called “Words”.
  • For every word in the line a new GameObject is created (“Word_”+i) and childed within the Line gameObject. At this point we generate a few things per word. 1. A Text Mesh which text will equal to the value we stored earlier when we split the sentence up. 2. A box collider the exact size of word. 3. A small data script to store the word ID and also a reference to the absolute main parent object (“DiallogueField”) so that it can notify it later when it is pressed on.
  • If the currently generated word is not the first word, then its X position is pushed to the right equal to the previous word, plus the previous word’s bounding box width value plus a padding.
  • When a new line is generated and it is not the first, its Y position is pushed down equal to the Y position of the previous line plus the bounding box height of the previous line.

Now, my actual word tapping method was a but of a naive implementation, but it still got the job done. The “DialogueField” main parent object had a script attached to it, which had an array of audio files for each word in each line (which probably won’t be very efficient when you have a screen covered in text with a lot of audio to load on start). This script handled the currently active word being played and so forth.

In general i had a tap/click input manager which checked which element was being clicked on, if a word is clicked which had a WordData script attached to it, it would tell it that it was clicked. This script would then access the member variable to see what it’s DialogueField parent is, and tell it that it needs to play its audio if it is able to, tell it to stop playing another word, and also tell it to change text color/fading and all that fancy stuff.

In a nut shell, all i basically did was automate what i would have done by hand anyway:

  • A bunch of text objects with colliders on them that would listen for clicks/on tap events, and then would notify their controller that they have been clicked on and react accordingly.
  • The thing i mainly focused on was the editor script to visually edit all these elements and make sure they all generate properly sized collider boxes and get spaced out properly.