Unity

Unityでアプリのチュートリアルを作成 -コルーチンを使ったテキスト表示機能-

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

こんにちはGE Planetです。

今回はTETRY 3Dにおいてチュートリアルで使われている、テキストの表示機能をご紹介します。

テキストの表示は簡単にできる部類ですが、RPGのように1文字づつ付け足されていくようなメッセージウインドウになっています。

また説明の合間に実際に操作できるような内容になっているのですが、補足のようなものを表示する小さなウインドウも付けています。

ご紹介するスクリプトはテキスト表示機能が主なものですが、間にそれぞれのアプリの操作用スクリプトを付け足せば気軽にチュートリアルが完成します。

コンテンツ

メインウインドウ部分

まずはメインのテキストを表示する部分を紹介します。

このウインドウが出ている間は画面のどこをタッチしても文字送りができます。そのためゲームの操作は出来ません。

IEnumerator MainTipsRegenerater(string[] sentences)
    {
        while (isEndMessage || sentences == null)
        {
            //アディティブシーンで使う場合はtipsCheckerを1にして
            //読み込んだシーンのStart()などでutilityTextやutilityButtonをセットする.
            switch (tipsChecker)
            {
                case 0:
                    if (!mainWindow.activeSelf)
                    {
                        mainWindow.SetActive(true);
                        yield return new WaitForSeconds(0.5f);

                        //アニメーション用
                        mainAnim.SetBool("onAir",true);
                        yield return new WaitForSeconds(0.5f);
                        utilityText = mainText;
                        utilityButton = nextButton;

                        //tipsCheckerを0にするとisEndMessageがfalseになってメイン部分が動く.
                        isEndMessage = false;
                    }
                    break;
                case 1:
                    tipsChecker = 2;
                    isEndMessage = false;
                    break;
                case 2:
                    break;
            }


            yield return null;
        }

        while (!isEndMessage)
        {
            //1回に表示するメッセージを表示していない	
            if (!isOneMessage)
            {
                //メッセージが全部表示されていたらゲームオブジェクトの非表示
                if (textCount >= sentences.Length)
                {
                    textCount = 0;
                    
                    switch (tipsChecker)
                    {
                        case 0:
                            //アニメーション用
                            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;
                }
                //それ以外はテキスト処理関連を初期化して次の文字から表示させる.

                //テキスト表示時間を経過したら1文字追加する.
                utilityText.text += sentences[textCount][nowTextNum];
                nowTextNum++;

                //メッセージを全部表示、または行数が最大数表示された
                if (nowTextNum >= sentences[textCount].Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            else
            {
                //1回に表示するメッセージを表示した
                yield return new WaitForSeconds(0.5f);

                //Nextボタンを押せるように.
                utilityButton.interactable = true;
                isEndMessage = true;
            }
        }
    }

まず引数としてsentencesというstring配列を持ちます。

ウインドウに表示できる文字数に上限があるため、超えた分は配列で分割します。文字数をカウントして自動で次のウインドウへ割り当てる方法も検討しましたが、多言語だとあまりうまく機能しなかったため、手動で文字のオーバーフローを調整しています。テキスト量が少ないためこのような方法になりました。

isEndMessageはメッセージの終了フラグです。falseのときはsentencesにまだ未表示のテキストがある状態になります。

最初にisEndMessageがtrueの状態で動きます。tipsCheckerの値を変えることで動作を制御することが出来ます。初期値は0でsentencesがnullでないならメインウインドウをアクティブにして操作するテキストオブジェクトとボタンを設定します。

tipsCheckerの主な使いみちは、Additive Sceneを用いたUIが登場する場合です。Additive Sceneが読み込まれるとその下のシーンは干渉出来なくなるため、Additive Sceneにもテキストウィンドウなどを用意します。

そのためutilityTextなどへ登録する際にAdditive SceneのStart()部分などに処理を記述して、MainTipsRegeneraterはtipsCheckerを1にして開始させることでutilityな変数が上書きされることを防ぎます。

そしてisEndMessageがfalseになって、テキスト表示機能が動き始めます。

isOneMessageはsentencesの1要素が表示完了したかどうかを監視しています。

if (textCount >= sentences.Length)の部分はsentencesの全要素が表示された場合の処理です。ここでもtipsCheckerがありますが、0であればウインドウのアニメーションと非アクティブ化をします。それ以外の場合はAdditive Sceneを閉じることで解決するようにしています。

chapterflagが出てきましたがこれはテキストを任意のタイミングで表示するためのフラグです。後述するチュートリアル作成用の関数で勝手にテキストが流れていかないようにするものです。

chapterflagがfalseの時にテキスト表示用関数が動きます。これはテキスト終了時に自動でtrueとなります。

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

この部分でテキストを1文字づつ表示しています。nowTextNumが文字数です。

textCountは後述するNextボタンを押した時にカウントアップされます。

if (nowTextNum >= sentences[textCount].Length)の部分で各要素の最大文字数を監視して、isOneMessageのフラグを動かします。

ここまでの流れを最後に0.01秒のウエイトで処理しています。端末の処理速度によっては表示が遅くなる可能性があるため実機で確認するほうが良い気がします。

isOneMessageがtrueになったら一旦isEndMessageもtrueにしてNextボタンを押せる状態にします。

次にNextボタンを見ます。

    public void NextButton()
    {
        //Nextボタンを押す時にメッセージ機能の初期化をする.
        utilityButton.interactable = false;
        utilityText.text = "";
        nowTextNum = 0;
        //複数回連続で表示するときはText数をカウントする.
        textCount++;
        isOneMessage = false;
        isEndMessage = false;
    }

ボタンはあらかじめインスペクターでinteractableをfalseにしておいてください。同様にこの親であるメインテキスト用のオブジェクトは初期状態で非アクティブとなるようにチェックを外しておきます。

ボタンを押すことでテキスト、文字数の初期化とtextCountの加算、そして各種フラグの再設定が行われて、次の文が表示されます。

サブウインドウの部分

次はサブウインドウの説明です。

サブウインドウはチュートリアルで操作中のプレイヤーへの補足等に使用できます。操作のヒントなどを表示する機能です。

IEnumerator SubTipsRegenerater(string sentence, int tar)
    {
        //サブボックスを表示する.
        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);

        //アニメーション用
        subAnim.SetBool("onAir", true);
        yield return new WaitForSeconds(0.5f);

        isEndMessage = false;

        while (!isEndMessage)
        {
            //1回に表示するメッセージを表示していない	
            if (!isOneMessage)
            {
                //テキスト表示時間を経過したら1文字追加する.
                subText.text += sentence[nowTextNum];
                nowTextNum++;

                //メッセージを全部表示、または行数が最大数表示された
                if (nowTextNum >= sentence.Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            //1回に表示するメッセージを表示した
            else
            {
                yield return new WaitForSeconds(0.5f);

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

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

        //アニメーション用
        subAnim.SetBool("onAir", false);
        chapterflag = true;
        yield return new WaitForSeconds(0.5f);
        subWindow.SetActive(false);
    }

SubTipsRegeneraterにはテキストとイメージを表示する機能があります。ボタンの画像などを入れられます。

画像もテキスト同様配列で準備しておきます。表示する画像をintで指定します。

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

今回はResourcesから読み込んでいます。

まずはメイン同様にオブジェクトをアクティブにします。初期状態は非アクティブにしておいてください。

画像の透明度を変更したい場合はcolorの4番目の引数を変更します。

主な仕組みはメインと同じですが、表示中もプレイヤーの操作を可能にするため、表示終了タイミングは別のコルーチンで制御します。

EndSubTips()を実行することでサブウインドウを終了させられます。

サブウインドウはテキストの連続表示ができないので、引数にはstringを単体でいれます。

チュートリアル本体

これらのコルーチンを組み合わせてチュートリアルを作ります。

その本体となるのが以下のコルーチンです。

 public IEnumerator ScenarioRegenerater()
    {
        //=====================================//
        //                                     //
        // ここにチュートリアルの手順を記述します. //
        //                                     //
        //=====================================//

        //例
        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();
    }

例として記述してみました。GitHubに上げたUnityPackageから見ることが出来ます。

MainTipsRegeneraterはwhile文で囲んで使用します。chapterflagがfalseの状態で1文ずつループさせます。

次のテキストを表示する時に毎回chapterflag = falseを記述します。SubTipsRegeneraterは単体のテキストなのでそのまま呼び出します。

このコルーチンの中にゲームのボタンの制御やステージの状態変化などを記述してチュートリアルを完成させてください。

SubTipsRegeneraterは任意のタイミングで表示を終了できます。ですがメインウインドウと同時に出力出来ない仕様なので、次にMainTipsRegeneraterを呼び出す前にEndSubTips()を実行しておいてください。

文字数カウントなどの変数をメインとサブで別々にすれば同時に出力が可能になります。

使い方

それではまず全体のスクリプトを見ます。

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

public class TutorialSystem : MonoBehaviour
{
    //メッセージボックス本体
    public GameObject mainWindow;
    //サブボックス本体
    public GameObject subWindow;
    //ウインドウを動かして表示するならば
    public Animator mainAnim;
    public Animator subAnim;
    //メッセージボックスのText
    public Text mainText;
    //サブボックスのText
    public Text subText;
    //実際に処理するText
    public Text utilityText;
    //サブボックス用Image
    public Image tagetImage;
    //使用するSprite. 多ければ配列にする.
    private Sprite[] targets = new Sprite[2];
    //アディティブシーン間で使うフラグ
    private int tipsChecker = 0;
    //メインボックス用Nextボタン
    public Button nextButton;
    //実際に使うNextボタン
    public Button utilityButton;
    //テキスト保存用クラス
    public tutorialTexts tutoText;
    //チュートリアル進行用
    private bool chapterflag = false;
    //現在のメッセージ番号
    private int textCount;
    //今見ている文字番号
    private int nowTextNum = 0;
    //1回分のメッセージを表示したかどうか
    private bool isOneMessage = false;
    //メッセージをすべて表示したかどうか
    private bool isEndMessage = true;


    // Start is called before the first frame update
    void Start()
    {
        //チュートリアルで無効化したいボタンなどがあれば処理を入れる.

        //サブボックスにImageを入れる場合.
        SetTargets();
        StartCoroutine(ScenarioRegenerater());
    }

    public IEnumerator ScenarioRegenerater()
    {
        //=====================================//
        //                                     //
        // ここにチュートリアルの手順を記述します. //
        //                                     //
        //=====================================//

        //例
        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)
        {
            //アディティブシーンで使う場合はtipsCheckerを1にして
            //読み込んだシーンのStart()などでutilityTextやutilityButtonをセットする.
            switch (tipsChecker)
            {
                case 0:
                    if (!mainWindow.activeSelf)
                    {
                        mainWindow.SetActive(true);
                        yield return new WaitForSeconds(0.5f);

                        //アニメーション用
                        mainAnim.SetBool("onAir",true);
                        yield return new WaitForSeconds(0.5f);
                        utilityText = mainText;
                        utilityButton = nextButton;

                        //tipsCheckerを0にするとisEndMessageがfalseになってメイン部分が動く.
                        isEndMessage = false;
                    }
                    break;
                case 1:
                    tipsChecker = 2;
                    isEndMessage = false;
                    break;
                case 2:
                    break;
            }


            yield return null;
        }

        while (!isEndMessage)
        {
            //1回に表示するメッセージを表示していない	
            if (!isOneMessage)
            {
                //メッセージが全部表示されていたらゲームオブジェクトの非表示
                if (textCount >= sentences.Length)
                {
                    textCount = 0;
                    
                    switch (tipsChecker)
                    {
                        case 0:
                            //アニメーション用
                            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;
                }
                //それ以外はテキスト処理関連を初期化して次の文字から表示させる.

                //テキスト表示時間を経過したら1文字追加する.
                utilityText.text += sentences[textCount][nowTextNum];
                nowTextNum++;

                //メッセージを全部表示、または行数が最大数表示された
                if (nowTextNum >= sentences[textCount].Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            else
            {
                //1回に表示するメッセージを表示した
                yield return new WaitForSeconds(0.5f);

                //Nextボタンを押せるように.
                utilityButton.interactable = true;
                isEndMessage = true;
            }
        }
    }

    IEnumerator SubTipsRegenerater(string sentence, int tar)
    {
        //サブボックスを表示する.
        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);

        //アニメーション用
        subAnim.SetBool("onAir", true);
        yield return new WaitForSeconds(0.5f);

        isEndMessage = false;

        while (!isEndMessage)
        {
            //1回に表示するメッセージを表示していない	
            if (!isOneMessage)
            {
                //テキスト表示時間を経過したら1文字追加する.
                subText.text += sentence[nowTextNum];
                nowTextNum++;

                //メッセージを全部表示、または行数が最大数表示された
                if (nowTextNum >= sentence.Length)
                {
                    isOneMessage = true;
                }

                yield return new WaitForSeconds(0.01f);
            }
            //1回に表示するメッセージを表示した
            else
            {
                yield return new WaitForSeconds(0.5f);

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

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

        //アニメーション用
        subAnim.SetBool("onAir", false);
        chapterflag = true;
        yield return new WaitForSeconds(0.5f);
        subWindow.SetActive(false);
    }

    public void NextButton()
    {
        //Nextボタンを押す時にメッセージ機能の初期化をする.
        utilityButton.interactable = false;
        utilityText.text = "";
        nowTextNum = 0;
        //複数回連続で表示するときはText数をカウントする.
        textCount++;
        isOneMessage = false;
        isEndMessage = false;
    }

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

テキスト保存用のクラスでtutorialTexts tutoTextがあります。以下に表示します。

using UnityEngine;

public class tutorialTexts : MonoBehaviour
{
    //表示するテキストを格納する.
    //一度に表示できる量を配列にそれぞれ入れる.
    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[]{
            "こんにちはGE Planetです。",
            "チュートリアルのテキスト表示実験中です。",
        };
    }

    void SetB()
    {
        mainsentencesB = new string[]{
            "サブブロックにはTipsなどを入れられます。",
            "テキストが多い場合は別途テキスト管理クラスを作ります。"
        };

    }

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

    void SetSubB()
    {
        subsentencesB = new string[]
        {
            "Example2",
            "補足も出来ます。"
        };
    }

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

構造はシンプルです。メイン用とサブ用に分けてテキストの配列を作ります。テキスト数が多いノベルなどで使用する場合は別途管理用クラスを作るほうが良いと思います。

さて変数の説明ですが、publicな変数の中でもutilityがついたもの以外はインスペクターから登録してしまっています。

構造としては

このようになっています。

メインウィンドウが「MessageBox」というImageになります。その子として「MainMessage」というテキスト部分と、「NextButton」が配置されています。「NextButton」には「Manager」のコンポーネントである「TutorialSystem」内のNextButton()を登録します。

「NextButton」は画面全体を範囲とできるように空のTextを画面全体に拡大してことして配置しています。Panelでも出来ますがTextのほうがパフォーマンスが良いようです。

「subMessageBox」にはイメージとテキストを子として配置します。

「Manager」という空のオブジェクトに「TutorialSystem」と「tutorialTexts」を取り付けます。

実際に使用するときはゲームマネージャーなどのオブジェクトに付けて、制御すると良いと思います。

まとめ

紹介したチュートリアルテキスト表示機能は簡素なものですが、シンプルなゲームの説明には十分な機能だと思います。

テキスト量が増えればもっと複雑な機能を入れる必要がありますが、まずは手軽に実装したものがこれらになります。

ボタンの制御や各種ゲームの動作などを記述すると煩雑になるため、チュートリアルの内容を細かく分けて、記述すると管理が楽になるかもしれません。

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

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

GE Planetでした。



0件のコメントを表示

コメントする