Unity

Unityでアプリのチュートリアルを作成 -コルーチンをUniTask2に置き換えてみる。

TETRY 3Dで使われている技術 5

お久しぶりです、GE Planetです。

今回は前回作ったチュートリアル用のテキスト表示コルーチンをUniTask2に書き換えてみるという内容です。

実はTETRY 3Dではまだ使用していませんが番外編ということで、いずれ使えるようにここで練習しておきます。

実装の細かい説明は前回の記事を参照してください。

解説の前に宣伝を。

現在Androdで目覚ましアプリを配信中です。

睡眠サイクルの取得やLive2Dを用いたキャラクターのアニメ描画が売りです。一度お試しください。

スリープマネージャー
スリープマネージャー
開発元:J.Y
無料
posted withアプリーチ

それでは始めましょう。

コンテンツ

UniTask2の導入

まずはUniTaskを使える状態にします。

UniTask

上のサイトから最新バージョンのUniTaskをダウンロードしてください。

この記事の投稿時点ではVer.2.0.37が最新でした。その場合、「UniTask.2.0.37.unitypackage」というファイルが選べると思うのでこれをダウンロードします。

unitypackageをプロジェクトにインポートしてください。

インポートが終わればそのプロジェクトでUniTaskを使う準備は完了です。

ところでUniTaskはVer2になって変更された点があります。

using Cysharp.Threading.Tasks;

今までは「using UniRx.Async」で宣言していましたが、UniRxから完全に独立したようです。

また後述する「CancellationToken」を使うためには

using System.Threading;

も宣言します。

今回の実装はタスクを途中で抜ける必要がないため必要がないかもしれませんが、今後のために使う癖をつけておきたいと思います。

Tokenの取得

CancellationTokenはオブジェクトが削除された時にUniTaskも破棄するためのキャンセル用トークンです。これがないとオブジェクトを削除してもUniTaskは独立して動き続きますので注意が必要です。whileなどでループし続けるタスクを作った場合は必ず使うべきです。

var token = this.GetCancellationTokenOnDestroy();

上記のように取得します。

UniTaskには用意したtokenを引数として渡すことになります。

MainTipsRegeneraterの書き換え

それでは書き換えていきます。

まずは前回同様メインのテキスト部分から。

書き換えた部分を抜粋して載せます。

コルーチン

IEnumerator MainTipsRegenerater(string[] sentences)

UniTask

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

違いはIEnumerator をasync UniTaskに変えたことと、

CancellationToken tokenを引数に書き加えたことですね。

続いていきます。

コルーチン

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

yield部分を書き換える必要があります。

WaitForSecondsはUniTask.Delayで再現できます。500はミリ秒なので0.5秒です。引数の最後でtokenを渡しています。

yield return nullはUniTask.Yieldを使います。同じくtokenを渡します。

yield breakはコルーチンを抜ける命令でしたが、UniTaskを途中で抜ける方法がなさそうでしたので処理を引き上げて、returnを使ってタスクが終わるところまで飛ばします。

0.01秒は10ミリ秒ですのでそのように記述します。

書き換えた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;
            }
        }
    }

SubTipsRegeneraterの書き換え

こちらも内容は同じです。tokenを引数で受け取り、yieldを上記のように書き換えます。

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

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

ScenarioRegeneraterの書き換え

次にこれらのテキストを制御するScenarioRegeneraterの書き換えです。

    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 UniTaskVoidを使用していますが、async UniTaskでも問題なく動作します。違いは調べてもわからなかったのですが、戻り値の参照がない分UniTaskVoidのほうがパフォーマンスがよいという噂です。

不確かな話題はさておき、このタスクでtokenを受け取り、他のタスクに同じものを渡しています。

前回は

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

このようにchapterflagを使用して一文づつ出力していましたが可読性を優先して変更しました。今回の記述はコルーチンでも有効です。

それでは変更点を解説します。

まずyield returnをawaitへと書き換えています。それからtokenが引数として追加されていますのでそれも記述します。

このシステムでは文章を書き込むたびにRegeneraterを呼び出していますが、最後に文章が空になったRegeneraterを呼び出すことでウインドウのクローズ処理を行っています。そのため文は2つづつありますが3回呼び出すことになります。

これはchapterflagで制御していた前回も同じ挙動です。

その他の記述は先程解説したとおりで特に追記はありません。

Start()部分の書き換え

    void Start()
    {
        //トークンの取得をする.
        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();
        //タスクの開始
        ScenarioRegenerater(token).Forget();
    }

Start()にも変更点があります。まずはトークンの取得です。前述したとおりタスクを開始する前に取得します。

次にタスクの開始方法ですが、メソッドのように記述することが出来ます。tokenが引数として渡されています。

末端の.Forget()は非同期処理に関する警告が出てくるのを抑止します。

非同期処理なので他の関数を待たないことが警告されますが、必要ないので.Forget()で無視します。もしかしたらなくても大丈夫なように修正されるかもしれません。

完成したタスク

全体はこうなります。

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

}

コルーチンの置き換えだけなら大きく変えるべきところは無いように思えます。UniTask固有の記述があるのでそこは慣れていくしかありませんね。

まとめ

UniTaskへの置き換えは実は割と苦戦しました。というのもUniTask Ver2へとアップグレードされたことで冒頭に解説したusing関連のゴタゴタやCancellationTokenを使うためにSystem.Threadingの記述に気がつくのに時間を取られてしまったためです。

他にも記述が違うためにあれこれ工夫しようとしたことでかえって複雑になりました。出来上がったものを見ればわかりますが、いじらないで要所だけ書き換えればそれで動くという結果に。

yield breakは最後に悩みましたが、たまたま最後までタスクが遷移すれば完了する仕様だったので、returnで解決しました。

他には待機時間をミリ秒で指定する方法に変わったことで0.001秒が指定できるようになりました。エディターでは速度が変わりましたが、実機ではまだ試していません。

非同期処理とメソッドの待機はゲーム作る上で便利な機能なので今後取り入れていきたいと思います。

以上で第5回TETRY 3Dに用いられる技術を終わります。

ご精読ありがとうございました。

GE Planetでした。



0件のコメントを表示

コメントする