UniTaskとuGUIを使ってシンプルなUIを作成する

# 概要
下リンクの続きです。
前回よりUIをやや複雑にします。
メニューのボタンの数を４つにして、実際のゲームで使えるようにします。

https://qiita.com/RYA234/items/ba73e6c37f1c39b1aeba
# 開発環境
UIシステム: uGUI
Unity:2021.3.9
IDE:Rider
UniTask:2.3.3
OS:windows10

# 図

## ユースケース
1.メインメニューの各色のボタンを押すと、対応したウィンドウが表示される。
2.ウィンドウ選択時、右クリックすると初期化されウィンドウが消える。
3.ウィンドウのOKボタンを押すとウィンドウは非表示になり、処理が完了する。（今回は何の処理も入れていない。）
4.既に開いていたウィンドウがある上で、他のウィンドウを選択すると、既に開いていたウィンドウは閉じ新しいウィンドウが表示される。

以下gifアニメのような動きを実現します。
![simpleGuiNeo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/2a7e3fe1-6913-33ff-9788-d6b5ef9debf3.gif)



## クラス図
yellow,red,greenはblackのクラスをリネームしただけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/9adbbcfd-e3f2-82fc-3b0a-c52119f0e04b.png)

# 実装

## Presenter
Presenterの役割は以下の２点です。
1.EnityEditor側のボタン、マウスのクリックイベントをbool型に変換する。
 bool型に変換することでGUIとビジネスロジックの分離が容易になる。
2.GameObject類の操作

``` BlackWindowPresenter.cs
using System;
using UnityEngine;
using System.Threading;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
/// <summary>
///  by RYA234
/// </summary>


public class BlackWindowPresenter : MonoBehaviour
{
    [SerializeField] private Button _blackWindowButton;
    [SerializeField] private GameObject _blackWindows;
    [SerializeField] private Button _blackWindowOkButton;
    [SerializeField]  private BlackWindowTask _blackWindowTask;
    private CancellationTokenSource _inputCts;

    private void Start()
    {
        _inputCts = new CancellationTokenSource();
        _blackWindowTask = new BlackWindowTask();
        BlackWindow();
        WaitForBlackInputToBoolean();
        _blackWindows.gameObject.SetActive(false);
    }

    /// <summary>
    ///  This method`role is converting from Input( such as MousePointerDown and KeyButtonDown ) to boolean type,Because of splitting GUI and MainLogic.
    ///  the spiriting help easy to make UnitTest in MainLogic.
    /// </summary>
    private async void WaitForBlackInputToBoolean()
    {
        while (true)
        {
            var result = await UniTask.WhenAny(
                    _blackWindowButton.OnClickAsync( _inputCts.Token),
                    _blackWindowOkButton.OnClickAsync(_inputCts.Token),
                    UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Mouse1), PlayerLoopTiming.Update,
                         _inputCts.Token)
                
                );
                if (result == 0)
                {
                    _blackWindowTask.setIsMenuButton(true);
                    Debug.Log("MenuBlackButtonOnClick make   ");
                }

                if (result == 1)
                {
                    _blackWindowTask.IsBlackOkButton = true;
                    Debug.Log("result Click" + result);
                }

                if (result == 2 && _blackWindowTask.getIsMenuButton())
                {
                    _blackWindowTask.IsCancel = true;
                    Debug.Log("result Click" + result);
                }
        }
    }
    

    /// <summary>
    /// This method is charge of controlling player`s operation.
    ///
    /// <see cref="BlackWindowTaskTest"/>
    /// </summary>
    public async void BlackWindow()
    {
        while (true)
        {
            try
            {
                _blackWindowTask.setCts(new CancellationTokenSource());
                await UniTask.WhenAny(_blackWindowTask.WaitForMenuButton());
                _blackWindows.gameObject.SetActive(true);
                _blackWindowButton.enabled = false;

                await UniTask.WhenAll(_blackWindowTask.WaitForOkButton(), _blackWindowTask.WaitForCancel(),
                    _blackWindowTask.FreeCancellationToken());
                _blackWindows.gameObject.SetActive(false);
                _blackWindowButton.enabled = true;
            }
            catch (OperationCanceledException e)
            {
                 _blackWindows.gameObject.SetActive(false);
                 _blackWindowButton.enabled = true;
            }
        }
    }
}

```
## Task
Taskの役割は以下の2点です。
1.Presenterから変換されたbool値に基づいてビジネスロジックを実行する。
2.他のウィンドウに干渉しないbool値を管理している。（ウィンドウのOKボタン、キャンセル時の初期化方法など）

```BlackWindowTask.cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Script.temp.CencellationCheck;
using UnityEngine;

/// <summary>
/// </summary>
public class BlackWindowTask : MonoBehaviour
{
    private CancellationTokenSource cts = new();

    public bool IsBlackOkButton { get; set; }

    public bool IsCancel { get; set; }

    public void SetCts(CancellationTokenSource cts)
    {
        UIGeneralManager.instance.Cts[(int)TaskEnum.black] = cts;
    }

    public CancellationTokenSource getCts()
    {
        return UIGeneralManager.instance.Cts[(int)TaskEnum.black];
    }

    public void SetIsMenuButton(bool isCancel)
    {
        UIGeneralManager.instance.IsMenuButtons[(int)TaskEnum.black] = isCancel;
    }

    public bool GetIsMenuButton()
    {
        return UIGeneralManager.instance.IsMenuButtons[(int)TaskEnum.black];
    }

    /// <summary>
    ///     This method is Cleaning Method.
    ///     this method does initializing boolean and cts.
    /// </summary>
    /// <returns></returns>
    public async UniTask<bool> FreeCancellationToken()
    {
        try
        {
            await UniTask.WaitUntil(() => UIGeneralManager.instance.Cts[(int)TaskEnum.black].IsCancellationRequested,
                PlayerLoopTiming.Update, UIGeneralManager.instance.Cts[(int)TaskEnum.black].Token);
            return true;
        }
        catch (OperationCanceledException e)
        {
            UIGeneralManager.instance.Cts[(int)TaskEnum.black] = null;
            Debug.Log("CancelationToken is null");
            IsCancel = false;
            IsBlackOkButton = false;
            UIGeneralManager.instance.IsMenuButtons[(int)TaskEnum.black] = false;
            return false;
        }
    }

    public async UniTask<bool> WaitForMenuButton()
    {
        try
        {
            await UniTask.WaitUntil(() => UIGeneralManager.instance.IsMenuButtons[(int)TaskEnum.black],
                PlayerLoopTiming.Update, UIGeneralManager.instance.Cts[(int)TaskEnum.black].Token);
            Debug.Log(UIGeneralManager.instance.checkOtherMenuBottonEnabled(TaskEnum.black));
            return true;
        }
        catch (OperationCanceledException e)
        {
            Debug.Log("BlackMenuButton is Canceled");
            return false;
        }
    }

    public async UniTask<bool> WaitForOkButton()
    {
        try
        {
            await UniTask.WaitUntil(() => IsBlackOkButton, PlayerLoopTiming.Update,
                UIGeneralManager.instance.Cts[(int)TaskEnum.black].Token);
            UIGeneralManager.instance.Cts[(int)TaskEnum.black].Cancel();
            return true;
        }
        catch (OperationCanceledException e)
        {
            Debug.Log("BlackSubMenuOkButton is Canceled");
            return false;
        }
    }

    public async UniTask<bool> WaitForCancel()
    {
        try
        {
            await UniTask.WaitUntil(() => IsCancel, PlayerLoopTiming.Update,
                UIGeneralManager.instance.Cts[(int)TaskEnum.black].Token);
            Debug.Log("Right Click is pressed.");
            UIGeneralManager.instance.Cts[(int)TaskEnum.black].Cancel();
            return true;
        }
        catch (OperationCanceledException e)
        {
            Debug.Log("BlackSubMenu画面においてCancelボタン(右クリック)がキャンセルされました。");
            return false;
        }
    }
}
```
## UIGeneralManager

UIGeneralManagerの役割は以下の２点です。
1.CancellationTokenSource、メニューの押下判定をするboolを一つのクラスに集約する。
　Task,PresenterともUIGeneralManagerの変数を参照している。

2.bool型を入力値として、既に開かれているウィンドウがある場合それのキャンセル処理を行って新しく選択したウィンドウを開く。

### プロダクトコード
``` UIGeneralManager.cs
using System.Threading;
using Script.temp.CencellationCheck;
using UnityEngine;

    /// <summary>
    ///  this role of Class is managing CancellationTokenSource and bool array.
    /// By RYA234
    /// </summary>
    public class UIGeneralManager : MonoBehaviour
    {
        
        // シングルトンを利用
    
        CancellationTokenSource[] _cts = new CancellationTokenSource[4];
        bool[] _isMenuButtons = new bool[4];

        private CancellationTokenSource _inputCts = new CancellationTokenSource();
        public static UIGeneralManager instance;
        private void Start()
        {
   
        }

        public void Awake()
        {
            if (instance == null)
            {
                instance = this;
                DontDestroyOnLoad(gameObject);
            }
            else
            {
                Destroy(gameObject);
            }
            _isMenuButtons = new bool[] { false, false, false,false };
            _cts = new CancellationTokenSource[] { null, null, null,null };
        }
         
        public CancellationTokenSource InputCts
        {
            get => _inputCts;
            set => _inputCts = value;
        }

        public CancellationTokenSource[] Cts
        {
            get => _cts;
            set => _cts = value;
        }

        public bool[] IsMenuButtons
        {
            get => _isMenuButtons;
            set => _isMenuButtons = value;
        }

        public void setCtsAndIsMenuButton(in bool isMenuButton, in CancellationTokenSource cts, TaskEnum taskEnum)
        {
            this.Cts[(int)taskEnum] = cts;
            this.IsMenuButtons[(int)taskEnum] = isMenuButton;
        }

        public UIGeneralManager()
        {
        }

        /// <summary>
        ///  
        /// </summary>
        /// <param name="enumNumber">Clicked Button </param>
        /// <returns>
        /// -1: 
        /// other:
        /// </returns>
        // menuがtrueになったとき、他のSubMenuがtrueになっているかmenuにCancelを実行する。
        public int checkOtherMenuBottonEnabled(TaskEnum taskEnum)
        {
            int index = 0;
            foreach (var menuButton in _isMenuButtons)
            {
                if ((int)taskEnum != index && menuButton)
                {
                    // IsMenuButtons[(int)taskEnum] = true;
                    // IsMenuButtons[index] = false;
                    _cts[index].Cancel();
                    return index;
                }
                index++;
            }
          //  IsMenuButtons[(int)taskEnum] = true;
            return -1;
        }
    }



```


### テストコード

``` UIGeneralManagerTest.cs

using System.Collections;
using System.Threading;
using NUnit.Framework;
using Script.temp.CencellationCheck;
using UnityEngine.TestTools;
using Is = UnityEngine.TestTools.Constraints.Is;


    /// <summary>
    ///
    /// By Rya234
    /// </summary>
    public class UIGeneralManagerTest
    {
        public UIGeneralManager _uiGeneralManager = new UIGeneralManager();
        
        // positive pattern
        // BlackWindowのボタンが押されている時、GreenWindowのボタンを押すと、IsGreenMenuがTrueになり、BlackCtsは初期化される。
        // blackwindowのcts.Cancelが実行される。
        [UnityTest]
        public IEnumerator givenBlackMenuButtonClick_whenGreenMenuButtonClick_thenBlackCtsIsCancel()
        {

            // given
            // in product code,this method  is executed on Start method  in BlackWindowTask Class 
            CancellationTokenSource blackCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(true,blackCts,TaskEnum.black);
            // in product code,this method  is executed on Start method  in YellowWindowTask Class
            CancellationTokenSource yellowCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,yellowCts,TaskEnum.yellow);
            // in product code,this method  is executed on Start method  in RedWindowTask Class
            CancellationTokenSource redCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,redCts,TaskEnum.red);
            // in product code,this method  is executed on Start method  in greenWindowTask Class
            CancellationTokenSource greenCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,greenCts,TaskEnum.green);

            // when do
            int result = _uiGeneralManager.checkOtherMenuBottonEnabled(TaskEnum.green);

            // veriry
            Assert.That(result, Is.EqualTo((int)TaskEnum.black));
 
            // assert Cancellation Requested
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.black].IsCancellationRequested,Is.EqualTo(true));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.yellow].IsCancellationRequested,Is.EqualTo(false));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.red].IsCancellationRequested,Is.EqualTo(false));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.green].IsCancellationRequested,Is.EqualTo(false));
            
            yield return null;
        }
        
        // negative pattern
        // 何もボタンを押していない時、YellowButtonを押すと、YellowMenuが表示される、
        // cts.Cancelは実行されない
        [UnityTest]
        public IEnumerator givenNoButtonClick_whenYellowMenuButtonClick_thenYellowMenu()
        {
            // given
            // in product code,this method  is executed on Start method  in BlackWindowTask Class 
            CancellationTokenSource blackCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,blackCts,TaskEnum.black);
            // in product code,this method  is executed on Start method  in YellowWindowTask Class
            CancellationTokenSource yellowCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,yellowCts,TaskEnum.yellow);
            // in product code,this method  is executed on Start method  in RedWindowTask Class
            CancellationTokenSource redCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,redCts,TaskEnum.red);
            // in product code,this method  is executed on Start method  in GreenWindowTask Class
            CancellationTokenSource greenCts = new CancellationTokenSource();
            _uiGeneralManager.setCtsAndIsMenuButton(false,greenCts,TaskEnum.green);

            // when do
            int result = _uiGeneralManager.checkOtherMenuBottonEnabled(TaskEnum.yellow);
            
            // veriry
            // assert return
            Assert.That(result, Is.EqualTo(-1));

            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.black].IsCancellationRequested,Is.EqualTo(false));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.yellow].IsCancellationRequested,Is.EqualTo(false));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.red].IsCancellationRequested,Is.EqualTo(false));
            Assert.That(_uiGeneralManager.Cts[(int)TaskEnum.green].IsCancellationRequested,Is.EqualTo(false));

            yield return null;
        }
        
    }
    


```
実際のゲームでの再現
positive pattern
![positivepattern.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/3bb31092-03f4-4265-7a16-af3263b557af.gif)


negative pattern
![negativepattern.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/ccba61ff-70cf-766f-1715-9a44d32c8012.gif)