Unityで気軽に使える通知ダイアログを作った

## やりたいこと => 簡単にダイアログ出したい
AndroidのToastとかiOSのAlertみたいに、気軽にユーザになにかを通知できるダイアログが欲しくなったので作りました。

### 要件
 - どこからでも呼べて状態を気にしなくていいstaticなメソッド
 - ダイアログの内容・コールバックを呼ぶときに変更できる
 - 多機能なものは求めてない。できるだけミニマルに

***

### 作り方
#### シーン
まずはこんな感じのダイアログを作ってprefabにします。[^1]
<img width="892" alt="スクリーンショット 2018-09-26 23.29.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/dd14002b-648f-f00e-5797-2b3e80c135ce.png">

ヒエラルキーはまあ適当に。「BackgroundPanel」はダイアログ外のぼんやり灰色っぽくなってる部分です。
<img width="275" alt="スクリーンショット 2018-09-26 23.22.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/9f76225d-3b71-316e-5ed0-565f8f504b10.png">
一番上のCanvasにDialogHandler.csという自作スクリプトを貼って必要なパーツをinspector上で補填します。
ついでにCanvasのSort Orderへ適当に大きな数字を入れて一番上に表示されるようにします。
<img width="488" alt="スクリーンショット 2018-09-26 23.23.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/488d58fd-4c89-288d-1fca-2bb951d62456.png">
完成したらprefabにしてResourcesフォルダに置いときます。
<img width="318" alt="スクリーンショット 2018-09-26 23.24.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/b5e4d261-3619-86ed-e960-e84e79873d99.png">

#### スクリプト
```c#:DialogHandler.cs
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class DialogHandler : MonoBehaviour
{
    [SerializeField] private Text _title;
    [SerializeField] private Text _description;

    [SerializeField] public Button _okButton;
    [SerializeField] public Button _ngButton;
    public UnityAction onDestroyed;

    [SerializeField] private Image _background;

    private static readonly string PREFAB_NAME = "DialogCanvasPrefab";
    private static GameObject prefab;


    public static DialogHandler ShowDialog(
        string title, string description, string ok = null, string ng = null
    )
    {
        if (prefab == null)
        {
            prefab = Resources.Load(PREFAB_NAME) as GameObject;
        }

        var instance = Instantiate(prefab);
        var handler = instance.GetComponent<DialogHandler>();

        handler._title.text = title;
        handler._description.text = description;

        if (string.IsNullOrEmpty(ok))
        {
            Destroy(handler._okButton.gameObject);
            handler._okButton = null;
        }
        else
        {
            handler._okButton.GetComponentInChildren<Text>().text = ok;
            handler._okButton.onClick.AddListener(() => Destroy(handler.gameObject));
        }

        if (string.IsNullOrEmpty(ng))
        {
            Destroy(handler._ngButton.gameObject);
            handler._ngButton = null;
        }
        else
        {
            handler._ngButton.GetComponentInChildren<Text>().text = ng;
            handler._ngButton.onClick.AddListener(() => Destroy(handler.gameObject));
        }

        return handler;
    }

    void Start()
    {
        var eventTrigger = _background.gameObject.AddComponent<EventTrigger>();
        var entry = new EventTrigger.Entry();
        entry.eventID = EventTriggerType.PointerClick;
        entry.callback.AddListener(eventData => { Destroy(this.gameObject); });
        eventTrigger.triggers.Add(entry);
    }

    private void OnDestroy()
    {
        onDestroyed?.Invoke();
    }
}

```

***

### こんな感じで使う

#### サンプルシーン
<img width="1181" alt="スクリーンショット 2018-09-26 23.36.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/146245/56936703-c077-f4d5-8f84-b60948116b4e.png">

#### サンプルシーン用スクリプト

```c#:SampleSceneManager.cs

using UnityEngine;
using UnityEngine.UI;

public class SampleSceneManager : MonoBehaviour
{
    [SerializeField] private InputField _firstPass;
    [SerializeField] private InputField _secondPass;
    [SerializeField] private Button _submitButton;

    void Start()
    {
        _submitButton.onClick.AddListener(CheckPasswords);
    }

    private void CheckPasswords()
    {
        var first = _firstPass.text;
        var second = _secondPass.text;

        if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second))
        {
            DialogHandler.ShowDialog("入力ミス", "空の入力欄があります！");
            return;
        }

        if (!string.Equals(first, second))
        {
            var sameHandler = DialogHandler.ShowDialog("入力ミス", "入力されているパスワードが違います。ひとつめのものに合わせますか？", "合わせる", "ふたつめ");
            sameHandler._okButton.onClick.AddListener(() => { _secondPass.text = first; });
            sameHandler._ngButton.onClick.AddListener(() => { _firstPass.text = second; });
            return;
        }

        var finishHandler = DialogHandler.ShowDialog("成功", "新しいパスワードが入力されました！");
        finishHandler.onDestroyed += () =>
        {
            _firstPass.text = null;
            _secondPass.text = null;
        };
    }
}

```

#### 動画
![ezgif-3-87923a8823.gif](https://qiita-image-store.s3.amazonaws.com/0/146245/310db7bf-4beb-331f-ffc5-af64f6ea67c7.gif)

***

### 解説

#### DialogHandler.cs

##### フィールド

```c#
    // ダイアログのタイトル
    [SerializeField] private Text _title;
    // ダイアログの説明文
    [SerializeField] private Text _description;

    // OKボタン
    [SerializeField] public Button _okButton;
    // NGボタン
    [SerializeField] public Button _ngButton;
    // ダイアログ終了時イベント
    public UnityAction onDestroyed;

    // ダイアログのまわりの灰色の背景.触られたときにダイアログを消す
    [SerializeField] private Image _background;

    // ダイアログのprefab名
    private static readonly string PREFAB_NAME = "DialogCanvasPrefab";
    // 一応prefabをキャッシュしておく.
    private static GameObject prefab;
```
外部に公開するのは以下の3つ。それぞれにコールバックを登録してもらう。
登録の仕方はgif動画を見ながらSampleSceneManager.cs読んでください。
ボタンのコールバックもUnityActionにしようかちょっと迷いましたが、そっちのほうがめんどくさそうなので直に触る方針。

- _okButton
- _ngButton
- onDestroyed

##### ShowDialog
```c#
// title:ダイアログのタイトル
// description:ダイアログの説明文
// ok:OKボタンの文言
// ng:NGボタンの文言    
// デフォルト引数により,OKボタンとNGボタンの文言を入力しなければ両ボタンは表示されない.入力すれば表示される
public static DialogHandler ShowDialog(
    string title, string description, string ok = null, string ng = null
)
{
    if (prefab == null)
    {
        // static変数に置くべきか,毎回読み込むべきか.
        // ベストプラクティスがよくわからない.
        prefab = Resources.Load(PREFAB_NAME) as GameObject;
    }

    // prefabをInstantiateして,そのインスタンスからこのDialogHandlerを取得する.
    var instance = Instantiate(prefab);
    var handler = instance.GetComponent<DialogHandler>();

    // タイトルと説明文を設定.
    handler._title.text = title;
    handler._description.text = description;
    
    if (string.IsNullOrEmpty(ok))
    {
        // okの文言がなかったらOKボタンを消す.
        // しっかり参照も消す.
        Destroy(handler._okButton.gameObject);
        handler._okButton = null;
    }
    else
    {
        // okの文言があったらボタンにセット.
        // クリックされたらダイアログを消す.
        handler._okButton.GetComponentInChildren<Text>().text = ok;
        handler._okButton.onClick.AddListener(() => Destroy(handler.gameObject));
    }

    // ngもokといっしょ.
    if (string.IsNullOrEmpty(ng))
    {
        Destroy(handler._ngButton.gameObject);
        handler._ngButton = null;
    }
    else
    {
        handler._ngButton.GetComponentInChildren<Text>().text = ng;
        handler._ngButton.onClick.AddListener(() => Destroy(handler.gameObject));
    }

    return handler;
}
```
ダイアログを呼び出すメソッド。ここだけ読んどけばOK。
デフォルト引数によりボタンの文言がセットされていたらボタンを表示、そうでなかったらボタンを出さない。
ちょっと早くなるかな……と思ってprefabをstaticにキャッシュしている。
static変数を使うといじめられる[^2]Android界隈で育ったのでとても抵抗があるのだけれど、毎回読み込むのも良くない気がするし……ベストプラクティスがよくわからない。
[Don't use it.](https://unity3d.com/jp/learn/tutorials/topics/best-practices/resources-folder?playlist=44987)とまで言われたResourcesを使っているが、staticメソッドから手軽にprefab読み込むなら使うしかない気がする。

##### Start
```c#
void Start()
{
    // imageをクリックされたときにアクションを行う.
    // スクリプトで付与.
    // この場合はダイアログ以外の場所を押されたらダイアログを消す.
    var eventTrigger = _background.gameObject.AddComponent<EventTrigger>();
    var entry = new EventTrigger.Entry();
    entry.eventID = EventTriggerType.PointerClick;
    entry.callback.AddListener(eventData => { Destroy(this.gameObject); });
    eventTrigger.triggers.Add(entry);
}
```
inspectorから登録するEventTriggerがなんか使いづらいので、いつもこうやってスクリプトで登録している。
Android育ちの習性としてViewとロジックが分離されてないとムカつくので。でもUnityの思想的には逆な気はする……。

##### OnDestroy
```c#

private void OnDestroy()
{
    // ダイアログ終了イベント.
    onDestroyed?.Invoke();
}
```
そのまんま。

***

### まとめ
要件で求めたものは達成できたかな、と思います。
ただ、UnityとC#始めて2~3ヶ月の自分が1時間くらいで作った代物なので、Unity熟練者から見たらいろいろアレかなーと……。なんか足りてない気がします。なんか。
AssetStoreで配るまでもないこういう簡単util的なものって需要あると思うので「こうしたらいいよ！」みたいなのあったら教えてください。ていうかもっといいやつ作ってくれたら嬉しいです。

おしまい。

#### TODO
- ShowDialog連打されたらどうしようかなーとかめんどくさくて考えてないです。無難にstatic変数でフラグ持っておくとか？

#### 参考
[自分用Unityメモ：EventTriggerにスクリプトからEventを追加する](http://kasatanet.hatenablog.com/entry/2017/11/19/233426)
[【Unity】Event Triggerの種類と用途と使い方【保存版】](http://tech.pjin.jp/blog/2017/09/03/unity_event-trigger/)

[^1]: これを見た知人「相変わらず君カラーリングおかしいね」
[^2]: 「あっこいつメンバ変数staticにしてるー！」「うわダッセー！　はいリジェクトー！」