Unityに関連する記事です

追加された行はこの色です。
削除された行はこの色です。

 抽象化設計を推し進めるため、UniTask に用意されている Channel 機能を活用する事例です。

 アイテムなどの情報が一覧表示されているポップアップがあり、
その中にあるアイテムを選択すると確認ポップアップや、詳細説明のポップアップが開くような場合の実装例です。


&ref(https://image01.seesaawiki.jp/i/o/i-school-memo/QSPWt7Wywn.png, 35%)


&ref(https://image02.seesaawiki.jp/i/o/i-school-memo/iEg8ap2jlq.png, 35%)

----

*設計とメリット

 一覧表示用のポップアップ内で、任意のボタンをタップした際、その上に確認用のポップアップが開く構造です。
このとき、確認用のポップアップには、選択したボタンが有している情報(アイテムの名前、価格、性能など)が表示されます。

 そのため挙動としては、一覧表示用のポップアップで選んだ内容が、確認用のポップアップに反映され、
「キャンセル」か「決定」のいずれかを押した場合に確認用のポップアップが閉じます。
「決定」を選択した場合に限り、一覧表示用のポップアップ内でロジックが動く作りです。

 そのため、確認用ポップアップ内の決定ボタン自体は、それ自身が''何を決定しているかは認知していません''。
一覧表示用のポップアップから指示されてその都度確認ポップアップは開きますが、''その結果(キャンセルか、決定かのみ)をフィードバックするという構造''です。
つまり、ボタン本来が持っている''抽象的な構造''を有している形のポップアップになります。

 この抽象的な構造を Channel の機能を応用することで表現できます。

----

*サンプルコード

 サンプルコードは外部ライブラリとして UniRx、UniTask、DOTween を利用しています。
また処理内部ではデリゲートの機能を利用しています。

 またアイテムのデータ管理クラスなどは用意していませんので、適宜、スクリプタブル・オブジェクトなどを活用してデータを用意してください。

----

**生成されるアイテムなどのボタンにアタッチされているクラス

 アイテム用のボタンにアタッチされて、アイテムの画像、データなどを管理します。
他にも任意の情報を追加してください。

 Setup メソッドの Action のメソッドを外部クラスで設定することにより、
アイテムの挙動を外部クラスで指定できる。例えば、アイテムを使うメソッドを紐づければアイテムを利用するボタンになり、
アイテムを捨てるメソッドを紐づければ、アイテムを捨てるボタンになります。

 また今回はアイテムの名称と Index を Action で渡していますが、
アイテムのクラス全体を渡すようにすれば、引数1つで渡すことも可能になります。

 自分のプロジェクトに合わせて調整してください。


[+]
=|BOX|
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.Events;
using UniRx;

/// <summary>
/// アイテム選択用のボタン
/// </summary>
public class SelectItemButton : MonoBehaviour
{
[SerializeField] private Image imgItem;
[SerializeField] private Button btnChooseItem;
[SerializeField] private TMP_Text txtItemName;


/// <summary>
/// 初期設定
/// </summary>
public void SetUp(int itemIndex, UnityAction<string, int> selectAction) {

// Index を利用し、アイテムの情報をスクリプタブル・オブジェクトなどから取得
//ItemData itemData = [取得処理]

// アイテムのアイコン画像、名前を設定
//imgItem.sprite = itemData.sprite;

//txtItemName = itemData.name;

// ボタン設定。ItemSelectPopup の SelectItem メソッドを実行
btnChooseItem.OnClickAsObservable()
.ThrottleFirst(System.TimeSpan.FromSeconds(2.0f))
.Subscribe(_ => selectAction(itemData.name, itemIndex)) // アイテムの名称と Index をわたしている
.AddTo(this);
}
}
||=
[END]

----

**ポップアップのアニメ設定用クラス

 作成後、スクリプタブル・オブジェクトを作成して必要な設定を行ってください。

[+]
=|BOX|
using UnityEngine;
using DG.Tweening;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class PopupAnimSettings : ScriptableObject {
[Header("Open")]
public Ease openEaseType = Ease.OutBack;
public float startScale = 0.8f;
public float openSeconds = 0.6f;

[Header("Close")]
public Ease closeEaseType = Ease.InCirc;
public float closeEndScale = 0.8f;
public float closeSeconds = 0.6f;

#if UNITY_EDITOR
[MenuItem("Assets/Create/PopupAnimSettings")]
public static void CreateInstance() {
PopupAnimSettings obj = ScriptableObject.CreateInstance<PopupAnimSettings>();
Generic.ScriptableObjectCreator.Create<PopupAnimSettings>(obj, name: "NewPopupAnimSettings");
}
#endif
}
||=
[END]

----

 作成したスクリプタブル・オブジェクトの参考画像です。


&ref(https://image01.seesaawiki.jp/i/o/i-school-memo/ED6cbuDdzz.png, 35%)

----

**ポップアップ用の親クラス

 popupAnimSettings 変数には先ほど作成した PopupAnimSettings スクリプタブル・オブジェクトをアサインします。

 参考までにコルーチンの処理もコメントアウトして残してあります。

[+]
=|BOX|
using Cysharp.Threading.Tasks;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public abstract class PopupBase : MonoBehaviour {

[Header("親スクリプタルオブジェクト")]
[SerializeField, Tooltip("アニメーション")] public PopupAnimSettings popupAnimSettings;

[Space(2),Header("親クラスオブジェクト")]
[SerializeField, Tooltip("タップで閉じるフィルター")] public Image filter;
[SerializeField, Tooltip("拡縮するための背景")]public Transform bg;
[SerializeField, Tooltip("ポップアップの表示/非表示用")]public Canvas Popup;

[Space(2),Header("親クラス変数")]
// 背景を押したら画面を閉じる機能ON/OFF
[SerializeField,Tooltip("背景フィルターを押したら閉じる機能")] public bool isCloseFilter;
// ポップアップを開く時閉じる時、アニメーションをする機能ON/OFF
[SerializeField, Tooltip("ポップアップを開く時閉じる時、アニメーションをする機能")] public bool isPopupAnime;
// ポップアップを閉じるとき時、削除する機能ON/OFF
[SerializeField, Tooltip("ポップアップを閉じるとき時、削除する機能")] public bool isDestroy;
protected bool isClickable; //クリックをしたかを判定する機能

[SerializeField,Tooltip("アルファ値の設定")] public float filterAplha;
Color filterBaseColor;

/// <summary>
/// 外部からInstantiateと同時にSetInitialize()を呼ぶ流れにする。Start()は使わない予定
/// </summary>
public virtual void InitializePopup() {
filterBaseColor = filter.color;
if (isCloseFilter) {
// フィルタ―をクリックで閉じれるようにする
Button closeButton = filter.gameObject.AddComponent<Button>();
closeButton.transition = Selectable.Transition.None;
filter.gameObject.GetComponent<Button>().onClick.AddListener(async () =>await ClosePopupProc());
}
if (filterAplha == 0f) {
filterAplha = filter.color.a;
}
filter.color = new Color(filter.color.r, filter.color.g, filter.color.b, 0f);
bg.localScale = Vector3.one * popupAnimSettings.startScale;
// ポップアップウィンドウを表示する
}


/// <summary>
/// ポップアップウィンドウを表示する処理の分岐
/// </summary>
public virtual async UniTask OpenPopupProc() {
await CommonOpenPopupProc();
}

/// <summary>
/// ポップアップウィンドウを表示する処理の分岐
/// UnityActionの引数有
/// </summary>
/// <param name="popupAction"></param>
/// <returns></returns>
public virtual async UniTask OpenPopupProc(UnityAction popupAction) {
await CommonOpenPopupProc();
}

private async UniTask CommonOpenPopupProc()
{
if (isClickable) {
return;
}
// ポップアップの表示をONにする
Popup.enabled = true;
isClickable = true;

// PopupAnimeFlgによってアニメーションするかしないかを分ける
if (isPopupAnime) {
await OpenPopup();
//yield return StartCoroutine(OpenPopup());
}
else {
OpenPopupWithoutAnimation();
}
}

/// <summary>
/// ポップアップウィンドウ表示(アニメ無し)
/// </summary>
protected void OpenPopupWithoutAnimation() {
bg.localScale = Vector3.one;
filter.color = filterBaseColor;
}

/// <summary>
/// ポップアップウィンドウ表示(アニメ有り)
/// </summary>
protected async UniTask OpenPopup() {
filter.DOFade(filterAplha, popupAnimSettings.openSeconds);
bg.DOScale(Vector3.one, popupAnimSettings.openSeconds).SetEase(popupAnimSettings.openEaseType);
await UniTask.Delay(System.TimeSpan.FromSeconds(popupAnimSettings.openSeconds));
//yield return new WaitForSeconds(popupAnimSettings.openSeconds);
}

/// <summary>
/// ポップアップウィンドウを閉じる処理の分岐
/// </summary>
public virtual async UniTask ClosePopupProc(bool isSe = true) {

// PopupAnimeFlgによってアニメーションするかしないかを分ける
if (isClickable) {
isClickable = false;
if(isSe) {
//SoundManager.Instance.PlaySE(SoundManager.ENUM_SE.BtnNO);
}
if (isPopupAnime) {
await ClosePopup();
//StartCoroutine(ClosePopup());
} else {
ClosePopupWithoutAnimation();
}
}

// アドモブの表示
PopupManager.instance.AdmobView(this);
}

/// <summary>
/// ポップアップウィンドウ閉じる(アニメ無し)
/// </summary>
protected void ClosePopupWithoutAnimation() {
bg.localScale = Vector3.zero;

// DestroyFalgによって削除するかしないかを分ける
if (isDestroy)
{
Destroy(gameObject);
}
else
{
Popup.enabled = false;
}
}

/// <summary>
/// ポップアップウィンドウ閉じる(アニメ有り)
/// </summary>
public async UniTask ClosePopup() {
filter.DOFade(0, popupAnimSettings.closeSeconds);
bg.DOScale(Vector3.one * popupAnimSettings.closeEndScale, popupAnimSettings.closeSeconds).SetEase(popupAnimSettings.closeEaseType);
await UniTask.Delay(System.TimeSpan.FromSeconds(popupAnimSettings.closeSeconds));
//yield return new WaitForSeconds(popupAnimSettings.closeSeconds);

// DestroyFalgによって削除するかしないかを分ける
if (isDestroy)
{
Destroy(gameObject);
}
else
{
Popup.enabled = false;
}
}
}
||=
[END]


----

**確認用のポップアップ

 選択したオブジェクトの用途に合わせた確認用の処理を行います。
例えばアイテムを選択したのであれば、それを利用する確認時、廃棄の確認時、など確認が必要な場面に利用します。

 ポイントは、このポップアップ内のボタンは、「可否」を通知する機能のみを有している状態にとどめていることです。
これにより、その先の処理である「アイテムを使ったときの処理」や「破棄する処理」など、ボタンがつながる処理は知らない状態です。

 Channel を使うことで多様な入力方式に備えつつ、どのような方式でも、どのような結果でも一括して await confirmationChannel.Reader.ReadAsync(); で待機できるようにしています。

[+]
=|BOX|
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Events;
using UniRx;
using Cysharp.Threading.Tasks;

public class ConfirmPopup : PopupBase
{
[SerializeField] private TMP_Text txtConfirmMessage;
[SerializeField] private Button btnCancel;
[SerializeField] private Button btnSubmit;
[SerializeField] private CanvasGroup canvasGroup;

private Channel<bool> confirmationChannel = default;


/// <summary>
/// 確認用ポップアップの設定
/// </summary>
public void SetupConfirmPopup() {

isDestroy = false;
canvasGroup.blocksRaycasts = false;

// キャンセルボタンの設定
btnCancel.OnClickAsObservable()
.ThrottleFirst(System.TimeSpan.FromSeconds(2.0f))
.Subscribe(async _ =>
{
// false で Channel のキューに値を書き込む => 結果通知
confirmationChannel.Writer.TryWrite(false);
await ClosePopupProc();
})
.AddTo(this);

// 確認(決定)ボタンの設定
btnSubmit.OnClickAsObservable()
.ThrottleFirst(System.TimeSpan.FromSeconds(2.0f))
.Subscribe(async _ => {
// true で Channel のキューに値を書き込む => 結果通知
confirmationChannel.Writer.TryWrite(true);
await ClosePopupProc();
})
.AddTo(this);
}

/// <summary>
/// 確認ポップアップを開き、どのボタンの押下したか通知
/// </summary>
/// <param name="confirmMessage"></param>
/// <returns></returns>
public async UniTask<bool> OpenConfirmPopup(string confirmMessage) {

canvasGroup.blocksRaycasts = true;

// 女神の名前 + 確認の表示
txtConfirmMessage.text = confirmMessage;

// 初期化
confirmationChannel = Channel.CreateSingleConsumerUnbounded<bool>();

await OpenPopupProc();

// ボタンの押下結果を待機(Channel のキューに値が入っていたら1つ取り出すまで待機)
bool result = await confirmationChannel.Reader.ReadAsync();

// 書き込み終了
confirmationChannel.Writer.TryComplete();

// 選択したボタンの結果を戻す(確認なら true、キャンセルなら false)
return result;
}

public override UniTask ClosePopupProc(bool isSe = true) {
canvasGroup.blocksRaycasts = false;
return base.ClosePopupProc(isSe);
}
}
||=
[END]

----

**一覧表示用のポップアップ

 アイテムなどのインベントリや、ショップなどのオブジェクトを一覧表示用のポップアップの作成例です。

 ポップアップ内において、それらのボタンオブジェクトを生成し、デリゲートを利用して実行したいメソッドをボタン側に提供しています。
この設計により、ボタン側はこの一覧表示用のポップアップを知る必要はなく、押下した際に一覧表示用ポップアップ内に用意したメソッドを実行できます。

 UniTask を活用することで、確認用ポップアップ内のボタンの押下処理の結果を取得できるまで待機し、
その結果を受けて処理の分岐や、選択したボタンに応じた処理につなげていく処理を書きやすくしています。


[+]
=|BOX|
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System.Collections.Generic;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class ItemSelectPopup : PopupBase
{
[SerializeField] private Button btnClose;

[SerializeField] private SelectItemButton SelectItemButtonPrefab;
[SerializeField] private Transform SelectItemButtonTran;
[SerializeField] private List<SelectItemButton> SelectItemButtonList = new();

[SerializeField] private ConfirmPopup confirmPopup;


/// <summary>
/// 初期設定
/// </summary>
public override void InitializePopup() {

isDestroy = false;

// 閉じるボタンの設定
btnClose.OnClickAsObservable()
.ThrottleFirst(System.TimeSpan.FromSeconds(2.0f))
.Subscribe(async _ => await ClosePopupProc())
.AddTo(this);

// 選択したアイテムの確認ポップアップの設定
confirmPopup.SetupConfirmPopup();

// アイテム選択ボタンの生成
GenerateSelectItemButtons();

Debug.Log("設定完了");
}

/// <summary>
/// 女神選択ボタンの生成
/// </summary>
private void GenerateSelectItemButtons() {

for (int i = 0; i < [所持しているアイテムなどの List].Count; i++) {

SelectItemButton SelectItemButton = Instantiate(SelectItemButtonPrefab, SelectItemButtonTran, false);

// 引数として SelectItem メソッドを渡す。これにより、ボタン側は一覧表示用ポップアップを知らずに SelectItem メソッドを実行できる
SelectItemButton.SetUp([所持しているアイテムなどの List].itemIndex, SelectItem);
}
}

/// <summary>
/// アイテム選択ボタンを押すと実行される
/// デリゲートでは非同期処理ができないので、一旦ここを呼んでから非同期処理を動かす
/// </summary>
/// <param name="itemIndex"></param>
/// <param name="bodyId"></param>
private void SelectItem(string itemName, int itemIndex) {

// 非同期処理を開始
_ = SelectItemAsync(itemName, ItemIndex);
}

/// <summary>
/// アイテム選択ボタンを押すと実行される
/// 選択したアイテムの確認ポップアップを開いて、ボタンの押下を待機
/// </summary>
/// <returns></returns>
private async UniTask SelectItemAsync(string itemName, int itemIndex) {

// 確認ポップアップを開いて、ボタンの押下を待機
bool buttonResult = await confirmPopup.OpenConfirmPopup(itemName);

// ☆ UniTask と Channel を組み合わせることで、非同期処理ながらも処理を上から順番に読み解くことが可能。可読性の高い処理と作りやすい処理が実現できている ☆

// 確認ボタンを押している場合
if(buttonResult) {

// アイテムの利用、破棄など、用途に応じた処理を書く

}

// ウインドウ閉じる
ClosePopupProc().Forget();
}
}
||=
[END]

----

**処理の説明

 アイテムなどの選択用のボタンを4つ(実際には複数個)生成し、それにデリゲートとして SelectItem メソッドを渡すことで、ボタン側と一覧表示用ポップアップ側の直接の依存関係を断っています。
その後、選択のボタンを押すと SelectItem メソッドが動き、併せて、選択されたボタンの情報が渡ってきます。

 ここで確認用ポップアップを開き、ポップアップ内にある「決定」「キャンセル」のいずれかのボタンが押されるまで待機します。
ほかの処理が止まるので、ゲーム内の処理を止め、ボタンの選択を待てる状況で使う前提です。

 SetupConfirmPopup メソッドで''ボタン自体の役割を設定しつつ、その結果が何に使われるかは認知せずに一覧表示用ポップアップ側に押したボタンの状態(決定かキャンセルか)フィードバック''します。

ConfirmPopup.cs 64 行目
=|PERL|
// ボタンの押下結果を待機(Channel のキューに値が入っていたら1つ取り出すまで待機)
bool result = await confirmationChannel.Reader.ReadAsync();
||=

 この ReadAsync を使って Channel に書き込むタイミングを非同期で待っているのですが、
今回の場合、''ボタン押下時に書きこみをリンクさせているので、ボタンを押すまで待機する、という挙動''になっています。

管理人/副管理人のみ編集できます