Unity

Creating a tutorial for an application in Unity. -Replace the coroutines with UniTask2.

Technologies used in TETRY 3D part 5

It’s been a while, GE Planet here.

In this article, we will try to rewrite the text display coroutine for the tutorial we made in the previous article into UniTask2.

Actually, I haven’t used it in TETRY 3D yet, but I’ll practice it here as an extra so that I can use it eventually.

For a detailed description of the implementation, please refer to the previous article.

Before I explain, I would like to advertise.

We are currently distributing an alarm app on Androd.

The selling point is the ability to acquire sleep cycles and draw animated characters using Live2D. Please give it a try.

Sleep Manager
Sleep Manager
Developer:J.Y
Free
posted withApp Reach

So let’s get started!

Content

Introduction of UniTask2

First, make sure that UniTask is ready for use.

UniTask

Download the latest version of UniTask from the site above.

At the time of posting this article, version 2.0.37 was the latest. In that case, you can choose the file “UniTask.2.0.37.unitypackage” and download it.

Import the unitypackage into your project.

Once the import is complete, you are ready to use UniTask in your project.

By the way, there is a change in UniTask in Ver2.

using Cysharp.Threading.Tasks;

It used to be declared by “using UniRx.Async”, but now it seems to be completely independent from UniRx.

Also, to use “cancellationToken” ,

using System.Threading;

declare it.

This implementation may not be necessary since there is no need to exit the task in the middle, but I would like to make a habit of using it for the future.

Obtaining Token

CancellationToken is a cancellation token that cancels the UniTask when an object is deleted. If you have created a task that keeps looping, you should use this token.

var token = this.GetCancellationTokenOnDestroy();

Get as above.

You will pass the prepared token as an argument to UniTask.

Rewriting MainTipsRegenerater

Now let’s rewrite it.

Let’s start with the main text section as before.

The following is an excerpt of the rewritten part.

Coroutine

IEnumerator MainTipsRegenerater(string[] sentences)

UniTask

async UniTask MainTipsRegenerater(string[] sentences, CancellationToken token)

The difference is that I changed IEnumerator to async UniTask, and

You have added CancellationToken token as an argument.

I will continue.

Coroutine

yield return new WaitForSeconds(0.5f);
yield return null;
yield break;
yield return new WaitForSeconds(0.01f);

UniTask

await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
await UniTask.Yield(PlayerLoopTiming.Update, token);
return;
await UniTask.Delay(10, false, PlayerLoopTiming.Update, token);

You need to rewrite the yield part.

WaitForSeconds can be reproduced by UniTask. 500 is milliseconds, so 0.5 seconds. The token is passed at the end of the argument.

For yield return null, use UniTask.Yield. Yield. It also passes a token.

The yield break was an instruction to exit the coroutine, but there seemed to be no way to exit UniTask in the middle, so we pulled up the process and used return to skip to the end of the task.

0.01 seconds is 10 milliseconds, so describe it as such.

Here is the rewritten MainTipsRegenerater.

    async UniTask MainTipsRegenerater(string[] sentences, CancellationToken token)
    {
        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);
                        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

                        //For animation
                        mainAnim.SetBool("onAir",true);
                        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
                        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;
            }


            await UniTask.Yield(PlayerLoopTiming.Update, token);
        }

        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);
                            await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
                            mainWindow.SetActive(false);
                            break;
                        case 1:
                            break;
                        case 2:
                            break;
                    }
                    isOneMessage = false;
                    isEndMessage = true;
                    return;
                }
                //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;
                }

                await UniTask.Delay(1, false, PlayerLoopTiming.Update, token);
            }
            else
            {
                //Message to be displayed at one time.
                await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

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

Rewriting SubTipsRegenerater

The contents are the same: take the token as an argument and rewrite the field as shown above.

    async UniTask SubTipsRegenerater(string sentence, int tar, CancellationToken token)
    {
        
        //Show subboxes.
        subWindow.SetActive(true);
        tagetImage.GetComponent<Image>().color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
        tagetImage.sprite = targets[tar];
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

        //For animation
        subAnim.SetBool("onAir", true);
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

        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;
                }

                await UniTask.Delay(1, false, PlayerLoopTiming.Update, token);
            }
            //Message to be displayed at one time.
            else
            {
                await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

                nowTextNum = 0;
                isEndMessage = true;
                return;
            }
        }
    }

Click here for EndSubTips.

    async UniTask EndSubTips(CancellationToken token)
    {
        subText.text = "";
        isOneMessage = false;

        //For animation
        subAnim.SetBool("onAir", false);
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
        subWindow.SetActive(false);
    }

Rewriting ScenarioRegenerater

The next step is to rewrite the ScenarioRegenerater that controls these texts.

    public async UniTaskVoid ScenarioRegenerater(CancellationToken token)
    {
        //======================================//
        //                                      //
        // Here are the steps of the tutorial. //
        //                                      //
        //======================================//

        //Example
        await MainTipsRegenerater(tutoText.mainsentencesA, token);
        await MainTipsRegenerater(tutoText.mainsentencesA, token);
        await MainTipsRegenerater(tutoText.mainsentencesA, token);

        await SubTipsRegenerater(tutoText.subsentencesA[0], 0, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

        await MainTipsRegenerater(tutoText.mainsentencesB, token);
        await MainTipsRegenerater(tutoText.mainsentencesB, token);
        await MainTipsRegenerater(tutoText.mainsentencesB, token);
        await SubTipsRegenerater(tutoText.subsentencesB[0], 1, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

        await SubTipsRegenerater(tutoText.subsentencesB[1], 1, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

    }

I am using async UniTaskVoid, but async UniTask works fine too. I didn’t know the difference, but it is rumored that UniTaskVoid has better performance because it doesn’t reference the return value.

Uncertain topics aside, we are receiving tokens in this task and passing the same to other tasks.

Previously.

        while (!chapterflag)
        {
            yield return MainTipsRegenerater(tutoText.mainsentencesA);
            yield return null;
        }
        chapterflag = false;

I used to use chapterflag to output one sentence at a time like this, but I changed it for readability. This description is also valid for coroutines.

I will now explain the changes.

First, rewrite “yield return” to “await”. Then, since token has been added as an argument, it is also written.

This system calls the regenerator every time a sentence is written, but it closes the window by calling the regenerator whose last sentence is empty. Therefore, there are two sentences each, but it is called three times.

This is the same behavior as the previous time when it was controlled by chapterflag.

The rest of the description is as explained earlier, and there is nothing to add.

Rewriting the Start() part

    void Start()
    {
        //Get token.
        var token = this.GetCancellationTokenOnDestroy();
        //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();
        //Starting a task
        ScenarioRegenerater(token).Forget();
    }

There are also some changes in Start(). First, we need to get the token. As mentioned earlier, it is obtained before starting the task.

Next, the method to start a task can be written as follows: token is passed as an argument.

.Forget() at the end suppresses the warning about asynchronous processing.

Since it is an asynchronous process, you will be warned not to wait for other functions, but since it is not necessary, ignore it with .Forget(). Maybe it will be fixed so that we don’t need it.

Completed tasks

The whole thing looks like this.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;

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;
    //Current message number
    private int textCount;
    //The character number you just displayed.
    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()
    {
        //Get token.
        var token = this.GetCancellationTokenOnDestroy();
        //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();
        //Starting a task
        ScenarioRegenerater(token).Forget();
    }

    public async UniTaskVoid ScenarioRegenerater(CancellationToken token)
    {
        //======================================//
        //                                      //
        // Here are the steps of the tutorial. //
        //                                      //
        //======================================//

        //Example

        await MainTipsRegenerater(tutoText.mainsentencesA, token);
        await MainTipsRegenerater(tutoText.mainsentencesA, token);
        await MainTipsRegenerater(tutoText.mainsentencesA, token);

        await SubTipsRegenerater(tutoText.subsentencesA[0], 0, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

        await MainTipsRegenerater(tutoText.mainsentencesB, token);
        await MainTipsRegenerater(tutoText.mainsentencesB, token);
        await MainTipsRegenerater(tutoText.mainsentencesB, token);

        await SubTipsRegenerater(tutoText.subsentencesB[0], 1, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

        await SubTipsRegenerater(tutoText.subsentencesB[1], 1, token);

        await UniTask.Delay(2000, false, PlayerLoopTiming.Update, token);

        await EndSubTips(token);

    }

    async UniTask MainTipsRegenerater(string[] sentences, CancellationToken token)
    {
        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);
                        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

                        //For animation
                        mainAnim.SetBool("onAir",true);
                        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
                        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;
            }


            await UniTask.Yield(PlayerLoopTiming.Update, token);
        }

        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);
                            await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
                            mainWindow.SetActive(false);
                            break;
                        case 1:
                            break;
                        case 2:
                            break;
                    }
                    isOneMessage = false;
                    isEndMessage = true;
                    return;
                }
                //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;
                }

                await UniTask.Delay(1, false, PlayerLoopTiming.Update, token);
            }
            else
            {
                //Message to be displayed at one time.
                await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

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

    async UniTask SubTipsRegenerater(string sentence, int tar, CancellationToken token)
    {
        
        //Show subboxes.
        subWindow.SetActive(true);
        tagetImage.GetComponent<Image>().color = new Color(1.0f, 1.0f, 1.0f, 1.0f);
        tagetImage.sprite = targets[tar];
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

        //For animation
        subAnim.SetBool("onAir", true);
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

        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;
                }

                await UniTask.Delay(1, false, PlayerLoopTiming.Update, token);
            }
            //Message to be displayed at one time.
            else
            {
                await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);

                nowTextNum = 0;
                isEndMessage = true;
                return;
            }
        }
    }

    async UniTask EndSubTips(CancellationToken token)
    {
        subText.text = "";
        isOneMessage = false;

        //For animation
        subAnim.SetBool("onAir", false);
        await UniTask.Delay(500, false, PlayerLoopTiming.Update, token);
        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");
    }

}

If it’s just a coroutine replacement, it doesn’t seem like there’s much that needs to be changed, but there are UniTask-specific descriptions that you’ll have to get used to.

Summary

Replacing UniTask was actually a rather difficult task. This is because the upgrade to UniTask Ver2 took a lot of time for me to notice the mess related to using and the description of System.Threading to use CancellationToken as explained in the beginning.

I also tried to make it more complicated by trying to write it differently. As you can see from the finished product, if I rewrote only the important parts, it would work.

The yield break was the last thing I had to worry about, but since the specification happened to be that the task would complete if it transitioned to the end, I solved it with return.

The other change is that the waiting time is now specified in milliseconds, so 0.001 seconds can now be specified. The speed has changed in the editor, but I haven’t tried it on the actual machine yet.

Asynchronous processing and method waiting are useful features for game development, and I would like to incorporate them in the future.

This concludes the fifth article on the technologies used in TETRY 3D.

Thank you for reading.

It was GE Planet.



0 Comments

Make a comment