🔥

【Unity】Visitorパターンを用いたボムの実装

2024/03/24に公開

Visitorパターンをボム実装に使用した理由について

こちら前回サブウェポンの実装でVisitorパターンを使い、用途にマッチしたため、今回のボムの実装でも同様に使用してみることにしました。現在は1種類ですが、今後ボムの種類が増えた際にボムをVisitorパターンで増やしていくことで簡単に実装ができるようになるためです。

ボム処理のプレイ動画

https://x.com/Cz_mirror/status/1771465361566679079?s=20

前回のサブウェポン実装の記事

https://zenn.dev/cz_mirror/articles/f6b7a813a8afeb

ソースコード

RAYSERのソースコードは以下の場所で公開しております
https://github.com/Czmirror/RAYSER_Script

クラス図

ソース解説

RootLifetimeScopeの定義

ボムで使用するシグナル情報を定義しています。(ボムボタン押下のシグナル情報BombUseSignalとボムの使用中を判定するためのシグナル情報BombActiveSignal)

RootLifetimeScope.cs
using _RAYSER.Scripts.Bomb;
using _RAYSER.Scripts.Item;
using _RAYSER.Scripts.Score;
using _RAYSER.Scripts.SubWeapon;
using BGM.Volume;
using MessagePipe;
using VContainer;
using VContainer.Unity;

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

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

            // MessagePipeの設定
            var messagePipeOptions = builder.RegisterMessagePipe();
            builder.RegisterMessageBroker<ItemPurchaseSignal>(messagePipeOptions);
            builder.RegisterMessageBroker<SubweaponMoveDirection>(messagePipeOptions);
            builder.RegisterMessageBroker<ItemData>(messagePipeOptions);
            builder.RegisterMessageBroker<SubweaponUseSignal>(messagePipeOptions);
            builder.RegisterMessageBroker<BombUseSignal>(messagePipeOptions);
            builder.RegisterMessageBroker<BombActiveSignal>(messagePipeOptions);

            // ItemAcquisitionをシングルトンとして登録
            builder.Register<ItemAcquisition>(Lifetime.Singleton);

            // SubWeaponMountedをシングルトンとして登録
            builder.Register<SubWeaponMounted>(Lifetime.Singleton);
        }
    }
}

BombLifetimeScope

ボムのライフタイムスコープです。サブウェポンの時とは異なり、ScriptableObjectを用いらない形で実装しています。また今回まだボムは1種類のみなので、このライフタイムスコープでボム(ForceField)を定義しています。
RegisterBuildCallbackでForceFieldを定義してしまったので、ボムの残数を表示する処理のBombPresenterをこの中で定義してしまっていますが、本当はもっとよい書き方がありそうな気がします。

BombLifetimeScope.cs
using InputSystem;
using MessagePipe;
using Shield;
using TMPro;
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボムのライフタイムスコープ
    /// </summary>
    public class BombLifetimeScope : LifetimeScope
    {

        [SerializeField] private BombTurret bombTurret;
        [SerializeField] private BombPrefab bomPrefab;
        [SerializeField] private ForceField forceField;
        [SerializeField] private PlayerShield playerShield;
        [SerializeField] private PlayerController playerController;

        /// <summary>
        /// ボムの使用回数表示UI
        /// </summary>
        [SerializeField] private TextMeshProUGUI bombUseCountText;

        private BombPresenter bombPresenter;
        protected override void Configure(IContainerBuilder builder)
        {
            builder.Register<ForceField>(Lifetime.Singleton);
            builder.Register<PlayerInputSetupService>(Lifetime.Singleton);

            builder.RegisterBuildCallback(container =>
            {
                var setupService = container.Resolve<PlayerInputSetupService>();
                setupService.SetupPlayerController(playerController, container);

                forceField = container.Resolve<ForceField>();
                forceField.SetPrefab(bomPrefab);
                var bombActiveSignalPublisher = container.Resolve<IPublisher<BombActiveSignal>>();
                forceField.SetPublisher(bombActiveSignalPublisher);

                var bombUseSignalSubscriber = container.Resolve<ISubscriber<BombUseSignal>>();
                bombTurret.Setup(bombUseSignalSubscriber, forceField);

                playerShield.Setup(container.Resolve<ISubscriber<BombActiveSignal>>());

                bombPresenter = new BombPresenter(forceField, bombUseCountText);
                bombPresenter.Start();
            });
        }
    }
}

InputSystemの設定

InputSystemでゲームパッドからボムの発射処理を設定します。ボムの発射処理はMessagePipeを用いて、シグナルを送信しています。ゲームパッドのボタンを押下すると、MessagePipeのPublishからBombUseSignalが発行され、BombTurretがBombUseSignalを受信することでサブウェポン発射処理が実行されるようになります。

PlayerController.cs
using _RAYSER.Scripts.Bomb;
using _RAYSER.Scripts.Item;
using _RAYSER.Scripts.SubWeapon;
using Event;
using Event.Signal;
using MessagePipe;
using PlayerMove;
using Turret;
using UI.Game;
using UniRx;
using UnityEngine;
using UnityEngine.InputSystem;

namespace InputSystem
{
    /// <summary>
    /// InputSystem自機コントローラー用クラス
    /// </summary>
    public class PlayerController : MonoBehaviour
    {
        private PlayerInputActions _playerInputActions;
        [SerializeField] private PlayerMoveCore _playerMove;
        [SerializeField] private PlayerLaserTurret _turret;

        private IPublisher<SubweaponMoveDirection> _subweaponMoveDirectionPublisher;
        private IPublisher<SubweaponUseSignal> _subweaponUseSignalPublisher;
        private IPublisher<BombUseSignal> _bombUseSignalPublisher;
        private ItemAcquisition _itemAcquisition;

        public void Setup(
            IPublisher<SubweaponMoveDirection> subweaponMoveDirectionPublisher,
            IPublisher<SubweaponUseSignal> subweaponUseSignalPublisher,
            IPublisher<BombUseSignal> bombUseSignalPublisher,
            ItemAcquisition itemAcquisition
            )
        {
            _subweaponMoveDirectionPublisher = subweaponMoveDirectionPublisher;
            _subweaponUseSignalPublisher = subweaponUseSignalPublisher;
            _bombUseSignalPublisher = bombUseSignalPublisher;
            _itemAcquisition = itemAcquisition;
        }

        private void Awake()
        {
            _playerInputActions = new PlayerInputActions();

            MessageBroker.Default.Receive<GameStartEventEnd>()
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Enable();
                    _playerInputActions.UI.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<Stage2Start>()
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Enable();
                    _playerInputActions.UI.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<Stage3Start>()
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Enable();
                    _playerInputActions.UI.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<Stage2IntervalStart>()
                .Where(x => x._talk == TalkEnum.TalkStart)
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<Stage3IntervalStart>()
                .Where(x => x._talk == TalkEnum.TalkStart)
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<Gameover>()
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Disable();
                    _playerInputActions.UI.Enable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<GameClear>()
                .Where(x=>x._talk == TalkEnum.TalkStart)
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Disable();
                })
                .AddTo(this);

            MessageBroker.Default.Receive<GameClear>()
                .Where(x=>x._talk == TalkEnum.TalkEnd)
                .Subscribe(x =>
                {
                    _playerInputActions.Player.Disable();
                    _playerInputActions.UI.Enable();
                })
                .AddTo(this);

        }

        private void OnEnable()
        {
            _playerInputActions.Player.Fire.performed += OnFire;
            _playerInputActions.Player.Fire.canceled += OnFireStop;
            _playerInputActions.Player.Move.performed += OnMove;
            _playerInputActions.Player.Move.canceled += OnMoveStop;
            _playerInputActions.Player.SubWeapon.performed += OnSubWeaponUse;
            _playerInputActions.Player.SubWeapon.canceled += OnSubWeaponStop;
            _playerInputActions.Player.Bomb.performed += OnBombUse;
            _playerInputActions.Player.Bomb.canceled += OnBombStop;
            _playerInputActions.Player.WeaponSwitchRight.performed += OnWeaponSwitchRight;
            _playerInputActions.Player.WeaponSwitchLeft.performed += OnWeaponSwitchLeft;
        }

        private void OnDisable()
        {
            _playerInputActions.Player.Fire.performed -= OnFire;
            _playerInputActions.Player.Fire.canceled -= OnFireStop;
            _playerInputActions.Player.Move.performed -= OnMove;
            _playerInputActions.Player.Move.canceled -= OnMoveStop;
            _playerInputActions.Player.SubWeapon.performed -= OnSubWeaponUse;
            _playerInputActions.Player.SubWeapon.canceled -= OnSubWeaponStop;
            _playerInputActions.Player.Bomb.performed -= OnBombUse;
            _playerInputActions.Player.WeaponSwitchRight.performed -= OnWeaponSwitchRight;
            _playerInputActions.Player.WeaponSwitchLeft.performed -= OnWeaponSwitchLeft;
        }

        private void OnFire(InputAction.CallbackContext obj)
        {
            _turret.Fire();
        }

        private void OnFireStop(InputAction.CallbackContext obj)
        {
            _turret.FireStop();
        }

        private void OnMoveStop(InputAction.CallbackContext obj)
        {
            _playerMove.SetDirection(Vector2.zero);
        }

        private void OnMove(InputAction.CallbackContext obj)
        {
            var moveValue = obj.ReadValue<Vector2>();
            _playerMove.SetDirection(moveValue);
        }

        /// <summary>
        /// サブウェポンの使用
        /// </summary>
        /// <param name="context"></param>
        private void OnSubWeaponUse(InputAction.CallbackContext context)
        {
            var selectedSubWeapon = _itemAcquisition.GetSelectingSubWeapon();
            if (selectedSubWeapon == null)
            {
                Debug.LogError("Selected subweapon is null");
                return;
            }

            _subweaponUseSignalPublisher.Publish(new SubweaponUseSignal(SubweaponUseType.Use));
        }

        /// <summary>
        /// サブウェポンの使用停止
        /// </summary>
        /// <param name="context"></param>
        private void OnSubWeaponStop(InputAction.CallbackContext context)
        {
            _subweaponUseSignalPublisher.Publish(new SubweaponUseSignal(SubweaponUseType.Stop));
        }

        /**
         * ボムの使用
         */
        private void OnBombUse(InputAction.CallbackContext context)
        {
            Debug.Log("OnBombUse");
            _bombUseSignalPublisher.Publish(new BombUseSignal(BombUseType.Use));

        }

        /**
         * ボムの使用停止
         */
        private void OnBombStop(InputAction.CallbackContext context)
        {
            _bombUseSignalPublisher.Publish(new BombUseSignal(BombUseType.Stop));
        }

        /**
         * 次のサブウェポンの切り替え
         */
        private void OnWeaponSwitchRight(InputAction.CallbackContext context)
        {
            _subweaponMoveDirectionPublisher.Publish(SubweaponMoveDirection.Right);
        }

        /**
         * 前のサブウェポンの切り替え
         */
        private void OnWeaponSwitchLeft(InputAction.CallbackContext context)
        {
            _subweaponMoveDirectionPublisher.Publish(SubweaponMoveDirection.Left);
        }
    }
}

ボム発射処理

InputSystemからボム発射・停止のシグナルを発信して、ボム側で受信して発射・停止処理を実行します。

BombUseSignal

ボム発射のシグナル。MessagePipeで使用する想定。BombUseTypeのenumで使用・停止の情報を持たせる。InputSystemからのボム発射ボタン押下の判定で使用します。

BombUseSignal.cs
namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボム使用シグナル
    /// </summary>
    public class BombUseSignal
    {
        public BombUseType BombUseType { get; }

        public BombUseSignal(BombUseType bombUseType)
        {
            BombUseType = bombUseType;
        }
    }

    public enum BombUseType
    {
        Use,
        Stop,
    }
}
BombActiveSignal

ボム使用中のシグナル。MessagePipeで使用する想定。BombActiveTypeのenumで利用中・利用停止の情報を持たせる。ボムの使用した際に発信し、終了時にも発信します。こちらの用途はボム使用中の自機の無敵処理を実施するためです。

BombActiveSignal.cs
namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボム有効シグナル
    /// </summary>
    public class BombActiveSignal
    {
        public BombActiveType BombActiveType { get; }

        public BombActiveSignal(BombActiveType bombActiveType)
        {
            BombActiveType = bombActiveType;
        }
    }

    public enum BombActiveType
    {
        Active,
        Inactive,
    }
}

BombTurret

ボムの発射処理です。、MessagePipeでBombUseSignalを受信すると、設定中のボムのクラスから発射処理を実施します。

BombTurret.cs
using System;
using MessagePipe;
using UnityEngine;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボム発射発動
    /// </summary>
    public class BombTurret : MonoBehaviour, IDisposable
    {
        private ISubscriber<BombUseSignal> _bombUseSignalSubscriber;
        private IDisposable _bombUseSignalSubscriberDisposable;
        private IBombVisitor _bombVisitor; // 現在のボムのVisitor

        public void Setup(ISubscriber<BombUseSignal> bombUseSignalSubscriber, IBombVisitor bombVisitor)
        {
            Debug.Log("BombTurret: Setupメソッドを呼び出しました");
            _bombUseSignalSubscriber = bombUseSignalSubscriber;
            var d = DisposableBag.CreateBuilder();

            _bombVisitor = bombVisitor;

            // SubscribeメソッドでサブスクリプションをDisposableBagに追加
            _bombUseSignalSubscriber.Subscribe(signal =>
            {
                Debug.Log("BombTurret: ボム使用シグナルを受け取りました");
                if (_bombVisitor.CanUse())
                {
                    Debug.Log("BombTurret: ボムを使用します");
                    var bombAction = new BombAction(transform.position);
                    bombAction.Accept(_bombVisitor);
                }
            }).AddTo(d);
            _bombUseSignalSubscriberDisposable = d.Build();
        }

        public void Dispose()
        {
            _bombUseSignalSubscriberDisposable?.Dispose();
        }

        private void OnDestroy()
        {
            // コンポーネントが破棄されるときに、Disposeメソッドを呼び出してリソースを解放する
            Dispose();
        }
    }
}


ボム共通処理のElement化処理

BombAction

前回のサブウェポンの処理と同様にボムを発射するためのVisitorパターンのElementを定義しています。ボムの位置情報を保持して、ボムのVisitorの処理Visitメソッドを実行できるように定義しています。

BombAction.cs
using UnityEngine;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボムの発射処理(Element)
    /// </summary>
    public class BombAction
    {
        public Vector3 Position { get; private set; }

        public BombAction(Vector3 position)
        {
            Position = position;
        }

        public void Accept(IBombVisitor visitor)
        {
            visitor.Visit(this);
        }
    }
}

ボム毎のVisitorパターンの作成

IBombVisitor

ボムのVisitorとして定義しているインターフェースです。こちらも前回のサブウェポンの処理と同様にVisitorを定義しています。Visitorパターンを用いることで条件分岐を定義しないで、処理を各ボムのクラスから呼び出しができるようになるので、今後ボムが増えても該当するVisitorを増やすのみで実装ができるようになります。

IBombVisitor.cs
using System;
using UnityEngine;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボムのVisitorインターフェース
    /// </summary>
    public interface IBombVisitor
    {
        void Visit(BombAction action);

        bool CanUse();
        void Use(Vector3 position);
        void Reset();

        /// <summary>
        /// 使用回数
        /// </summary>
        int UseCount { get; }

        /// <summary>
        /// 使用回数変更イベント
        /// </summary>
        event Action<int> OnUseCountChanged;
    }
}

ForceField

ボムの実際の処理を行います。フォースフィールドという名前のボムで発動時にボムのエフェクトを表示して、周りの敵を攻撃します。ボム発動中は自機を無敵にするため、BombActiveSignalを発信しています。

ForceField.cs
using UnityEngine;
using Cysharp.Threading.Tasks;
using System;
using MessagePipe;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// フォースフィールド : ボムの一種 Visitor
    /// </summary>
    [System.Serializable]
    public class ForceField : IBombVisitor
    {
        private BombPrefab forceFieldPrefab;
        private int useCount = 3;
        private int maxUseCount = 3;
        private float duration = 5.0f;
        private DateTime lastUseTime = DateTime.MinValue;
        private BombPrefab currentInstance;

        /// <summary>
        /// ボム使用判定
        /// </summary>
        private bool isUse = false;

        public int UseCount => useCount;
        public event Action<int> OnUseCountChanged;

        private IPublisher<BombActiveSignal> bombActiveSignalPublisher;

        public void Visit(BombAction action)
        {
            Use(action.Position);
        }

        public void SetPrefab(BombPrefab prefab)
        {
            forceFieldPrefab = prefab;
        }

        public void SetPublisher(IPublisher<BombActiveSignal> publisher)
        {
            bombActiveSignalPublisher = publisher;
        }

        public bool CanUse()
        {
            return useCount > 0 && !isUse;
        }

        public async void Use(Vector3 position)
        {
            if (!CanUse()) return;

            isUse = true;
            useCount--;
            NotifyUseCountChanged();
            bombActiveSignalPublisher.Publish(new BombActiveSignal(BombActiveType.Active)); // 使用開始時にPublish

            if (currentInstance != null) UnityEngine.Object.Destroy(currentInstance.gameObject);
            currentInstance = UnityEngine.Object.Instantiate(forceFieldPrefab, position, Quaternion.identity);

            await UniTask.Delay(TimeSpan.FromSeconds(duration));

            if (currentInstance != null) UnityEngine.Object.Destroy(currentInstance.gameObject);

            isUse = false;
            bombActiveSignalPublisher.Publish(new BombActiveSignal(BombActiveType.Inactive));
        }

        public void Reset()
        {
            useCount = maxUseCount;
            NotifyUseCountChanged();
            if (currentInstance != null) UnityEngine.Object.Destroy(currentInstance.gameObject);
            lastUseTime = DateTime.MinValue;
            isUse = false;
        }

        private void NotifyUseCountChanged()
        {
            OnUseCountChanged?.Invoke(useCount);
        }
    }
}

BombPrefab

フォースフィールドのプレハブなどで使用するクラスです

BombPrefab.cs
using Damage;
using UnityEngine;

namespace _RAYSER.Scripts.Bomb
{
    /// <summary>
    /// ボムのPrefab
    /// </summary>
    public class BombPrefab : MonoBehaviour, IDamageableToEnemy
    {
        [SerializeField] private Rigidbody _rigidbody;

        /// <summary>
        /// 接触時ダメージ
        /// </summary>
        [SerializeField] private float damage;

        public float AddDamage()
        {
            return damage;
        }
    }
}

PlayerShield

今回ボム発動時に自機に無敵状態を施すために、MessagePipeでBombActiveSignalを受信したら、無敵にする処理を実施しています。終了のBombActiveSignalを受信したら無敵無効化処理を実行します。

PlayerShield.cs
using System;
using System.Threading;
using _RAYSER.Scripts.Bomb;
using Cysharp.Threading.Tasks;
using Damage;
using Event;
using Event.Signal;
using MessagePipe;
using Status;
using UniRx;
using UnityEngine;

namespace Shield
{
    /// <summary>
    /// 自機のシールド
    /// </summary>
    public class PlayerShield : MonoBehaviour, IShield
    {
        [SerializeField] private GameObject player;

        /// <summary>
        /// ゲームステータス
        /// </summary>
        [SerializeField] private GameStatus _gameStatus;

        /// <summary>
        /// 爆発処理のゲームオブジェクト
        /// </summary>
        [SerializeField] private GameObject explosionDestruction;

        /// <summary>
        /// ダメージ処理のゲームオブジェクト
        /// </summary>
        [SerializeField] private GameObject explosionDamage;

        /// <summary>
        /// シールドUniRx
        /// </summary>
        [SerializeField] private ReactiveProperty<float> shield = new ReactiveProperty<float>(100f);

        /// <summary>
        /// 外部参照用シールドUniRx
        /// </summary>
        public IObservable<float> ShieldObservable => shield;

        /// <summary>
        /// シールド最大値UniRx
        /// </summary>
        [SerializeField] private ReactiveProperty<float> maxShield = new ReactiveProperty<float>(100);

        /// <summary>
        /// 外部参照用シールド最大値UniRx
        /// </summary>
        public IObservable<float> maxShieldObservable => maxShield;

        /// <summary>
        /// イベント時無敵状態
        /// </summary>
        private bool isEventInvincible = false;

        /// <summary>
        /// ダメージ後無敵状態UniRx
        /// </summary>
        [SerializeField] private ReactiveProperty<bool> isDamageInvincible = new ReactiveProperty<bool>(false);

        /// <summary>
        /// 外部参照用ダメージ後無敵状態UniRx
        /// </summary>
        public IObservable<bool> IsDamageInvincibleObservable => isDamageInvincible;

        /// <summary>
        /// ダメージ後無敵状態有効時間
        /// </summary>
        private float isDamageInvincibleTime = 3f;

        private ISubscriber<BombActiveSignal> _bombActiveSubscriber;

        public void Setup(ISubscriber<BombActiveSignal> bombActiveSubscriber)
        {
            bombActiveSubscriber.Subscribe(signal =>
            {
                switch (signal.BombActiveType)
                {
                    case BombActiveType.Active:
                        // ボムがアクティブになった時にプレイヤーを無敵状態にする
                        isEventInvincible = true;
                        break;
                    case BombActiveType.Inactive:
                        // ボムの効果が終了した時に無敵状態を解除する
                        isEventInvincible = false;
                        break;
                }
            }).AddTo(this);
        }

        private void Start()
        {
            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Gamestart)
                .Subscribe(_ => isEventInvisibleActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Stage1)
                .Subscribe(_ => isEventInvisibleDeActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Stage2)
                .Subscribe(_ => isEventInvisibleDeActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Stage3)
                .Subscribe(_ => isEventInvisibleDeActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Stage2Interval)
                .Subscribe(_ => isEventInvisibleActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Stage3Interval)
                .Subscribe(_ => isEventInvisibleActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.GameClear)
                .Subscribe(_ => isEventInvisibleActivate())
                .AddTo(this);

            _gameStatus.CurrentGameStateReactiveProperty
                .Where(x => x == GameState.Gameover)
                .Subscribe(_ => isEventInvisibleActivate())
                .AddTo(this);

            MessageBroker.Default.Receive<PlayerShieldRecover>().Where(x => x.ShieldRecoverPoint > 0)
                .Subscribe(x => ShieldRecover(x.ShieldRecoverPoint)).AddTo(this);
        }

        public void ShieldReduction(float damage)
        {
            var _damageExplosion = Instantiate(explosionDamage, transform.position, transform.rotation);

            shield.Value -= damage;
            DamageInvincible(this.GetCancellationTokenOnDestroy()).Forget();

            if (shield.Value < 0)
            {
                shield.Value = 0;
            }

            if (shield.Value == 0)
            {
                FuselageDestruction();
            }
        }

        public void FuselageDestruction()
        {
            MessageBroker.Default.Publish(new Gameover());

            var _destructionExplosion = Instantiate(explosionDestruction, transform.position, transform.rotation);
            Destroy(player);
        }

        /// <summary>
        /// 接触ダメージ処理の発火イベント
        /// </summary>
        /// <param name="collision">接触したゲームオブジェクト</param>
        private void OnCollisionStay(Collision collision)
        {
            if (InvisibleCheck())
            {
                return;
            }

            if (collision.gameObject.TryGetComponent(out IDamageableToPlayer damagetarget))
            {
                ShieldReduction(damagetarget.AddDamage());
            }
        }

        /// <summary>
        /// 貫通ダメージ処理の発火イベント
        /// </summary>
        /// <param name="other">貫通したゲームオブジェクト</param>
        private void OnTriggerEnter(Collider other)
        {
            if (InvisibleCheck())
            {
                return;
            }

            if (other.gameObject.TryGetComponent(out IDamageableToPlayer damagetarget))
            {
                ShieldReduction(damagetarget.AddDamage());
            }
        }

        /// <summary>
        /// 無敵状態のチェック
        /// </summary>
        /// <returns>無敵の有無のbool値</returns>
        private bool InvisibleCheck()
        {
            if (isEventInvincible == true)
            {
                return true;
            }

            if (isDamageInvincible.Value)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// イベント無敵状態有効処理
        /// </summary>
        private void isEventInvisibleActivate()
        {
            isEventInvincible = true;
        }

        /// <summary>
        /// イベント無敵状態無効処理
        /// </summary>
        private void isEventInvisibleDeActivate()
        {
            isEventInvincible = false;
        }

        /// <summary>
        /// 無敵処理
        /// </summary>
        private async UniTaskVoid DamageInvincible(CancellationToken cancellationToken)
        {
            isDamageInvincible.Value = true;
            await UniTask.Delay(TimeSpan.FromSeconds(isDamageInvincibleTime), false, 0, cancellationToken);
            isDamageInvincible.Value = false;
        }


        /// <summary>
        /// シールド回復処理
        /// </summary>
        /// <param name="recover">シールド回復値</param>
        private void ShieldRecover(float recover)
        {
            shield.Value += recover;

            if (maxShield.Value < shield.Value)
            {
                shield.Value = maxShield.Value;
            }
        }
    }
}

BombPresenter

ボムの残り回数を表示します、ViewはBombLifetimeScopeでアタッチしています。


BombPresenter.cs
using System;
using TMPro;
using VContainer.Unity;

namespace _RAYSER.Scripts.Bomb
{
    public class BombPresenter : IStartable, IDisposable
    {
        private readonly IBombVisitor bombVisitor;
        private readonly TextMeshProUGUI bombUseCountText;

        public BombPresenter(IBombVisitor bombVisitor, TextMeshProUGUI bombUseCountText)
        {
            this.bombVisitor = bombVisitor;
            this.bombUseCountText = bombUseCountText;
        }

        public void Start()
        {
            bombVisitor.OnUseCountChanged += UpdateBombUseCountView;
            UpdateBombUseCountView(bombVisitor.UseCount); // 初期表示を更新
        }

        public void Dispose()
        {
            bombVisitor.OnUseCountChanged -= UpdateBombUseCountView;
        }



        private void UpdateBombUseCountView(int useCount)
        {
            if (bombUseCountText != null)
            {
                bombUseCountText.text = $"Bombs: {useCount}";
            }
        }
    }
}

Visitorパターンについての参考記事

Visitorパターンについてはとりすーぷさんの記事が大変参考になります。Switch文を使っていた記述から、VIsitorパターンに書き換えた場合の例が非常にわかりやすく、Visitorパターンを使う前の問題点がどのように解消されるかが把握できるので、Visitorパターンを利用する場面がイメージしやすいです。
https://qiita.com/toRisouP/items/d96a09fab827af17fb37

最後

もしよかったら、いいね、コメントをお願いいたします。

Discussion