💡

【Unity】敵機の弾処理をSOLID原則を意識して修正してみました

2024/12/14に公開

敵機の弾処理をSOLID原則を意識して、修正してみました

前回作った移譲用のTurretController.csはControllerという責務のはっきりしない命名を使用していて、実際責務の集中が発生している状態でした
https://zenn.dev/cz_mirror/articles/d3026df676b039

まずはTurretController.csの責務を弾座標提供、オブジェクトプール、砲撃処理に分割して、移譲するように変更しました。
責務を分けることで、一つのクラスの役割を明確になってきたと思います。まだ他の場所で使えるほど汎用化はされていませんが、今後他のクラスでも使えるように意識しながら修正できればと思います。

砲台インターフェース

ITurret.cs
using Cysharp.Threading.Tasks;

namespace Turret
{
    /// <summary>
    /// 砲台インターフェース
    /// </summary>
    public interface ITurret
    {
        /// <summary>
        /// 初期化処理
        /// </summary>
        void Initialize();

        /// <summary>
        /// 射撃開始処理
        /// </summary>
        void StartShooting();

        /// <summary>
        /// 射撃停止処理
        /// </summary>
        void StopShooting();

        /// <summary>
        /// リソース解放処理 (非同期)
        /// </summary>
        UniTask CleanupAsync();
    }
}

砲台クラスは実際の敵機にアタッチする想定のクラスで、こちらを基盤に各種処理を移譲して弾発射処理を実施します

EnemyTurret.cs
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Serialization;

namespace Turret
{
    /// <summary>
    /// 敵機の射撃処理
    /// </summary>
    public class EnemyTurret : MonoBehaviour, ITurret
    {
        /// <summary>
        /// ショットインターバル
        /// </summary>
        [FormerlySerializedAs("shotInterbalTime")] [SerializeField] private float shotInterval = 1.5f;

        /// <summary>
        /// 敵機のビームのPrefab
        /// </summary>
        [SerializeField] private EnemyBeam enemyBeamPrefab;

        /// <summary>
        /// 発射点のリスト
        /// </summary>
        [SerializeField] private List<FirePoint> firePoints = new List<FirePoint>();

        private TurretShooter _turretShooter;
        private BeamPool _beamPool;

        public void Initialize()
        {
            var firePointProvider = new FirePointProvider(transform, firePoints);
            _beamPool = new BeamPool(enemyBeamPrefab);
            _turretShooter = new TurretShooter(firePointProvider, _beamPool, shotInterval);
        }

        public void StartShooting()
        {
            // ゲームオブジェクトが有効である場合のみ発射
            if (gameObject.activeInHierarchy)
            {
                _turretShooter.StartShooting();
            }
        }

        public void StopShooting()
        {
            _turretShooter.StopShooting();
        }

        public async UniTask CleanupAsync()
        {
            _turretShooter.Cleanup();
            if (_beamPool != null)
            {
                await _beamPool.ClearPoolAsync();
            }
        }

        private void OnEnable()
        {
            Initialize();
            StartShooting();
        }

        private async void OnDisable()
        {
            StopShooting();
            await CleanupAsync();
        }

        private async void OnDestroy()
        {
            await CleanupAsync();
        }
    }
}

弾座標提供クラスは、砲撃クラスにPositionとRotation情報を提供するためのクラスです

FirePointProvider.cs
using System.Collections.Generic;
using UnityEngine;

namespace Turret
{
    /// <summary>
    /// 弾座標提供
    /// </summary>
    public class FirePointProvider
    {
        private readonly Transform ownerTransform;
        private readonly List<FirePoint> firePoints;

        public FirePointProvider(Transform ownerTransform, List<FirePoint> firePoints)
        {
            this.ownerTransform = ownerTransform;
            this.firePoints = firePoints;
        }

        public List<Vector3> GetPositions()
        {
            return firePoints.ConvertAll(fp => ownerTransform.TransformPoint(fp.Position));
        }

        public List<Quaternion> GetRotations()
        {
            return firePoints.ConvertAll(fp => ownerTransform.rotation * Quaternion.Euler(fp.Rotation));
        }
    }
}

オブジェクトプール用クラスで、弾の再利用やクリア処理を行います

BeamPool.cs
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;

namespace Turret
{
    /// <summary>
    /// ビームのオブジェクトプール管理クラス
    /// </summary>
    public class BeamPool
    {
        private readonly ObjectPool<EnemyBeam> beamPool;
        private readonly List<EnemyBeam> activeBeams = new List<EnemyBeam>();

        private int _cleanUpDelay = 5000;

        public BeamPool(EnemyBeam beamPrefab)
        {
            beamPool = new ObjectPool<EnemyBeam>(
                createFunc: () =>
                {
                    var beam = Object.Instantiate(beamPrefab);
                    beam.OnDeactivation += () => ReleaseBeam(beam); // 非アクティブ化時にプールへ戻す
                    return beam;
                },
                actionOnGet: beam =>
                {
                    beam.gameObject.SetActive(true);
                    activeBeams.Add(beam);
                },
                actionOnRelease: beam =>
                {
                    beam.gameObject.SetActive(false);
                    activeBeams.Remove(beam);
                },
                actionOnDestroy: beam => Object.Destroy(beam.gameObject),
                defaultCapacity: 10,
                maxSize: 50
            );
        }

        public EnemyBeam GetBeam()
        {
            return beamPool.Get();
        }

        private void ReleaseBeam(EnemyBeam beam)
        {
            beamPool.Release(beam);
        }

        public async UniTask ClearPoolAsync()
        {
            await UniTask.Delay(_cleanUpDelay);

            // すべてのアクティブなビームを強制的に非アクティブ化
            foreach (var beam in new List<EnemyBeam>(activeBeams))
            {
                ReleaseBeam(beam);
            }
            activeBeams.Clear();

            // プールそのものもクリア
            beamPool.Clear();
        }
    }
}

砲撃処理クラスでは、提供された位置情報、プレハブなどを元に弾を生成するクラスです

TurretShooter.cs
using System;
using UniRx;

namespace Turret
{
    /// <summary>
    /// 砲撃処理クラス
    /// </summary>
    public class TurretShooter
    {
        private readonly FirePointProvider firePointProvider;
        private readonly BeamPool beamPool;
        private readonly float shotInterval;
        private IDisposable shootingSubscription;

        public TurretShooter(FirePointProvider firePointProvider, BeamPool beamPool, float shotInterval)
        {
            this.firePointProvider = firePointProvider;
            this.beamPool = beamPool;
            this.shotInterval = shotInterval;
        }

        public void StartShooting()
        {
            StopShooting();

            shootingSubscription = Observable.Interval(TimeSpan.FromSeconds(shotInterval))
                .Subscribe(_ => Shoot());
        }

        public void StopShooting()
        {
            shootingSubscription?.Dispose();
        }

        public void Cleanup()
        {
            StopShooting();
        }

        private void Shoot()
        {
            var positions = firePointProvider.GetPositions();
            var rotations = firePointProvider.GetRotations();

            for (int i = 0; i < positions.Count; i++)
            {
                var beam = beamPool.GetBeam();
                beam.transform.position = positions[i];
                beam.transform.rotation = rotations[i];
                beam.gameObject.SetActive(true);
            }
        }
    }
}

さらに汎用的に敵機の砲撃パターンなどを定義できるように修正してみました
https://zenn.dev/cz_mirror/articles/0ca51f674faaad

Discussion