MessageWindowの使い方について

最近は便利なものでChatGPTなどのAIにプログラムを作ってもらうことができる。今回ゲームを作る中で、ユーザー側に質問と選択肢を与えて、その応答によって処理を分岐させるようなプログラムを作成しようと思い、AIにプログラムを作成してもらった。一応所望のものは出てきて、動作も問題なさそうだったが、内容が私にとっては難しく感じたので、ここは一度立ち止まって中身をよく確認し、備忘録として残そうと思う。

コードとオブジェクト設定など

以下が生成されたコードである。

MessageWindow.cs
メッセージとYES, NOの選択肢を持つウィンドウを出現させ、ボタンが押されたら消えるような機能を持つ。以下で何か所か取り上げてコード内容を説明する。

using UnityEngine;
using UnityEngine.UI;
using System;
using UIButton = UnityEngine.UI.Button;//①
using TMPro;
public enum UserChoice
{
    Yes,
    No,
    Cancel,
    None
}
public class MessageWindow : MonoBehaviour
{
    public TMP_Text messageText;
    public UIButton yesButton;
    public UIButton noButton;
    Action<UserChoice> callback;//②
    public void Show(string message, Action<UserChoice> onSelected)
    {
        gameObject.SetActive(true);
        messageText.text = message;
        callback = onSelected;//③
        yesButton.onClick.RemoveAllListeners();//④
        noButton.onClick.RemoveAllListeners();
        yesButton.onClick.AddListener(() => Select(UserChoice.Yes));//⑤
        noButton.onClick.AddListener(() => Select(UserChoice.No));
        
    }
    void Select(UserChoice choice)
    {
        gameObject.SetActive(false);//⑥
        callback?.Invoke(choice);//⑦
    }
}

using UIButton = UnityEngine.UI.Button;

これは、かなり基本的な内容だと思う。UnityにはもともとButtonというクラスがあって、このコードではそのクラスを使いたかったのだが、私が既に独自のButtonクラスを作ってしまっていたのでクラス名の競合が発生してしまった。そういう時には、この例のようにクラス名を再定義してやることができる。このような操作のことをusing エイリアスという。ちなみにエイリアス(alias)は通称とか別名という意味の単語だ。(He is known by the alias “Jack”.)

Action<UserChoice> callback;

これもC#を使う人にとっては常識的な内容だろうか。Action<T> は C#標準のデリゲート型でTは引数である。デリゲート型というのはメソッドそのものを値として扱うための型なので、上記の記述は、UserChoice型の変数1つを引数とする何らかのメソッドを格納できる戻り値無しのデリゲート型変数としてcallbackを定義しているということになる。戻り値ありにしたい場合はActionではなくFuncとする。この変数callbackはコード内では

callback = onSelected;

となっており、onSelectedはShowメソッドの引数である。つまり、Showメソッドを使うとき、ユーザーはYesNoボタンを押したときに実行してほしい任意のメソッドonSelected(引数はUserChoice)を格納しておくことができるのである。

yesButton.onClick.RemoveAllListeners();

これはYesボタンをクリックした時のリスナーを全て削除している。リスナーというのは、ある条件が満たされるまでは待機しておき、その条件が満たされたら処理を実行するような仕組みのことをいう。そして呼び出される処理そのもののことをコールバックという。(リスナーとコールバックはなんとなく似ているが、リスナーは仕組みそのものを指す。)リスナーを全て削除する理由としては、リスナーが複数個蓄積していて、ボタンを押した時に連続で実行されるなどのバグが生じうるため、その対策である。

yesButton.onClick.AddListener(() => Select(UserChoice.Yes));

ここでは、上記と逆にリスナーにコールバック関数を登録している。() => Select(UserChoice.Yes)についてはラムダ式で書かれた無名関数である。したがって、名前のない、引数がない関数をリスナーに登録しており、その関数の中身がSelect(UserChoice.Yes)の実行ということである。yesButton.onClick.AddListener( Select(UserChoice.Yes));
ついこのように書いてしまうと、Select(UserChoice.Yes)がここで即座に実行されて、その戻り値をリスナーに登録するという意味になってしまう。

gameObject.SetActive(false);

これは、MessageWindow.csの実態であるオブジェクトを非表示にしている。これがないと、ウィンドウがいつまでたっても消えない。

callback?.Invoke(choice);

これはユーザーが登録したコールバック関数にchoiceという引数を渡して実行する文である。invokeは引き起こす、呼び出すと言った意味の英語であり(ex. to invoke a memory)デリゲートを実行するための文法である。?はnull条件演算子であり、この場合は デリゲート型の変数であるcallbackがnullでなければ、Invokeメソッドによりその関数を呼び出す、ということである。もし仮に callbackに何も入っていない、すなわちnullなのに呼び出そうとすると、クラッシュしてしまうため、安全策として?をつけて確認してから呼び出しているのである。

上記で説明したクラスMessageWindow.csは以下に示すようなMessageWindowオブジェクトにアタッチする。このオブジェクト自身はPannelであり、子オブジェクトとしてボタンUIと質問テキストを含んでいる。これら子オブジェクトは各メンバーに登録する。

ヒエラルキー
MessageWindowオブジェクトのInspector

使用側

ManagerクラスのInspector

使用側としては下記のコードのようにしてmessageWindowクラスを使用する。

System.Collections.IEnumerator AskBorrowMoney()//①
    {
        bool decided = false;
        messageWindow.Show(
            "質問?",
            choice =>
            {
                userAns = choice;
                decided = true;
            }
        );
        yield return new WaitUntil(() => decided);//②
        //ここに何か処理を書く
    }

System.Collections.IEnumerator AskBorrowMoney()

まずこの関数の型はIEnumeratorである。IEnumeratorのIはインターフェイスであり、Enumeratorは列挙子のことである。列挙子とは集合の中身を一つずつ取り出す役割のことである。したがって配列、リスト、コレクションは全て列挙子である。だから、IEnumeratorは本来は foreachと同じような使い方(というよりforeachを構成している)をするが、ここではメソッドをコルーチンとして動作させるためにIEnumerator型で宣言している。コルーチンというのは処理を途中で止めて再開できるような関数のことである。

yield return new WaitUntil(() => decided

yield は譲るという英単語である。処理を一旦ランタイム(Unity)に譲り、WaitUntilの条件が満たされた時、すなわちdecidedがtrueになった時、処理が再開して、yield returnより下流の処理がなされる。今の例だとこの下流の処理がないので、何のためにコルーチンにしているんだということになるが、本来ここにはユーザーの応答に応じた処理がある。WaitUntilにラムダ式を渡しているのは、そもそもそういう仕様だからというのはあるが、毎フレーム関数() => decidedを呼び出して判定するためである。Unityが毎フレーム関数を呼び出すことで、decidedの変化を検知できる。

投稿者 kumaharakumao

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です