見出し画像

RAYSER進捗(20231203)

アイテム購入時のダイアログ

今回はVContainerとMessagePipeを駆使して、アイテム購入時のダイアログを表示するようにしてみました。

RootLifetimeScopeは特に変更なしです。

using _RAYSER.Scripts.Item;
using _RAYSER.Scripts.Score;
using _RAYSER.Scripts.Weapon;
using BGM.Volume;
using VContainer;
using VContainer.Unity;

namespace _RAYSER.Scripts.VContainer
{
    public class RootLifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            base.Configure(builder);

            // 子のLifetimeScopeに同じVolumeDataを引き渡す
            builder.Register<VolumeData>(Lifetime.Singleton);
            builder.Register<ScoreData>(Lifetime.Singleton);
            builder.Register<ItemAcquisition>(Lifetime.Singleton);
            builder.Register<SubWeaponMounted>(Lifetime.Singleton);
        }
    }
}

ItemLifetimeScopeはダイアログを生成するなどの処理を追加しました。

using _RAYSER.Scripts.Score;
using _RAYSER.Scripts.UI.Dialog;
using _RAYSER.Scripts.UI.Modal;
using _RAYSER.Scripts.UI.Title;
using MessagePipe;
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace _RAYSER.Scripts.Item
{
    /// <summary>
    /// アイテムライフタイムスコープクラス
    /// </summary>
    public class ItemLifetimeScope : LifetimeScope
    {
        [SerializeField] private ItemList itemList;
        [SerializeField] private ItemBuyButton itemButtonPrefab;
        [SerializeField] private Transform itemModalContentTransform;
        [SerializeField] private ItemDialog itemDialog;
        [SerializeField] private Transform itemModalTransform;

        private ItemDialog _itemDialogInstance;

        protected override void Configure(IContainerBuilder builder)
        {
            base.Configure(builder);

            var options = builder.RegisterMessagePipe();
            builder.RegisterMessageBroker<ItemData>(options);
            builder.RegisterMessageBroker<ItemPurchaseSignal>(options);
            builder.RegisterMessageBroker<DialogOpenSignal>(options);
            builder.RegisterMessageBroker<DialogCloseSignal>(options);

            if (itemList != null)
            {
                builder.RegisterInstance(itemList);
            }

            builder.RegisterBuildCallback(container =>
            {
                _itemDialogInstance = Instantiate(itemDialog, itemModalTransform);
                var itemPurchaseSignalPublisher = container.Resolve<IPublisher<ItemPurchaseSignal>>();
                var dialogCloseSignalPublisher = container.Resolve<IPublisher<DialogCloseSignal>>();
                var dialogOpenSignalSubscriber = container.Resolve<ISubscriber<DialogOpenSignal>>();
                var dialogCloaseSignalSubscriber = container.Resolve<ISubscriber<DialogCloseSignal>>();
                _itemDialogInstance.Setup(
                    itemPurchaseSignalPublisher,
                    dialogCloseSignalPublisher,
                    dialogOpenSignalSubscriber,
                    dialogCloaseSignalSubscriber);

                var scoreData = container.Resolve<ScoreData>();
                foreach (var item in itemList.items)
                {
                    var itemBuyButton = Instantiate(itemButtonPrefab, itemModalContentTransform);
                    var publisher = container.Resolve<IPublisher<DialogOpenSignal>>();
                    itemBuyButton.Setup(item, publisher, scoreData, _itemDialogInstance);
                }
            });

            // Entry Point
            builder.RegisterEntryPoint<ItemPurchaseProcessing>();
        }
    }
}

アイテムモーダル(ショップ)で表示するアイテムのボタンの判定で、ScoreDataに更新イベントを持たせて、ボタンはそのイベントを取得して、ボタンの状態を変更するようにしてみました。スコアが不足している時はボタンを無効にしています。
購入確認ダイアログをItemLifetimeScopeの処理で生成するようにしています。こちら生成場所を[SerializeField] private Transform itemModalTransformで定義しています。
またダイアログ表示中はボタンを無効にする処理を追加するなどしています。

using System;

namespace _RAYSER.Scripts.Score
{
    /// <summary>
    /// スコア管理クラス
    /// </summary>
    public class ScoreData
    {
        private int _score;

        /// <summary>
        /// スコア更新イベント
        /// </summary>
        public event Action<int> OnScoreChanged;

        public int GetScore()
        {
            return _score;
        }

        public void SetScore(int score)
        {
            if (_score != score)
            {
                _score = score;
                OnScoreChanged?.Invoke(_score);
            }
        }
    }
}
using System;
using _RAYSER.Scripts.Score;
using _RAYSER.Scripts.UI.Dialog;
using MessagePipe;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using VContainer;

namespace _RAYSER.Scripts.Item
{
    /// <summary>
    /// カスタマイズ画面アイテム購入ボタンUIクラス
    /// </summary>
    public class ItemBuyButton : MonoBehaviour
    {
        [SerializeField] private TextMeshProUGUI itemNameText;
        [SerializeField] private Image thumbnailImage;
        [SerializeField] private TextMeshProUGUI priceText;
        [SerializeField] private Button button;

        private ItemData itemData;
        public int Id { get; set; }

        private IPublisher<DialogOpenSignal> _itemPurchasePublisher;
        private IDisposable _disposable;

        private ScoreData _scoreData;
        private bool isDialogOpen = false;

        public void Setup(
            IItem item,
            IPublisher<DialogOpenSignal> itemPurchasePublisher,
            ScoreData scoreData,
            ItemDialog itemDialog
            )
        {
            _itemPurchasePublisher = itemPurchasePublisher;



            if (item is ItemData data) // セーフキャストを使用
            {
                itemData = data;

                itemNameText.text = itemData.name;
                thumbnailImage.sprite = itemData.iconImage;
                priceText.text = itemData.requiredScore.ToString();

                // スコア変更時にボタンの状態を更新
                _scoreData = scoreData;
                _scoreData.OnScoreChanged += UpdateButtonState;
                UpdateButtonState(scoreData.GetScore());

                itemDialog.OnDialogStateChanged += dialogState =>
                {
                    isDialogOpen = dialogState;
                    UpdateButtonInteractable();
                };

                // 購入ボタン押下時処理
                button.onClick.AddListener(() => OnPurchase(itemData));
            }
            else
            {
                Debug.LogError("提供されたアイテムは ItemData 型ではありません。");
            }
        }

        /// <summary>
        /// 購入ボタン押下時処理
        /// </summary>
        /// <param name="itemData"></param>
        public void OnPurchase(ItemData itemData)
        {
            if (itemData != null)
            {
                _itemPurchasePublisher.Publish(new DialogOpenSignal(itemData));
            }
            else
            {
                Debug.LogError("アイテムデータが設定されていません。");
            }
        }

        /// <summary>
        /// ボタンの有効・無効を更新
        /// </summary>
        private void UpdateButtonInteractable()
        {
            if (button == null)
            {
                Debug.LogError("Button is null.");
                return;
            }

            if (_scoreData == null)
            {
                Debug.LogError("ScoreData is null.");
                return;
            }

            if (itemData == null)
            {
                Debug.LogError("ItemData is null.");
                return;
            }

            button.interactable = !isDialogOpen && (_scoreData.GetScore() >= itemData.requiredScore);
        }

        /// <summary>
        /// スコアの更新メソッドでボタンの状態を更新
        /// </summary>
        /// <param name="currentScore"></param>
        private void UpdateButtonState(int currentScore)
        {
            UpdateButtonInteractable();
        }

        private void OnDestroy()
        {
            if (_scoreData != null)
            {
                _scoreData.OnScoreChanged -= UpdateButtonState;
            }
        }
    }
}
using System;
using _RAYSER.Scripts.Item;
using MessagePipe;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace _RAYSER.Scripts.UI.Dialog
{
    /// <summary>
    /// アイテム購入・キャンセルダイアログクラス
    /// </summary>
    public class ItemDialog : MonoBehaviour, IDisposable
    {
        /// <summary>
        /// 開閉状態変更イベント
        /// </summary>
        public event Action<bool> OnDialogStateChanged;

        private IPublisher<ItemPurchaseSignal> _itemPurchaseProcessingPublisher;
        private IPublisher<DialogCloseSignal> _dialogCloseSignalPublisher;
        private ISubscriber<DialogOpenSignal> _dialogOpenSignalSubscriber;
        private ISubscriber<DialogCloseSignal> _dialogCloseSignalSubscriber;
        private IDisposable _dialogOpenSignalDisposable;
        private IDisposable _dialogCloseSignalDisposable;
        private ItemData _currentItemData;

        [SerializeField] private TextMeshProUGUI itemNameText;
        [SerializeField] private TextMeshProUGUI itemPriceText;
        [SerializeField] private Image itemImage;

        [SerializeField] private Button purchaseButton;
        [SerializeField] private Button closeButton;

        public void Setup(
            IPublisher<ItemPurchaseSignal> itemPurchaseSignalPublisher,
            IPublisher<DialogCloseSignal> dialogCloseSignalPublisher,
            ISubscriber<DialogOpenSignal> openDialogsubscriber,
            ISubscriber<DialogCloseSignal> closeDialogsubscriber)
        {
            _itemPurchaseProcessingPublisher = itemPurchaseSignalPublisher;
            _dialogCloseSignalPublisher = dialogCloseSignalPublisher;
            _dialogOpenSignalSubscriber = openDialogsubscriber;
            _dialogCloseSignalSubscriber = closeDialogsubscriber;

            var d = DisposableBag.CreateBuilder();

            gameObject.SetActive(false);

            purchaseButton.onClick.AddListener(() =>
            {
                if (_currentItemData != null)
                {
                    _itemPurchaseProcessingPublisher.Publish(new ItemPurchaseSignal(_currentItemData));
                    _dialogCloseSignalPublisher.Publish(new DialogCloseSignal());
                }
            });


            closeButton.onClick.AddListener(() => { _dialogCloseSignalPublisher.Publish(new DialogCloseSignal()); });

            _dialogOpenSignalSubscriber
                .Subscribe(signal => { show(signal.Item); }).AddTo(d);
            _dialogOpenSignalDisposable = d.Build();

            _dialogCloseSignalSubscriber
                .Subscribe(signal => { hide(); })
                .AddTo(d);
            _dialogCloseSignalDisposable = d.Build();
        }

        /// <summary>
        /// 購読解除
        /// </summary>
        public void Dispose()
        {
            _dialogOpenSignalDisposable?.Dispose();
            _dialogCloseSignalDisposable?.Dispose();
        }


        private void show(IItem item)
        {
            OnDialogStateChanged?.Invoke(true);

            _currentItemData = item as ItemData; // itemがItemData型であれば、それを_currentItemDataに割り当てる
            gameObject.SetActive(true);
            itemNameText.text = item.name;
            itemPriceText.text = item.requiredScore.ToString();
            itemImage.sprite = item.iconImage;
        }

        private void hide()
        {
            OnDialogStateChanged?.Invoke(false);

            gameObject.SetActive(false);
        }

        private void OnDestroy()
        {
            Dispose();
        }
    }
}

UI周りは思っている以上に、処理が面倒なので、思っている以上に時間がかかりましたが、VContainerとMessagePipeに慣れてくると、ウィンドウ→モーダル→ダイアログといったアイテム情報の受け渡しもかなりシンプルになってきたので、処理の流れさえ把握していれば、VContainerとMessagePipe利用前よりも実装がスマートになってきた気がします。
その代わり今まで便利だと思っていたMonoBehaviourがVContainerとMessagePipeで利用する場合、container.Resolveを使って、生成時に受け渡すようにしないとうまく動かなかったので、その部分を把握するのに時間がかかりました。(公式ドキュメントにも書いてあったのでちゃんと目を通さないとですね。。)

この記事が気に入ったらサポートをしてみませんか?