🐈

【Unity】類似した敵機の弾発射処理をインターフェース化して汎用化しました

2024/12/14に公開

現在開発中の3DSTG RAYSERでは敵の弾発射するTurret系クラスを雑魚キャラとボス用にわけていていました。
https://zenn.dev/cz_mirror/articles/73d0bc80fef8bb

ほとんど同じような内容のクラスですが差異は弾を発射する発射トリガーの条件が違ったり自機の方角を自動的に向くようにしたりしている程度で、それほど記述量が多いわけではないので、当時は一旦その形で実装していました。
ただ最近のリファクタリングをする過程で、やはり似たようなクラスをコピペするのはよくないので、EnemyTurretをベースに外部から発射トリガーを移譲で設定できるように修正してみました。

ITurretTrigger.cs
using Turret;

namespace _RAYSER.Scripts.Turret.Trigger
{
    /// <summary>
    /// 弾発射トリガーのインターフェース
    /// </summary>
    public interface ITurretTrigger
    {
        void Initialize(EnemyTurret turret);
    }
}

即時発射するトリガーを定義するクラスです、主に雑魚キャラの敵機で使用するトリガーです

ImmediateFireTrigger.cs
using System;
using Turret;
using UnityEngine;

namespace _RAYSER.Scripts.Turret.Trigger
{
    /// <summary>
    /// 即時発射トリガー
    /// </summary>
    [Serializable]
    public class ImmediateFireTrigger : ITurretTrigger
    {
        public void Initialize(EnemyTurret turret)
        {
            turret.StartShooting();
        }
    }
}

ステージ1のボスキャラで会話が終わったら攻撃を開始するトリガーを定義したクラスです。
MessageBrokerでシグナルを受け取ってそこから発火するようにしていたので、今回もそれを踏襲しています。ただ本当はMeesagePipeなどに変えたほうが良いと思いますが、また別の機会で修正しようと思います。

Stage1BossEncounterTrigger.cs
using System;
using Event.Signal;
using Turret;
using UI.Game;
using UniRx;

namespace _RAYSER.Scripts.Turret.Trigger
{
    /// <summary>
    /// ステージ1ボスエンカウントトリガー
    /// </summary>
    [Serializable]
    public class Stage1BossEncounterTrigger : ITurretTrigger
    {
        public void Initialize(EnemyTurret turret)
        {
            MessageBroker.Default.Receive<Stage1BossEncounter>()
                .Where(x => x._talk == TalkEnum.TalkEnd)
                .Subscribe(_ => turret.StartShooting())
                .AddTo(turret);
        }
    }
}

弾の座標のパターンも固定の他に、自機の方向に向いて撃つ敵のパターンにも合わせられるようにFirePointProviderをインターフェース化して差し替えできるように変更しました。

IFirePointProvider.cs
using System.Collections.Generic;
using Turret;
using UnityEngine;

namespace _RAYSER.Scripts.Turret.FirePointProvider
{
    public interface IFirePointProvider
    {
        void Initialize(Transform ownerTransform, List<FirePoint> firePoints);
        List<Vector3> GetPositions();
        List<Quaternion> GetRotations();
    }
}

通常の弾座標提供用クラス

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

namespace _RAYSER.Scripts.Turret.FirePointProvider
{
    /// <summary>
    /// 弾座標提供
    /// </summary>
    public class FirePointProvider : IFirePointProvider
    {
        private Transform ownerTransform;
        private List<FirePoint> firePoints;

        public void Initialize(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));
        }
    }
}

自機を自動的に向くタイプの弾座標提供クラス

TargetedFirePointProvider.cs
using System;
using System.Collections.Generic;
using Target;
using Turret;
using UnityEngine;

namespace _RAYSER.Scripts.Turret.FirePointProvider
{
    /// <summary>
    /// 自機をターゲットとするプロバイダー
    /// </summary>
    [Serializable]
    public class TargetedFirePointProvider: IFirePointProvider
    {
        [SerializeField] private Transform targetTransform;
        private Transform ownerTransform;
        private List<FirePoint> firePoints;

        public void Initialize(Transform ownerTransform, List<FirePoint> firePoints)
        {
            this.ownerTransform = ownerTransform;
            this.firePoints = firePoints;
        }
        public List<Vector3> GetPositions()
        {
            if (firePoints == null || ownerTransform == null)
            {
                Debug.LogError("TargetedFirePointProvider is not properly initialized.");
                return new List<Vector3>();
            }

            return firePoints.ConvertAll(fp => ownerTransform.TransformPoint(fp.Position));
        }

        public List<Quaternion> GetRotations()
        {
            if (firePoints == null || ownerTransform == null)
            {
                Debug.LogError("TargetedFirePointProvider is not properly initialized.");
                return new List<Quaternion>();
            }

            // ターゲットが存在する場合、その方向に回転
            return firePoints.ConvertAll(fp =>
            {
                var position = ownerTransform.TransformPoint(fp.Position);
                if (targetTransform != null)
                {
                    var direction = (targetTransform.position - position).normalized;
                    return Quaternion.LookRotation(direction);
                }

                // ターゲットがない場合は現在の回転を維持
                return ownerTransform.rotation;
            });
        }
    }
}

移譲を受け付ける敵機の砲台クラス

EnemyTurret.cs
using System.Collections.Generic;
using _RAYSER.Scripts.Turret.FirePointProvider;
using _RAYSER.Scripts.Turret.Trigger;
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;
        public float ShotInterval => shotInterval;

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

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

        /// <summary>
        /// 発射トリガー
        /// </summary>
        [SerializeReference] public ITurretTrigger _trigger;

        /// <summary>
        /// 弾座標プロバイダー
        /// </summary>
        [SerializeReference] public IFirePointProvider _firePointProvider;

        private TurretShooter _turretShooter;
        private BeamPool _beamPool;

        public void Initialize()
        {
            if (_firePointProvider == null)
            {
                _firePointProvider = new FirePointProvider();
            }
            _firePointProvider.Initialize(transform, firePoints);

            _beamPool = new BeamPool(enemyBeamPrefab);
            _turretShooter = new TurretShooter(_firePointProvider, _beamPool, shotInterval);

            _trigger?.Initialize(this);
        }

        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();
        }

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

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

さきほどのトリガーと合わせて、現在RAYSERで定義した敵機の攻撃パターンをEnemyTurretで定義できるようになり、他にも攻撃パターンが増えても汎用的に拡張できるようになったので、類似クラスを増やす必要はなくなりました。

今回の修正で、AnnulusGamesさんのAlchemyというライブラリを使わせていただきました、非常に便利なので、おすすめです。こちらを用いることでインスペクタからインターフェース関連のクラスのアタッチをすることができて非常に助かりました。
https://github.com/AnnulusGames/Alchemy

Discussion