i-school - UniRx を利用した攻撃処理の実装例
 オートバトル系のシステムを UniRx を利用して実装するサンプルになります。 
一定時間経過ごとに指定した範囲内に攻撃対象がいることを確認し、対象がいる場合には、そのうちの最もプレイヤーに近い対象に1回攻撃を行います。
これを自動的に繰り返します。
 
 応用することでヴァンパイアサバイバーズやアーチャー伝説などのようなオートバトルシステムの根幹として利用可能です。

 インターフェースと基底クラスを作成した上、MVP パターンを採用し、Model、View、Presenter の3つを作成します。



ISetup インターフェース




using UnityEngine;

/// <summary>
/// 初期設定共通化用のインターフェース
/// </summary>
public interface ISetup {
    void SetUp(GameObject entityObject = null);
}



BarBase



using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

/// <summary>
/// Slider 用基底クラス
/// </summary>
public class BarBase : MonoBehaviour {

#pragma warning disable 0649
    [SerializeField] protected CanvasGroup canvasGroup;  // TODO あとで TextViewBase と ViewBase として共通化する
    [SerializeField] protected Canvas canvas;            // あとで TextViewBase と共通化する
    [SerializeField] protected Slider slider;
#pragma warning restore 0649


    /// <summary>
    /// スライダー表示更新
    /// </summary>
    /// <param name="currentValue"></param>
    public virtual void UpdateBar(float currentValue) {
        slider.DOValue(currentValue, 0.1f).SetEase(Ease.Linear).SetLink(gameObject);
    }

    /// <summary>
    /// 初期設定
    /// </summary>
    /// <param name="maxValue"></param>
    public virtual void SetUpView(float maxValue) {
        slider.maxValue = maxValue;

        if (canvas.worldCamera == null) {
            canvas.worldCamera = Camera.main;
        }
    }

    /// <summary>
    /// スライダー表示
    /// </summary>
    public virtual void ShowBarView() {
        //canvasGroupTextView.alpha = 1.0f;
        canvas.enabled = true;
    }

    /// <summary>
    /// スライダー非表示
    /// </summary>
    public virtual void HideBarView() {
        //canvasGroupTextView.alpha = 0f;
        canvas.enabled = false;
    }


    public Transform GetCanvasTran() {
        return canvas.transform;
    }
}



Model



using UniRx;

/// <summary>
/// 攻撃処理 Model
/// </summary>
public class AttackModeModel {

    public ReactiveProperty<float> CurrentActionGaugePoint;
    private readonly CompositeDisposable disposables = new();

    public float MaxActionGaugePoint { get; private set; }
    public float AttackSpeed { get; private set; }
    public int Atk { get; private set; }
    public int Inte { get; private set; }
    public int Def { get; private set; }
    public int Mdef { get; private set; }

    public float AttackRange { get; private set; }
    public DamageType DamageType { get; private set; }
    
    public eAttribute AttributeType { get; private set; }

    public int MinDmg { get; private set; }


    public AttackModeModel(float attackSpeed, int atk, float attackRange) { }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="maxAttackPoint">ConstData から参照(300)</param>
    /// <param name="spd">ConstaData から参照(199) + spd</param>
    /// <param name="atk"></param>
    /// <param name="inte"></param>
    /// <param name="def"></param>
    /// <param name="mdef"></param>
    /// <param name="attackRange">攻撃範囲</param>
    /// <param name="damageType">攻撃のタイプ。物理か魔法</param>
    /// <param name="attributeType">攻撃の属性</param>
    /// <param name="minDmg">最小ダメージ値Player = 0</param>
    public AttackModeModel(float spd, int atk, int inte, int def, int mdef, float attackRange, DamageType damageType, eAttribute attributeType, int minDmg = 0) {
        CurrentActionGaugePoint = new(0f);

        MaxActionGaugePoint = ConstData.MAX_ACTION_GAUGE_POINT;
        AttackSpeed = ConstData.ATTACK_SPEED_OFFSET + spd;

        Atk = atk;
        Inte = inte;
        Def = def;
        Mdef = mdef;

        AttackRange = attackRange;
        DamageType = damageType;
        AttributeType = attributeType;

        MinDmg = minDmg;
    }


    /// <summary>
    /// アクションゲージ値のリセット
    /// </summary>
    public void ResetActionGaugePoint() {
        CurrentActionGaugePoint.Value = 0f;
    }

    /// <summary>
    /// オブジェクト(インスタンス)が破棄されるときにCompositeDisposableのDisposeも呼び出す
    /// </summary>
    public void Dispose() {
        // Dispose メソッドが呼ばれたときに、CompositeDisposable に含まれる全てのストリームが自動的に解放
        disposables.Dispose();
    }
}



View



/// <summary>
/// 攻撃処理 View
/// </summary>
public class ActionGuageBar : BarBase {

    /// <summary>
    /// 初期設定
    /// </summary>
    /// <param name="maxValue"></param>
    public override void SetUpView(float maxValue) {
        base.SetUpView(maxValue);

        // 初期値を 0 に設定
        slider.value = 0;

        // 隠す
        HideBarView();
    }

    /// <summary>
    /// 攻撃ゲージの表示更新
    /// </summary>
    /// <param name="currentValue"></param>
    public override void UpdateBar(float currentValue) {
        slider.value = currentValue;
    }
}




Presenter


using UnityEngine;
using UniRx;
using UniRx.Triggers;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// 攻撃処理 Presenter
/// </summary>
public class AttackModePresenter : ISetup {

    private LifeBase attackTarget;
    private ReactiveProperty<bool> IsAttackReady = new(false);

    private AttackModeModel myAttackModeModel;
    private ActionGuageBar attackModeView;

    private HpBar hpBarView;
    private AttackRangeCollider attackRangeCollider;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public AttackModePresenter() {}

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="spd"></param>
    /// <param name="attackPower"></param>
    /// <param name="attackRange"></param>
    public AttackModePresenter(float spd, int atk, int inte, int def, int mdef, float attackRange, DamageType damageType, eAttribute attributeType, int minDmg = 0) {
        myAttackModeModel = new(spd, atk, inte, def, mdef, attackRange, damageType, attributeType, minDmg);
    }

    /// <summary>
    /// 初期設定
    /// </summary>
    /// <param name="entityObject"></param>
    public void SetUp(GameObject entityObject) {

        // entityObject から攻撃用のデータを参照してもらう
        if (entityObject.TryGetComponent(out attackModeView)) {
            attackModeView.SetUpView(ConstData.MAX_ACTION_GAUGE_POINT);
        }

        // Hp 取得
        LifeBase myLife;
        if (!entityObject.TryGetComponent(out myLife)) {
            return;
        }

        myAttackModeModel = myLife.AttackModeModel;

        // HpBar 取得
        if (entityObject.TryGetComponent(out hpBarView)) {
            hpBarView.SetUpView(myLife.Hp.Value);
        }

        // Hp の購読 Hp ゲージ更新
        myLife.Hp.Subscribe(hp => hpBarView.UpdateBar(myLife.Hp.Value)).AddTo(entityObject);

        // 攻撃範囲用のコライダーの取得ができない場合、処理しない
        if (!entityObject.TryGetComponent(out attackRangeCollider)) {
            return;
        }

        // 攻撃範囲のコライダーのサイズを設定
        attackRangeCollider.SetUpColliderRange(myAttackModeModel.AttackRange);

        // モック用
        if (myLife.EntityType == EntityType.Enemy) {

            return;
        }

        // 攻撃対象となるタグを設定
        string tag = myLife.GetTargetEntityType().ToString();

        // 攻撃対象の探索
        entityObject.UpdateAsObservable()
            .Subscribe(_ => SearchAttackTarget(entityObject, tag));

        // 侵入判定の購読 自動攻撃準備
        //attackRangeCollider.AttackRange.OnTriggerStay2DAsObservable()
        //    .Where(col => attackTarget == null)
        //    .Where(col => col.transform.TryGetComponent(out attackTarget))  // コライダーへの侵入判定
        //    .Where(_ => myLife.CheckTarget(attackTarget.EntityType))        // 攻撃対象か判定
        //    .Subscribe(col => {
        //        // 攻撃準備開始とゲージ表示
        //        IsAttackReady.Value = true;
        //        attackModeView.ShowBarView();

        //        Debug.Log($"攻撃準備 開始 {attackTarget}");
        //    });

        // いなくなった判定の購読 自動攻撃停止
        //attackRangeCollider.AttackRange.OnTriggerExit2DAsObservable()
        //    .Where(col => col.transform.TryGetComponent(out attackTarget))  // コライダーへの侵入判定
        //    .Where(_ => myLife.CheckTarget(attackTarget.EntityType))        // 攻撃対象か判定
        //    .Subscribe(col => StopAttack());

        // Update の購読
        entityObject.UpdateAsObservable()
            .Where(_ => IsAttackReady.Value)
            .Where(_ => myAttackModeModel.CurrentActionGaugePoint.Value < myAttackModeModel.MaxActionGaugePoint)
            .Subscribe(_ => {
                // 攻撃ゲージの加算
                myAttackModeModel.CurrentActionGaugePoint.Value += myAttackModeModel.AttackSpeed * Time.deltaTime;

                // 攻撃ゲージの View 更新
                attackModeView.UpdateBar(myAttackModeModel.CurrentActionGaugePoint.Value);
            });

        // 攻撃ゲージの購読
        myAttackModeModel.CurrentActionGaugePoint
            .Where(_ => attackTarget)
            .Where(currentActionGaugePoint => currentActionGaugePoint >= myAttackModeModel.MaxActionGaugePoint)
            .Subscribe(_ => {
                if (attackTarget == null) {
                    // アクションゲージリセット
                    myAttackModeModel.ResetActionGaugePoint();
                } else {
                    // 自動攻撃
                    AutoAttack();
                }
            })
            .AddTo(entityObject);

        // 破壊時の購読
        entityObject.OnDestroyAsObservable().Subscribe(_ => myAttackModeModel.Dispose());

        // TODO 名前表示


        //Debug.Log($"{this} Setup 完了");
    }

    /// <summary>
    /// 自動攻撃
    /// </summary>
    private void AutoAttack() {
        //Debug.Log("自動攻撃");

        // 属性係数算出
        float attributeRate = BattleCalculater.CalcAttributeRate(myAttackModeModel.AttributeType, attackTarget.AttackModeModel.AttributeType);
        //Debug.Log($"属性補正 : {attributeRate}");

        //Debug.Log($"攻撃力 : {myAttackModeModel.Atk}");
        //Debug.Log($"知力 : {myAttackModeModel.Inte}");
        //Debug.Log($"防御力 : {attackTarget.AttackModeModel.Def}");

        // 現在の攻撃タイプを元にダメージ計算
        int damage = myAttackModeModel.DamageType == DamageType.atk
            ? BattleCalculater.CalcAtkDamage(myAttackModeModel.Atk, myAttackModeModel.Inte, attributeRate, attackTarget.AttackModeModel.Def)      // 物理ダメージ
            : BattleCalculater.CalcInteDamage(myAttackModeModel.Inte, myAttackModeModel.Atk, attributeRate, attackTarget.AttackModeModel.Mdef);   // 魔法ダメージ

        Debug.Log($"最終ダメージ : {damage}");

        // アクションゲージリセット
        myAttackModeModel.ResetActionGaugePoint();

        // 攻撃対象が残っているか確認
        if (attackTarget == null) {
            Debug.Log("攻撃対象消失");
            return;
        }
        
        // ダメージ値をフロート表示。オブジェクトプール利用
        attackTarget.ShowFloatingView(damage);
        
        //FloatingView floatingView = (FloatingView)FloatingViewGenerator.instance.GetObjectFromPool(attackTarget.transform.position, attackTarget.transform.rotation);
        //floatingView.SetUpView(damage.ToString());

        // Hp 計算。対象の 残 hp が 0 以下なら
        if (!attackTarget.CalcLife(-damage)) {
            // 自動攻撃停止
            StopAttack();
        }

        //Debug.Log("攻撃完了");
    }

    /// <summary>
    /// 自動攻撃停止
    /// </summary>
    private void StopAttack() {
        attackTarget = null;

        // 攻撃停止。アクションゲージリセットとゲージ非表示
        IsAttackReady.Value = false;
        myAttackModeModel.ResetActionGaugePoint();
        attackModeView.HideBarView();

        //Debug.Log("攻撃停止");
    }

    /// <summary>
    /// 攻撃力などのデータ更新
    /// </summary>
    /// <param name="attackModeModel"></param>
    public void UpdateAttackModel(AttackModeModel attackModeModel, GameObject entityObject) {
        myAttackModeModel = attackModeModel;
        myAttackModeModel.Dispose();

        // 攻撃ゲージの購読やり直し
        myAttackModeModel.CurrentActionGaugePoint
            .Where(_ => attackTarget)
            .Where(currentActionGaugePoint => currentActionGaugePoint >= myAttackModeModel.MaxActionGaugePoint)
            .Subscribe(_ => AutoAttack())
            .AddTo(entityObject);
        Debug.Log("更新");
    }

    /// <summary>
    /// 攻撃対象の探索
    /// </summary>
    /// <param name="entityObject"></param>
    /// <param name="tag"></param>
    private void SearchAttackTarget(GameObject entityObject, string tag) {

        // OverlapCircleAll メソッドを利用して、範囲内のコライダー付きのオブジェクトを取得
        //Collider2D[] hitColliders = Physics2D.OverlapCircleAll(entityObject.transform.position, myAttackModeModel.AttackRange / 2);
        //List<GameObject> objList = new();

        //// 配列内の要素を1つずつ取り出す
        //foreach (Collider2D collider in hitColliders) {
        //    // 指定されたタグを持つオブジェクトであるか判定。それを攻撃対象候補として認識
        //    if (collider.gameObject.CompareTag(tag)) {
        //        objList.Add(collider.gameObject);
        //    }
        //}

        // 上記を LINQ で記述
        List<GameObject> objList = Physics2D.OverlapCircleAll(entityObject.transform.position, myAttackModeModel.AttackRange / 2)
            .Where(collider => collider.CompareTag(tag))
            .Select(collider => collider.gameObject)
            .ToList();

        // 取得したゲームオブジェクトが 0 なら攻撃対象の候補なし
        if (objList.Count == 0) {
            Debug.Log("攻撃対象なし");
            StopAttack();
            return;
        }

        // 最も近いオブジェクトの距離を代入するための変数
        //float nearDistance = 0;

        // 検索された最も近いゲームオブジェクトを代入するための変数
        //GameObject searchTargetObj = null;

        // objsから1つずつobj変数に取り出す
        //foreach (GameObject obj in objList) {

        //    // objに取り出したゲームオブジェクトと、このゲームオブジェクトとの距離を計算して取得
        //    float distance = Vector3.Distance(obj.transform.position, entityObject.transform.transform.position);

        //    // nearDistanceが0(最初はこちら)、あるいはnearDistanceがdistanceよりも大きい値なら
        //    if (nearDistance == 0 || nearDistance > distance) {

        //        // nearDistanceを更新
        //        nearDistance = distance;

        //        // searchTargetObjを更新
        //        searchTargetObj = obj;
        //    }
        //}

        // 上記を LINQ で記述。最も近い対象を抽出
        GameObject searchTargetObj = objList
            .OrderBy(obj => Vector3.Distance(obj.transform.position, entityObject.transform.position))
            .FirstOrDefault();

        //最も近かったオブジェクトを攻撃対象とする
        attackTarget = searchTargetObj.GetComponent<LifeBase>();

        // 攻撃準備開始とゲージ表示
        IsAttackReady.Value = true;
        attackModeView.ShowBarView();

        Debug.Log($"攻撃準備 開始 {attackTarget}");
    }
}


<Physics2D.OverlapCircleAll>


 指定した範囲内にコライダーを展開し、その範囲内にあるコライダーを取得して配列に代入するメソッドです。

 レイヤーで指定し、GC を少なくする OverlapCircleNonAlloc メソッドもあります。


https://docs.unity3d.com/ja/current/ScriptReferenc...

https://docs.unity3d.com/ja/current/ScriptReferenc...


繋ぎ込み


 上記のクラスをプレイヤーや敵のクラスとつなぎ合わせることで、共通の攻撃処理を作成することが可能です。