Chapter 11:
Unity UI: Buttons and User Input

Objectives

  • Create Button GameObjects within a Canvas
  • Configure Vertical Layout Group to force child expansion
  • Implement dynamic button creation using prefabs
  • Apply Instantiate() to create UI elements at runtime
  • Parse Ink Choice objects into Button text
  • Evaluate Unity UI vs TextMeshPro for button text rendering

Table of Contents


Working with Buttons

The Button is a fundamental user interface element. It is something a user can click on that should produce some effect.

Note: As of 2025, while Unity UI (uGUI) buttons remain widely used, consider using TextMeshPro - Button for better text rendering quality. The concepts in this chapter apply to both, but TextMeshPro provides superior text display and more formatting options.

alt text

Based on the GameObject created previously in this book, adding a new Button GameObject to the existing Canvas is possible through right-clicking on it, going to UI, and then clicking on Button. This will add a Button as a child of Canvas.

alt text

Once added, the new Button will be child of the Canvas and also have its own child: another Text. This is because the Button is only that, a button. The text component of the Button is actually another Text.

alt text

When viewed on the Game View, the existing Text (with its text of “New Text”) and the button will be arranged vertically (due to the use of the Vertical Layout Group added in the previous chapter). However, when viewed, the Button is small compared to the existing content of the Canvas.

Forcing Child Expansion

alt text

In the previous chapter, the option Child Force Expand was left unchecked in the Vertical Layout Group Properties. While this worked with the sole child of Text, with the additional child GameObject of Button, it now needs to be used to force all children to expand within the Canvas.

Reminder: Clicking on a component in the Hierarchy Window lists its components in the Inspector Window. Clicking on Canvas allows for accessing its components and the properties of the Vertical Layout Group.

alt text

The result of forcing the child GameObjects to expand can be seen in the Game View.

alt text

With the new option checked, all of the child GameObjects are forced to expand to fill the available space of the Canvas.

While not ideal, the new arrangement of GameObjects is a good place to stop and return to C# code in order to start processing Choice objects and using their text properties to change the text of the Text child of the Button.

Parsing Choices into Buttons

Based on what was introduced in a previous chapter, the method GetComponentInChildren<T>() can be used to search for a child component starting from a parent GameObject. In the case of Canvas, because the added Button is now a child of it, the method can be used again. This time, instead of searching for Text, it can be used to find the added Button.

Note: As of 2025, it’s recommended to use specific type parameters with generic methods. The syntax GetComponentInChildren<Button>() is preferred over the older GetComponentInChildren<GameObject>() pattern.

// From this GameObject, look in its children for a component of the type "Button".
// Return a reference to this component and save it locally.
Button childButton = GetComponentInChildren<Button>();

This time, unlike in the previous chapter with using the now saved component, an additional search is needed to find the Text child of the found Button.

// From this GameObject, look in its children for a component of the type "Button".
// Return a reference to this component and save it locally.
Button childButton = GetComponentInChildren<Button>();

// From this GameObject, look in its children for a component of the type "Text".
// Return a reference to this component and save it locally.
Text buttonText = childButton.GetComponentInChildren<Text>();

Note: The method GetComponentInChildren<T>() returns the first component it finds of the specified type. When searching from Canvas, this finds its child Text first. When searching from Button, this finds its Text component first.

Finally, to demonstrate the editing of its text property, the following line is added:

// Change the text
buttonText.text = "Testing";

Put all together with the existing code developed in the previous chapter, it would be the following:

NewBehaviourScript.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Add the Ink Runtime
using Ink.Runtime;
// Add Unity UI
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    // Add a TextAsset representing the compiled Ink Asset
    [SerializeField] private TextAsset inkJSONAsset;
    
    // Add a Button Prefab
    [SerializeField] private Button buttonPrefab;
    
    // Private Story object
    private Story exampleStory;
    
    // Private Text reference
    private Text childText;
    
    // Start is called before the first frame update
    void Start()
    {
        // Create a new Story object using the compiled (JSON) Ink story text
        exampleStory = new Story(inkJSONAsset.text);
        
        // From this GameObject, look in its children for a component of the type "Text".
        childText = GetComponentInChildren<Text>();
        
        // Reset the existing text
        childText.text = "";
        
        // From this GameObject, look in its children for a component of the type "Button".
        Button childButton = GetComponentInChildren<Button>();
        
        // From the Button, get its Text child
        Text buttonText = childButton.GetComponentInChildren<Text>();
        
        // Change the text
        buttonText.text = "Testing";
    /* Lines 101-153 omitted */
    }
}

When run, the text of the child GameObject of Button will be overwritten.

alt text

With this in place, it is time to change the existing Ink story from the previous chapters and re-introduce the code to parse Choice objects.

New Ink.ink:

The door opens upon a darkly-lit attic.

* [Proceed?]
-

You cautiously walk forward, noting the dust coating everything around you. Looking down, you even seen your own footprints in the dust from the door.

In the new Ink code, there is a choice with selected output. (This means the words in the choice will not appear in the output.) It is also using a gathering point, -, that will gather the story back into the point after the set of choices instead of branching it.

To parse the new choice in the Ink story, the inner loop code developed in previous chapters can be used again.

// For each choice in currentChoices, set its values to the new variable 'choice'
foreach (Choice choice in exampleStory.currentChoices)
{
}

This time, instead of using Debug.Log(), the text of the choice will be used to overwrite the text of the Button’s Text.

// For each choice in currentChoices, set its values to the new variable 'choice'
foreach (Choice choice in exampleStory.currentChoices)
{
    // Set the button's text to the choice's text
    buttonText.text = choice.text;
}

Put at the end of the previous code inside of the while() loop, it would look like the following:

NewBehaviourScript.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Add the Ink Runtime
using Ink.Runtime;
// Add Unity UI
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    // Add a TextAsset representing the compiled Ink Asset
    [SerializeField] private TextAsset inkJSONAsset;

    // Start is called before the first frame update
    void Start()
    {
        // Create a new Story object using the compiled (JSON) Ink story text
        Story exampleStory = new Story(inkJSONAsset.text);

        // From this GameObject, look in its children for a component of the type "Text".
        // Return a reference to this component and save it locally.
        Text childText = GetComponentInChildren<Text>();

        // Reset the existing text of "New Text" to an empty string
        childText.text = "";

        // From this GameObject, look in its children for a component of the type "Button".
        // Return a reference to this component and save it locally.
        Button childButton = GetComponentInChildren<Button>();

        // From this GameObject, look in its children for a component of the type "Text".
        // Return a reference to this component and save it locally.
        Text buttonText = childButton.GetComponentInChildren<Text>();

        // Each loop, check if there is more story to load
        while (exampleStory.canContinue)
        {
            // Load the next story chunk and return the current text
            string currentTextChunk = exampleStory.Continue();

            // Get any tags loaded in the current story chunk
            List<string> currentTags = exampleStory.currentTags;

            // Create a blank line of dialogue
            string line = "";

            // For each tag in currentTag, set its values to the new variable 'tag'
            foreach (string tag in currentTags)
            {
                // Concatenate the tag and a colon
                line += tag + ": ";
            }

            // Concatenate the current text chunk
            // (This will either have a tag before it or be by itself.)
            line += currentTextChunk;

            // Concatenate the content of 'line' to the existing text
            childText.text += line;

            // For each choice in currentChoices, set its values to the new variable 'choice'
            foreach (Choice choice in exampleStory.currentChoices)
            {
                // Set the button's text to the choice's text
                buttonText.text = choice.text;
            }

        }
    }
}

alt text

When run, the choices within the Ink Story API property currentChoices will be parsed and their text used to overwrite the text of the Button’s Text. This would place the text inside the of the Button and present the user with an option.

The current solution, while great for only one option per choice set in an Ink story, only has one Button. If the story presented multiple options for the player, this would not work well. There needs to be a way to create one or more Buttons dynamically. Instead of overwriting a single button each time, they could be created and destroyed as needed.

Creating Prefabs

Unity has a concept known as a prefab. This a GameObject that is stored as an Asset. All of its property values and components are saved. When needed, it can be instantiated (created based on the stored Asset) and added to a Scene.

Prefabs are commonly used in situations where one or more GameObject (because they can be nested in parent-child relationships) need to be placed in a Scene during runtime or generated due to some player actions. A GameObject is created, its property values set, and then it is stored. When the Scene is run, code creates one or more copies of the GameObject based on prefab’s values.

Creating a Button Prefab

In the previous section, a problem was identified where the Button added to the Canvas did not reflect the possibly many choices generated by an Ink story. A single button was not enough. Creating a single Button prefab and then creating copies of it, however, is a great solution where multiple Button GameObjects are needed.

Creating a Prefab in Unity is as easy as dragging and dropping a GameObject from the Hierarchy window into the Project window.

alt text

Dragging and dropping the existing Button from the Hierarchy window into the Project window creates a Prefab of the GameObject. Its icon color changes from a grey to a blue outline, signaling it is now a Prefab (and an Asset).

alt text

The Inspector window for the Button also shows its new status. As a Prefab, it is labeled as a “Prefab Asset” now.

Now that the Button is a Prefab, it can be deleted from the Canvas.

alt text

Right-clicking and clicking on “Delete” will remove the GameObject Button.

It may seem strange to remove the Button GameObject, but now that it is a Prefab, its property values are saved as a “template” that can be recreated easily. With the ability to create copies, the original is no longer needed.

Dynamically Creating Buttons

Now that Button is a Prefab, it needs to be used in a coding context. As with the compiled Ink (JSON) file used in previous chapters, an Asset can be associated with a scripting component through a two-step process:

1) Create a public property on the C# class 1) Drag and drop the Asset from the Project window on the scripting component property value

Because the Button is a Button, this will be its data type (much like was used to find it using the GetComponentInChildren<GameObject>() method).

// Add a Button representing the ButtonPrefab
public Button ButtonPrefab;

Just like the compiled Ink (JSON) file, this property is added before the class declaration.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Add the Ink Runtime
using Ink.Runtime;
// Add Unity UI
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    // Add a TextAsset representing the compiled Ink Asset
    public TextAsset InkJSONAsset;

    // Add a Button representing the ButtonPrefab
    public Button ButtonPrefab;

    // Start is called before the first frame update
    void Start()
    {
      // ...
    }
}

alt text

Clicking on the Canvas shows the new public property.

alt text

Next, dragging and dropping the Button Prefab from the Project window to the “Button Prefab” property value associates it with code.

In the code, new lines need to be added as part of loop handling the parsing of the choices from the Ink Story API to create new Button dynamically.

// For each choice in currentChoices, set its values to the new variable 'choice'
foreach (Choice choice in exampleStory.currentChoices)
{
    // Create a new GameObject based on a Prefab and set its parent to this.transform
    Button choiceButton = Instantiate(ButtonPrefab, this.transform);
}

The method Instantiate() creates a clone of an existing object. Because ButtonPrefab is an object because of its Prefab status, it can be cloned and a copy saved. At the same time, in order to make the new Button part of the Canvas, it needs to be set as a child. The use of the second parameter, this.transform, tells the method to set its parent to the current “transform” (position, rotation, and scale) of the current GameObject, Canvas.

Reminder: All GameObjects have a Transform component. Through setting a GameObject’s transform property to another, it makes it a child of that other GameObject. In Unity, setting a new GameObject’s transform to this.transform makes it a child of the current GameObject.

Next, the Text component of the newly-created Button needs to be found. The use of the GetComponentInChildren<GameObject>() method follows the same pattern used previously in this chapter.

// For each choice in currentChoices, set its values to the new variable 'choice'
foreach (Choice choice in exampleStory.currentChoices)
{
    // Create a new GameObject based on a Prefab and set its parent to this.transform
    Button choiceButton = Instantiate(ButtonPrefab, this.transform);

    // From choiceButton, look in its children for a component of the type "Text".
    // Return a reference to this component and save it locally.
    Text choiceText = choiceButton.GetComponentInChildren<Text>();

    // Set the button's text to the choice's text
    choiceText.text = choice.text;
}

Put all together, the code is the following:

NewBehaviourScript.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Add the Ink Runtime
using Ink.Runtime;
// Add Unity UI
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    // Add a TextAsset representing the compiled Ink Asset
    public TextAsset InkJSONAsset;

    // Add a Button representing the ButtonPrefab
    public Button ButtonPrefab;

    // Start is called before the first frame update
    void Start()
    {
        // Create a new Story object using the compiled (JSON) Ink story text
        Story exampleStory = new Story(InkJSONAsset.text);

        // From this GameObject, look in its children for a component of the type "Text".
        // Return a reference to this component and save it locally.
        Text childText = GetComponentInChildren<Text>();

        // Reset the existing text of "New Text" to an empty string
        childText.text = "";

        // Each loop, check if there is more story to load
        while (exampleStory.canContinue)
        {
            // Load the next story chunk and return the current text
            string currentTextChunk = exampleStory.Continue();

            // Get any tags loaded in the current story chunk
            List<string> currentTags = exampleStory.currentTags;

            // Create a blank line of dialogue
            string line = "";

            // For each tag in currentTag, set its values to the new variable 'tag'
            foreach (string tag in currentTags)
            {
                // Concatenate the tag and a colon
                line += tag + ": ";
            }

            // Concatenate the current text chunk
            // (This will either have a tag before it or be by itself.)
            line += currentTextChunk;

            // Concatenate the content of 'line' to the existing text
            childText.text += line;

            // For each choice in currentChoices, set its values to the new variable 'choice'
            foreach (Choice choice in exampleStory.currentChoices)
            {
                // Create a new GameObject based on a Prefab and set its parent to this.transform
                Button choiceButton = Instantiate(ButtonPrefab, this.transform);

                // From choiceButton, look in its children for a component of the type "Text".
                // Return a reference to this component and save it locally.
                Text choiceText = choiceButton.GetComponentInChildren<Text>();

                // Set the button's text to the choice's text
                choiceText.text = choice.text;
            }

        }
    }
}

alt text

When run, a new Button will be created based on the saved Prefab. It’s Text’s text will be set to the text of the Choice.

In fact, because the new code is inside the loop parsing Choice objects based on the currentChoice property of the Ink Story API, it will create multiple buttons based on the number of choices in any given set.

For example, changing the Ink code to the following –

New Ink.ink:

This example has multiple choices.

* Choice 1
* Choice 2
* Choice 3
* Choice 4

– would create as many buttons as choices.

alt text