🔥

【Unity】敵機の弾を複数発射できるように修正してみました

2024/12/13に公開

現在RAYSERという3Dシューティングゲームを作っていますが、数年前の実装なので今から見ると非効率な作りをしてしまっているところがあり、いろいろ見直しをしています。

EnemyTurretと呼ばれるclassを持ったゲームオブジェクトを敵機の下に持たせるようにしていたのですが、こちら3方向に撃つ敵を作りたい時に、同じようなゲームオブジェクトを複数持たせてそれぞれRotationの値を変える方法をとっていました。

前回の委譲化に伴い、この辺りの構造も手直しして一つのclassで複数のPositonとRotationをセットにした座標を持たせるように修正してみました

https://zenn.dev/cz_mirror/articles/39b4c765e7252c
https://zenn.dev/cz_mirror/articles/c2ae295feccb36

まずは弾の発射地点を定義するクラスを作成

FirePoint.cs
using System;
using UnityEngine;

namespace Turret
{
    /// <summary>
    /// 発射点のリスト
    /// </summary>
    [Serializable]
    public class FirePoint
    {
        public Vector3 Position;
        public Vector3 Rotation;
    }
}

次に委譲用のクラスの方で複数のPositonとRotationを定義できるように修正しました。

TurretController.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
using UniRx;

namespace Turret
{
    /// <summary>
    /// 砲台の制御クラス
    /// </summary>
    public class TurretController
    {
        private readonly GameObject owner;
        private readonly EnemyBeam enemyBeamPrefab;
        private readonly float shotInterval;
        private ObjectPool<EnemyBeam> beamPool;
        private List<EnemyBeam> activeBeams = new List<EnemyBeam>();
        private IDisposable shotSubscription;
        private readonly Func<List<Vector3>> getPosition;
        private readonly Func<List<Quaternion>> getRotation;

        public TurretController(
            GameObject owner,
            EnemyBeam enemyBeamPrefab,
            float shotInterval,
            Func<List<Vector3>> getPosition,
            Func<List<Quaternion>> getRotation
            )
        {
            this.owner = owner;
            this.enemyBeamPrefab = enemyBeamPrefab;
            this.shotInterval = shotInterval;
            this.getPosition = getPosition;
            this.getRotation = getRotation;

            // その他の初期化
            InitializePool();
        }

        private void InitializePool()
        {
            beamPool = new ObjectPool<EnemyBeam>(
                createFunc: () => GameObject.Instantiate(enemyBeamPrefab),
                actionOnGet: beam => beam.gameObject.SetActive(true),
                actionOnRelease: beam => beam.gameObject.SetActive(false),
                actionOnDestroy: beam => GameObject.Destroy(beam.gameObject),
                defaultCapacity: 10,
                maxSize: 20
            );
        }

        public void StartShooting()
        {
            StopShooting(); // 既存の購読を停止して重複を防止

            shotSubscription = Observable
                .Interval(TimeSpan.FromSeconds(shotInterval))
                .Subscribe(_ => Shoot())
                .AddTo(owner);
        }

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

        private void Shoot()
        {
            if (!owner.transform.root.gameObject.activeSelf) return;

            // 発射点のリストを取得
            var positions = getPosition();
            var rotations = getRotation();

            // 発射点と回転の数が一致していない場合はエラーを表示
            if (positions.Count != rotations.Count)
            {
                Debug.LogError("Positions and Rotations count mismatch.");
                return;
            }

            // 各発射点に基づいてビームを発射
            for (int i = 0; i < positions.Count; i++)
            {
                var beam = beamPool.Get();
                beam.transform.position = positions[i];
                beam.transform.rotation = rotations[i];

                activeBeams.Add(beam);

                // ビームが非アクティブ化された際にプールに戻す処理
                Action releaseAction = null;
                releaseAction = () =>
                {
                    beamPool.Release(beam);
                    beam.OnDeactivation -= releaseAction;
                    activeBeams.Remove(beam);
                };

                beam.OnDeactivation += releaseAction;
            }
        }

        public void Cleanup()
        {
            StopShooting();

            foreach (var beam in activeBeams)
            {
                if (beam != null)
                {
                    GameObject.Destroy(beam.gameObject);
                }
            }

            activeBeams.Clear();
            beamPool?.Clear();
        }
    }
}

最後にEnemyBeamTurretクラスでFirePointを複数アタッチできるようにすることで、一つのクラスで複数の弾を設定できるようになりました。

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

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

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

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

        private TurretController _turretController;

        private void Start()
        {
            // TurretControllerの初期化
            _turretController = new TurretController(
                owner: gameObject,
                enemyBeamPrefab: enemyBeamPrefab,
                shotInterval: shotInterbalTime,
                getPosition: () => firePoints.ConvertAll(fp => transform.TransformPoint(fp.Position)),
                getRotation: () => firePoints.ConvertAll(fp => transform.rotation * Quaternion.Euler(fp.Rotation))
            );

            // 通常の敵タレットは開始時から発射を開始
            _turretController.StartShooting();
        }

        private async void OnDestroy()
        {
            // TurretControllerのリソースをクリア
            _turretController?.Cleanup();
        }
    }
}

こちらの内容ですが、移譲用クラスに対して責務が集中しているのとControllerというSOLID原則に違反しやすい命名を改善するために、次回の記事で修正を行いました
https://zenn.dev/cz_mirror/articles/73d0bc80fef8bb

Discussion