Unity

Creating App Tutorials in Unity – Text Display Features with Coroutines

Technologies used in TETRY 3D. part 4

Hello GE Planet.

In this article, I’d like to introduce the text display function used in the tutorial in TETRY 3D.

The text display is an easy part of the game, but the message window is like an RPG, where the characters are added one by one.

I’ve also included a small window that shows some supplementary information, although the content is designed to be actually manipulated in between explanations.

Although the scripts introduced here are mainly for displaying text, you can easily complete a tutorial by adding a script for operating each application in between.

Contents

Main Window

Let’s start with the part that displays the main text.

While this window is on, you can touch any part of the screen to send text. Therefore, you can’t control the game.

IEnumerator MainTipsRegenerater(string[] sentences)
    {
        while (isEndMessage || sentences == null)
        {
            //If you want to use it in an additive scene, set tipsChecker to 1 
            //and set utilityText or utilityButton to Start() of the loaded scene.
            switch (tipsChecker)
            {
                case 0:
                    if (!mainWindow.activeSelf)
                    {
                        mainWindow.SetActive(true);
                        yield return new WaitForSeconds(0.5f);

                        //For animation
                        mainAnim.SetBool("onAir",true);
                        yield return new WaitForSeconds(0.5f);
                        utilityText = mainText;
                        utilityButton = nextButton;

                        //When tipsChecker is set to 0, isEndMessage is set to false and the main part works.
                        isEndMessage = false;
                    }
                    break;
                case 1:
                    tipsChecker = 2;
                    isEndMessage = false;
                    break;
                case 2:
                    break;
            }


            yield return null;
        }

        while (!isEndMessage)
        {
            //No message to be displayed at one time
            if (!isOneMessage)
            {
                //If all messages are displayed, the game objects are hidden
                if (textCount >= sentences.Length)
                {
                    textCount = 0;
                    
                    switch (tipsChecker)
                    {
                        case 0:
                            //For animation
                            mainAnim.SetBool("onAir", false);
                            yield return new WaitForSeconds(0.5f);
                            mainWindow.SetActive(false);
                            break;
                        case 1:
                            break;
                        case 2:
                            break;
                    }
                    chapterflag = true;
                    isOneMessage = false;
                    isEndMessage = true;
                    yield break;
                }
                //Otherwise, initialize the text processing related items and display them from the next character.

                //Add one character after the text display time has elapsed.
                utilityText.text += sentences[textCount][nowTextNum];
                nowTextNum++;

                //The full message was displayed, or the maximum number of lines were displayed.
                if (nowTextNum >= sentences[textCount].Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            else
            {
                //Message to be displayed at one time.
                yield return new WaitForSeconds(0.5f);

                //Press the Next button.
                utilityButton.interactable = true;
                isEndMessage = true;
            }
        }
    }

The first argument is a string array called sentences.

As there is an upper limit to the number of characters that can be displayed in a window, the exceeded characters are divided in an array. We also considered counting the number of characters and automatically allocating them to the next window, but it didn’t work so well with multiple languages, so we adjusted the character overflow manually. This is the way it works because the amount of text is small.

isEndMessage is the end-of-message flag, which, if false, means there is still text in the sentences that is not yet displayed.

You can control the behavior by changing the value of tipsChecker, which initially works when isEndMessage is true. If the default value is 0 and sentences is not null, it activates the main window and sets the text object and button to be operated on.

TipsChecker is mainly used when an additive scene is used to create a UI, because once an additive scene has been loaded, the scene below it cannot be interfered with, so it is necessary to prepare a text window and other objects in the additive scene as well.

For this reason, you can prevent utility variables from being overwritten by describing the process in the Start() part of the Additive Scene when you register them in utilityText, etc., and starting the MainTipsRegenerater with tipsChecker set to 1.

Then isEndMessage is set to false and the text display feature begins to work.

isOneMessage is monitoring to see if one element of sentences has completed being displayed.

The if (textCount >= sentences.Length) part is what happens when all the elements of sentences are displayed. Again, there is a tipsChecker, and if it is 0, it will animate and deactivate the window. If it’s not, then closing the additive scene will solve the problem.

You see the chapterflag, which is a flag to display the text at any given time. This is to prevent the text from flowing through the function for creating a tutorial, which we will discuss later.

If chapterflag is false, the text display function is activated. This is automatically set to true at the end of the text.

utilityText.text += sentences[textCount][nowTextNum];
nowTextNum++;

This part of the text is displayed one character at a time, where nowTextNum is the number of characters.

The textCount will be counted up when you press the Next button, which is described later.

If (nowTextNum >= sentences[textCount].Length), monitor the maximum number of characters in each element and move the flag of isOneMessage.

The flow to this point is processed with 0.01 second weighting at the end. Depending on the processing speed of the device, there is a possibility that the display may become slow, so I think it’s better to check with the actual device.

Once isOneMessage is true, make isEndMessage true as well so that you can press the Next button.

Next, look at the Next button.

    public void NextButton()
    {
        //Initialize the message function when you press the Next button.
        utilityButton.interactable = false;
        utilityText.text = "";
        nowTextNum = 0;
        //When displaying multiple times in a row, the number of Text is counted.
        textCount++;
        isOneMessage = false;
        isEndMessage = false;
    }

The button must first have interactable set to false in the inspector. Similarly, the parent main text object should be unchecked so that it is initially inactive.

Pressing the button initializes the text and the number of characters, adds the textCount, and re-sets various flags, and then the next sentence is displayed.

Sub Window

Next, let’s take a look at the sub-windows.

The sub-window can be used to supplement the player in the tutorial. This sub-window will give you hints on how to operate the game.

IEnumerator SubTipsRegenerater(string sentence, int tar)
    {
        //Show subboxes.
        subWindow.SetActive(true);
        tagetImage.GetComponent<Image>().color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
        tagetImage.sprite = targets[tar];
        yield return new WaitForSeconds(0.5f);

        //For animation
        subAnim.SetBool("onAir", true);
        yield return new WaitForSeconds(0.5f);

        isEndMessage = false;

        while (!isEndMessage)
        {
            //No message to be displayed at one time	
            if (!isOneMessage)
            {
                //Add one character after the text display time has elapsed.
                subText.text += sentence[nowTextNum];
                nowTextNum++;

                //The full message was displayed, or the maximum number of lines were displayed.
                if (nowTextNum >= sentence.Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            //Message to be displayed at one time.
            else
            {
                yield return new WaitForSeconds(0.5f);

                nowTextNum = 0;
                isEndMessage = true;
                yield break;
            }
        }
    }

    IEnumerator EndSubTips()
    {
        subText.text = "";
        isOneMessage = false;

        //For animation
        subAnim.SetBool("onAir", false);
        chapterflag = true;
        yield return new WaitForSeconds(0.5f);
        subWindow.SetActive(false);
    }

SubTipsRegenerater has the ability to display text and images. You can add an image of a button.

Images are also prepared as an array like text. Specify the image to be displayed by int.

private Sprite[] targets = new Sprite[2];
   
void SetTargets()
{
  targets[0] = Resources.Load<Sprite>("TutorialSprite/example1");
  targets[1] = Resources.Load<Sprite>("TutorialSprite/example2");
}

This time it is loaded from Resources.

First, we activate the object as well as the main one. Initially, you should leave it inactive.

If you want to change the transparency of the image, you can change the fourth argument of color.

The main mechanics are the same as in the main game, but the end of the display is controlled by a separate coroutine to allow the player to control the game while it is being displayed.

You can end the sub-window by calling EndSubTips().

The sub-window can’t display text continuously, so the argument should be a single string.

Tutorial body

We will combine these coroutines to create a tutorial.

The following coroutine is the main body of the system.

public IEnumerator ScenarioRegenerater()
    {
        //======================================//
        //                                      //
        // Here are the steps of the tutorial. //
        //                                      //
        //======================================//

        //例
        while (!chapterflag)
        {
            yield return MainTipsRegenerater(tutoText.mainsentencesA);
            yield return null;
        }
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesA[0], 0);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
        chapterflag = false;
        while (!chapterflag)
        {
            yield return MainTipsRegenerater(tutoText.mainsentencesB);
            yield return null;
        }
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesB[0], 1);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesB[1], 1);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
    }

I’ve described it as an example, which you can see in the UnityPackage on GitHub.

MainTipsRegenerater is used by enclosing it with a while statement, which loops one sentence at a time while chapterflag is false.

You need to set chapterflag = false every time you want to display the following text.

Complete the tutorial by describing the game’s button controls and stage state changes in this coroutine.

The SubTipsRegenerator can end the display at any point in time, but it can’t output at the same time as the main window. However, it cannot output at the same time as the main window, so be sure to call EndSubTips() before calling MainTipsRegenerater next.

Separate variables, such as character count, can be output at the same time if you have separate main and sub variables.

Usage

Let’s look at the overall script first.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class TutorialSystem : MonoBehaviour
{
    //message box
    public GameObject mainWindow;
    //sub box
    public GameObject subWindow;
    //If you want to move the window to display
    public Animator mainAnim;
    public Animator subAnim;
    //Message Box Text
    public Text mainText;
    //Sub  Box Text
    public Text subText;
    //The actual Text to process
    public Text utilityText;
    //Image for Sub Box
    public Image tagetImage;
    //Sprites to be used. If there are many, make an array.
    private Sprite[] targets = new Sprite[2];
    //Flags to be used between additive scenes
    private int tipsChecker = 0;
    //Next button for the main box
    public Button nextButton;
    //The Next button, which is actually used
    public Button utilityButton;
    //Class for storing text
    public tutorialTexts tutoText;
    //For tutorial progress
    private bool chapterflag = false;
    //Current message number
    private int textCount;
    //今見ている文字番号
    private int nowTextNum = 0;
    //Whether you have displayed one message
    private bool isOneMessage = false;
    //Whether you have displayed all the messages.
    private bool isEndMessage = true;


    // Start is called before the first frame update
    void Start()
    {
        //If there are any buttons you want to disable in the tutorial, add them to the process.

        //If you want to include an Image in the subbox.
        SetTargets();
        StartCoroutine(ScenarioRegenerater());
    }

    public IEnumerator ScenarioRegenerater()
    {
        //======================================//
        //                                      //
        // Here are the steps of the tutorial. //
        //                                      //
        //======================================//

        //Example
        while (!chapterflag)
        {
            yield return MainTipsRegenerater(tutoText.mainsentencesA);
            yield return null;
        }
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesA[0], 0);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
        chapterflag = false;
        while (!chapterflag)
        {
            yield return MainTipsRegenerater(tutoText.mainsentencesB);
            yield return null;
        }
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesB[0], 1);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
        chapterflag = false;
        yield return SubTipsRegenerater(tutoText.subsentencesB[1], 1);
        yield return new WaitForSeconds(2.0f);
        yield return EndSubTips();
    }

    IEnumerator MainTipsRegenerater(string[] sentences)
    {
        while (isEndMessage || sentences == null)
        {
            //If you want to use it in an additive scene, set tipsChecker to 1 
            //and set utilityText or utilityButton to Start() of the loaded scene.
            switch (tipsChecker)
            {
                case 0:
                    if (!mainWindow.activeSelf)
                    {
                        mainWindow.SetActive(true);
                        yield return new WaitForSeconds(0.5f);

                        //For animation
                        mainAnim.SetBool("onAir",true);
                        yield return new WaitForSeconds(0.5f);
                        utilityText = mainText;
                        utilityButton = nextButton;

                        //When tipsChecker is set to 0, isEndMessage is set to false and the main part works.
                        isEndMessage = false;
                    }
                    break;
                case 1:
                    tipsChecker = 2;
                    isEndMessage = false;
                    break;
                case 2:
                    break;
            }


            yield return null;
        }

        while (!isEndMessage)
        {
            //No message to be displayed at one time
            if (!isOneMessage)
            {
                //If all messages are displayed, the game objects are hidden
                if (textCount >= sentences.Length)
                {
                    textCount = 0;
                    
                    switch (tipsChecker)
                    {
                        case 0:
                            //For animation
                            mainAnim.SetBool("onAir", false);
                            yield return new WaitForSeconds(0.5f);
                            mainWindow.SetActive(false);
                            break;
                        case 1:
                            break;
                        case 2:
                            break;
                    }
                    chapterflag = true;
                    isOneMessage = false;
                    isEndMessage = true;
                    yield break;
                }
                //Otherwise, initialize the text processing related items and display them from the next character.

                //Add one character after the text display time has elapsed.
                utilityText.text += sentences[textCount][nowTextNum];
                nowTextNum++;

                //The full message was displayed, or the maximum number of lines were displayed.
                if (nowTextNum >= sentences[textCount].Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            else
            {
                //Message to be displayed at one time.
                yield return new WaitForSeconds(0.5f);

                //Press the Next button.
                utilityButton.interactable = true;
                isEndMessage = true;
            }
        }
    }

    IEnumerator SubTipsRegenerater(string sentence, int tar)
    {
        //Show subboxes.
        subWindow.SetActive(true);
        tagetImage.GetComponent<Image>().color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
        tagetImage.sprite = targets[tar];
        yield return new WaitForSeconds(0.5f);

        //For animation
        subAnim.SetBool("onAir", true);
        yield return new WaitForSeconds(0.5f);

        isEndMessage = false;

        while (!isEndMessage)
        {
            //No message to be displayed at one time	
            if (!isOneMessage)
            {
                //Add one character after the text display time has elapsed.
                subText.text += sentence[nowTextNum];
                nowTextNum++;

                //The full message was displayed, or the maximum number of lines were displayed.
                if (nowTextNum >= sentence.Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            //Message to be displayed at one time.
            else
            {
                yield return new WaitForSeconds(0.5f);

                nowTextNum = 0;
                isEndMessage = true;
                yield break;
            }
        }
    }

    IEnumerator EndSubTips()
    {
        subText.text = "";
        isOneMessage = false;

        //For animation
        subAnim.SetBool("onAir", false);
        chapterflag = true;
        yield return new WaitForSeconds(0.5f);
        subWindow.SetActive(false);
    }

    public void NextButton()
    {
        //Initialize the message function when you press the Next button.
        utilityButton.interactable = false;
        utilityText.text = "";
        nowTextNum = 0;
        //When displaying multiple times in a row, the number of Text is counted.
        textCount++;
        isOneMessage = false;
        isEndMessage = false;
    }

    void SetTargets()
    {
        targets[0] = Resources.Load<Sprite>("TutorialSprite/example1");
        targets[1] = Resources.Load<Sprite>("TutorialSprite/example2");
    }
}

There is a class for storing text, tutorialTexts tutoText. It is displayed as follows.

using UnityEngine;

public class tutorialTexts : MonoBehaviour
{
    //Store the text to be displayed.
    //Put each amount that can be displayed at a time into an array.
    public string[] mainsentencesA;
    public string[] mainsentencesB;
    public string[] subsentencesA;
    public string[] subsentencesB;

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

    void SetA()
    {
        mainsentencesA = new string[]{
            "Hi GE Planet.",
            "We are experimenting with displaying the text of the tutorial.",
        };
    }

    void SetB()
    {
        mainsentencesB = new string[]{
            "Sub-blocks can contain tips and other information.",
            "If you have a lot of text, create a separate text management class."
        };

    }

    void SetSubA()
    {
        subsentencesA = new string[]
        {
            "Example1",
        };
    }

    void SetSubB()
    {
        subsentencesB = new string[]
        {
            "Example2",
            "You can also supplement it."
        };
    }

    void masseageSet()
    {
        SetA();
        SetB();
        SetSubA();
        SetSubB();
    }
}

The structure is simple. Create an array of text for the main and sub classes. If you have a lot of text in your novels, you may want to create a separate class to manage it.

Now, to explain the variables, the inspector registers all public variables except those with utility attached.

As for the structure.

This is how it works.

The main window is an Image called “MessageBox”. The text part named “MainMessage” and “NextButton” are placed as its children. NextButton() in “TutorialSystem” which is a component of “Manager” is registered in “NextButton”.

You can also do this with Panel, but Text seems to perform better.

The “subMessageBox” is populated with images and text as children.

Attach the “TutorialSystem” and “tutorialTexts” to an empty object called “Manager”.

When you actually use it, you should attach it to an object, such as a game manager, to control it.

Summary

The tutorial text display feature I’ve introduced is a simple one, but I think it’s enough to explain a simple game.

As the amount of text increases, more complex features will need to be added, but these are the ones that are easily implemented first.

Because it would be cumbersome to describe button control and various game actions, it may be easier to manage if you divide the tutorials into smaller sections.

This concludes the techniques used in the 4th TETRY 3D.

Thank you for your close reading.

It was GE Planet.