うどんてっくメモ

技術的なメモをまったりと

C#MessageBoxのようなダイアログをコルーチンみたいな感じで作ってみる

この記事はUnityゆるふわサマーアドベントカレンダー 2018の2日目の記事になります
1日目の記事は@splas_boomelangさんのOculusGoのコントローラーをUnity上でエミュレートするでした!

qiita.com

ゲーム開発を行う上で、思わぬ部分でネックになりやすいのがアウトゲームです
中でもゲーム中でもよく表示するダイアログ、ソーシャルゲームを開発する上で数えきれないほど実装することになると思います
どんな場面でも必要になり様々なロジックを担保するダイアログ、この設計が手を抜くと落とし穴(カオスコード)にはまりやすいです
今回はそのダイアログの作りの一つとしてC# MessageBoxを参考にしたダイアログについて説明します


C# MessageBox

C# MessageBoxは.NETFrameworkに搭載されているダイアログ表示用のクラスです

MessageBox クラス (System.Windows.Forms)

Windows民ならよく見るこれを作ってるやつです

f:id:myudon:20180801015734g:plain

MessageBoxでは、ダイアログ自体はロジックを持たず呼び出す側が担保します
呼び出し側がダイアログを展開し、DialogResultで結果待ちをし、その結果を利用してロジックを持つ呼び出し側が処理を行うわけです
聞いてみると普通じゃんって感じがすると思うのですが、アウトゲームを作っていくうえでロジックの所在などをあまり気にせず動くものを作る意識で開発を進めていくと割とダイアログがロジックを持つパターンで実装しがちかなって思います
あまり責務を分散させずにスマートなUI処理がしたいなと思い、これをコルーチンチックに実装してみました

実装

今回紹介するのはシンプルなダイアログのケースです
ダイアログを展開し、ユーザーからのポジティブ/ネガティブな結果を受け取りダイアログをクローズ、処理に移行するという形になります
具体的なケースとしては処理確認系ダイアログが近いでしょう、〇〇しますか?と聞いてはい・いいえで答えるようなやつですね

下記が実装コード、まずはダイアログ本体です
シンプルに形成したものなので各々が使う形に実装をアレンジしていただければと思います
実装の特徴としては簡単なダイアログの実装に加えて待機用のクローズフラグとダイアログのリザルトを含めてあります

public class ExampleDialog : MonoBehaviour {

    /// <summary>
    /// ダイアログそのもののRectTransform
    /// </summary>
    [SerializeField]
    private RectTransform _dialogBody;

    /// <summary>
    /// ダイアログに表示する文章
    /// </summary>
    [SerializeField]
    private Text _description;

    /// <summary>
    /// はいボタン
    /// </summary>
    [SerializeField]
    private Button _positiveButton;

    /// <summary>
    /// いいえボタン
    /// </summary>
    [SerializeField]
    private Button _negativeButton;

    /// <summary>
    /// 処理を終えてクローズしたかの判定
    /// </summary>
    private bool _isClosed;
    public bool IsClosed => _isClosed;

    public DialogResult Result { get; private set; }

    /// <summary>
    /// プレハブからダイアログ作る
    /// </summary>
    /// <param name="parent">吊るす親</param>
    /// <param name="description">ダイアログに表示する文章</param>
    /// <returns>ダイアログのインスタンス</returns>
    public static ExampleDialog CreateDialog(RectTransform parent, string description)
    {
        var dialog = Instantiate(Resources.Load<ExampleDialog>("ExampleDialog"), parent);
        dialog.Initialize(description);
        dialog.Open();
        return dialog;
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    /// <param name="description">ダイアログに表示する文章</param>
    private void Initialize(string description)
    {
        _description.text = description;
        Result = new DialogResult();
    }

    /// <summary>
    /// ダイアログオープンアニメーション
    /// </summary>
    private void Open()
    {
        _dialogBody.DOScale(1f, 1f).OnComplete(() => SetEvent());
    }

    /// <summary>
    /// ダイアログクローズアニメーション
    /// </summary>
    private void Close()
    {
        _dialogBody.DOScale(0f, 1f).OnComplete(() => 
        {
            _isClosed = true;
            Destroy(gameObject);
        });
    }

    /// <summary>
    /// ダイアログボタンのイベント処理、リザルト確定とクローズ
    /// </summary>
    private void SetEvent()
    {
        _positiveButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.POSITIVE;
            Close();
        });

        _negativeButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.NEGATIVE;
            Close();
        });
    }
}

次にダイアログの結果となるクラスです
ここネガポジをリザルトとして定義して持たせていますが、個々のダイアログで得体結果は違ってくるのでそこは応用が必要になってきます
各々のUIの基盤やシステム構想、ダイアログの使用箇所などと相談して工夫を加えると良いでしょう

// ダイアログの結果
public class DialogResult
{
    public enum ResultTypeEnum
    {
        NONE, // 何もなし
        POSITIVE, // はい
        NEGATIVE, // いいえ
    }

    /// <summary>
    /// ダイアログの結果タイプ
    /// </summary>
    public ResultTypeEnum ResultType;

    // ほかに必要そうなリザルトはここに置く、インターフェイスを定義して応用しても良い
}

最後にダイアログのクローズ及び結果を待つカスタムコルーチンです
特に変わった実装はしておらず、シンプルにダイアログのクローズを監視しています
ここもダイアログの仕様などでアレンジが入る箇所ですね

// ダイアログを待つカスタムコルーチン
public class WaitForDialogClose : CustomYieldInstruction
{
    public DialogResult Result => _dialog.Result;

    private ExampleDialog _dialog;

    public override bool keepWaiting
    {
        get
        {
            // ダイアログがクローズするまで待つ
            return !_dialog.IsClosed;
        }
    }

    public WaitForDialogClose(ExampleDialog dialog)
    {
        _dialog = dialog;
    }
}

以上が今回のダイアログのシンプルな形での実装例です
全てのダイアログに対して汎用的に組み込むことができるかと言われると怪しい部分もありますが、臨機応変に応用しましょう


実験

private IEnumerator Test()
{
    var dialog = ExampleDialog.CreateDialog(_canvas, "ほげほげ?");
    var result = new WaitForDialogClose(dialog);

    yield return result;

    Debug.Log(result.Result.ResultType);
}

上記のようなコルーチンを走らせて挙動を見てみます

ダイアログの結果を受け取ることができました、後は受け取った結果を使って処理を行いましょう
また機会があればアウトゲームについてのこういった小さな技術共有をしていきたいと思います