UniTaskを使ったポップアップの作成

UniTaskを使ったポップアップ(ダイヤログ)の作り方を紹介します。

ポップアップ内での選択結果で処理を分岐するのに適しています。

また、ポップアップのアニメーションは共通化しておき、ポップアップの内容だけ変えたい場合にも使えます。

サンプルアニメーションです。

f:id:tetsujp84:20191210225024g:plain

 

UniTaskを使ったYesOrNoの処理

UniTaksのWhenAnyは何番目のタスクが完了したかを返してくれるので、WhenAnyの結果からどのボタンが押されたかが判断できます。

var isYes = await UniTask.WhenAny(noButton.OnClickAsync(), yesButton.OnClickAsync()) == 1;

noButtonはIndex=0なので押されるとfalseが、yesButtonはindex=1なので押されるとtrueが返ります。

また、複数ボタンにも対応しています。

var index = await UniTask.WhenAny(buttons.Select(b => b.OnClickAsync()).ToArray());

どれか1つを押すと次に進むポップアップはこの仕組みと相性がよいです。

ポップアップでの使われ方イメージ

// 1. ポップアップ生成
var popup = await PopupCreator.CreateAsync<生成するポップアップの型>();
// 2. 表示アニメーション→入力待ち→閉じるアニメーション
var isYes= await popup.ShowAsync();
// 3. ポップアップでの入力を受け取り外で分岐処理
if (isYes) "Yes時の処理~"

awaitすることにより同期的に書けるので、コールバックが不要になります。

// awaitを使わない場合のコールバック例
popup.ShowAsync(() => Yesの処理, () => Noの処理);

// awaitを使った処理
var isYes = await popup.ShowAsync();
if (isYes) Yesの処理~
else Noの処理~

 

実装編

共通クラスの実装

  • PopupCreator

ポップアップ作成クラス

PopupCreator.CreateAsync<生成するポップアップの型>()でポップアップを作る。

using UniRx.Async;
using UnityEngine;

/// <summary>
/// ポップアップ生成クラス
/// </summary>
public static class PopupCreator
{
    /// <summary>
    /// PopupとContentをロードしポップアップを生成する
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static async UniTask<T> CreateAsync<T>() where T : PopupContentBase
    {
        var popupContainer = await Resources.LoadAsync<PopupContainer>("Popup/PopupContainer") as PopupContainer;
        if (!popupContainer)
        {
            Debug.LogAssertion("PopupContainerが見つかりません");
            return null;
        }

        var popupInstance = Object.Instantiate(popupContainer);
        var content = await Resources.LoadAsync<T>("Popup/Content/" + typeof(T).Name) as T;
        if (!content)
        {
            Debug.LogAssertion($"{typeof(T).Name}が見つかりません");
            return null;
        }

        var contentInstance = Object.Instantiate(content);
        popupInstance.SetContent(contentInstance);
        return contentInstance;
    }
}
  • PopupContainer

ポップアップの親となるクラス。

ポップアップのフレーム部分に相当。

using UnityEngine;

/// <summary>
/// ポップアップの格納、フレーム部分
/// アニメーションへの参照と要素の紐づけを行う
/// </summary>
public class PopupContainer : MonoBehaviour
{
    [SerializeField] private PopupAnimator animator;

    public PopupAnimator PopupAnimator => animator;
    
    /// <summary>
    /// ポップアップの要素を設定する
    /// </summary>
    /// <param name="content"></param>
    public void SetContent(PopupContentBase content)
    {
        content.transform.SetParent(animator.transform, false);
        content.Initialize(this);
    }
}
  • PopupAnimator

ポップアップ開閉時のアニメーションクラス。

Animation/Animatorでアニメーションしてもいいし、DoTweenなどのスクリプトでアニメーションしてもよいです。 実際はインターフェースのほうがいいかも。

ShowとHideがawait可能なように記述することでアニメーションが終わるまで次の処理を待つことができます。

サンプルとしてAnimatorでShowとHideのアニメーションを設定しています。

using UniRx.Async;
using UnityEngine;

/// <summary>
/// ポップアップアニメーション
/// </summary>
public class PopupAnimator : MonoBehaviour
{
    // ShowPopupとHidePopupのアニメーションが設定されている
    [SerializeField] private Animator animator;

    public async UniTask PlayAnimationAsync()
    {
        animator.Play("ShowPopup");
        await UniTask.WaitUntil(IsFinish);
    }

    public async UniTask HideAnimationAsync()
    {
        animator.Play("HidePopup");
        await UniTask.WaitUntil(IsFinish);
    }

    private bool IsFinish()
    {
        return animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1;
    }
}
  • PopupContentBase

ポップアップの中身に相当し、各ポップアップごとにPopupContentBaseを継承してプレハブとして作ります。

using UniRx.Async;
using UnityEngine;

/// <summary>
/// ポップアップの中身
/// 実際は継承して使われる
/// </summary>
public abstract class PopupContentBase : MonoBehaviour
{
    private PopupContainer container;
    
    /// <summary>
    /// 初期化
    /// </summary>
    /// <param name="container"></param>
    public void Initialize(PopupContainer container)
    {
        this.container = container;
    }
    
    /// <summary>
    /// 表示する
    /// </summary>
    /// <returns></returns>
    protected async UniTask ShowAnimationAsync()
    {
        await container.PopupAnimator.PlayAnimationAsync();
    }

    /// <summary>
    /// 非表示にする
    /// </summary>
    /// <returns></returns>
    protected async UniTask HideAnimationAsync()
    {
        await container.PopupAnimator.HideAnimationAsync();
        Destroy(container.transform.gameObject);
    }
}

PopupContentの実装例

確認メッセージがあり、Yes or Noで選択するポップアップは以下のようになります。

using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 確認メッセージがあり、Yes or Noで選択するポップアップ
/// </summary>
public class ConfirmPopupContent : PopupContentBase
{
    [SerializeField] private Text _confirmMessage;
    // noが左、yesが右想定
    [SerializeField] private Button noButton;
    [SerializeField] private Button okButton;

    public void Initialize(string message)
    {
        _confirmMessage.text = message;
    }
    
    public async UniTask<bool> ShowAsync()
    {
        await ShowAnimationAsync();
        var isYes = await UniTask.WhenAny(noButton.OnClickAsync(), okButton.OnClickAsync()) == 1;
        await HideAnimationAsync();
        return isYes;
    }
}

使用例

上記のクラスを使ったポップアップ生成までの実装例です。

プレハブの作成

PopupContainerプレハブを以下のように作り、Resources/Popupフォルダ内に設置します。

f:id:tetsujp84:20191210233910p:plain

Animationを担当するオブジェクト。

フェードイン/フェードアウトはCanvasGroupのAlphaを変更することで実現しています。 f:id:tetsujp84:20191210233913p:plain

YesOrNo(Confirm)のポップアップはクラス名とプレハブ名を合わせてResource/Popup/Contentフォルダ内に設置します。 f:id:tetsujp84:20191210233918p:plain

見た目は適当に。

f:id:tetsujp84:20191210233921p:plain

ShowとHideのアニメーションも設定しておきます。 f:id:tetsujp84:20191210233925p:plain

f:id:tetsujp84:20191210233927p:plain

ポップアップの呼び出し

ボタンのOnClickイベントに合わせ以下のように呼び出します。

confirmButton.onClick.AddListener(async () =>
{
    var popup = await PopupCreator.CreateAsync<ConfirmPopupContent>();
    popup.Initialize("ポップアップ確認?");
    var isYes = await popup.ShowAsync();
    if (isYes)
    {
        text.text = "Yesが押された";
    }
    else
    {
        text.text = "Noが押された";
    }
});

これでポップアップ生成→アニメーション→入力待ち→処理の分岐が行えました。

応用編

そのほかのポップアップ

複数選択肢があるポップアップは以下のようになります。

ボタンのラベル変更機能があり、複数ボタンのクリック待ちをLINQでUniTask化していること以外は、ほぼYesOrNoのポップアップと同じです。

using System.Linq;
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// メッセージと複数選択肢があるポップアップ
/// </summary>
public class SelectionPopupContent : PopupContentBase
{
    [SerializeField] private Text confirmMessage;
    [SerializeField] private Button[] buttons;

    public void Initialize(string message, string[] buttonLabels)
    {
        confirmMessage.text = message;
        var length = Mathf.Min(buttons.Length, buttonLabels.Length);
        for (var i = 0; i < length; i++)
        {
            buttons[i].GetComponentInChildren<Text>().text = buttonLabels[i];
        }
    }

    /// <summary>
    /// Yes or Noのポップアップを表示
    /// </summary>
    /// <returns></returns>
    public async UniTask<int> ShowAsync()
    {
        await ShowAnimationAsync();
        var index = await UniTask.WhenAny(buttons.Select(b => b.OnClickAsync()).ToArray());
        await HideAnimationAsync();
        return index;
    }
}

使い方

selectionButton.onClick.AddListener(async () =>
{
    var popup = await PopupCreator.CreateAsync<SelectionPopupContent>();
    var labels = new string[]{"A","B","C"};
    popup.Initialize("選択肢を表示します", labels);
    var index = await popup.ShowAsync(); 
    text.text = $"{labels[index]}ボタンが押された";
});

ポップアップ表示の入力ブロック

アニメーション中にユーザーの入力を禁止したいときは、AnimationでGanvasGroupのInteractableをオフにすると簡単です。

アニメーション終了時に再度interactableをオンにするのを忘れずに。

また、ポップアップ表示中に後ろのUIへの入力を禁止したいときは、Parentオブジェクト(Animatorがアタッチされたオブジェクト)に透明なImageコンポーネントをアタッチすることで可能です。

環境

Unity:Unity2018.4

UniTask:Ver 1.2.0

今回紹介したサンプルはGitHubで公開しています

GitHub