Roba Memo - 素人のUnity覚書と奮闘記

素人のUnity覚書と奮闘記

Mobの攻撃

関連記事:ディフェンスゲームを作る 目次 - Roba Memo - 素人のUnity覚書と奮闘記

細かいダメージ計算は後回しで、とりあえず1対1の攻撃処理をメモ。

前記事までのおさらい

Mobとなるゲームオブジェクトには、 以下のクラスがアタッチされている。
・Mob.cs
・HPController.cs

処理の流れを考える

まずは、Mobの行動パターンを考える。
・城に向かって進む
・城に到達したら、停止して城を攻撃する
・キャラクターの攻撃を受けたら、攻撃範囲なら停止して反撃する
・複数のキャラクターから攻撃を受けたら、順番に攻撃する
・HPが0になったら、消える
・キャラクターを倒したら、再び城に向かって進む

停止のタイミング

・城に到達したら、停止して城を攻撃する
・キャラクターの攻撃を受けたら、攻撃範囲なら停止して反撃する
ということより、攻撃するタイミングで停止処理を施せばOK

攻撃対象の絞り込み

・城に到達したら、停止して城を攻撃する
・キャラクターの攻撃を受けたら、攻撃範囲なら停止して反撃する
・複数のキャラクターから攻撃を受けたら、順番に攻撃する
ということは、城も含めて攻撃対象となるリストを作り、そこから攻撃範囲にいるものを探せばいいはず!

処理の流れ

(1)攻撃リストを作成する。
(2)攻撃を受ける or 城に到達したら、攻撃リストに追加。
(3)攻撃リストから、攻撃範囲にいるものを選び、攻撃対象にする。
(4)攻撃対象のHPを削る。
(5)HPが0になったら、次の攻撃対象をリストから選ぶ。

ってな感じで、攻撃リストを軸に考えたら、だいたい次のタイミングでエラーがでちゃう。
・他のMobに攻撃対象が倒されたとき
・攻撃対象が、攻撃範囲から外れたとき
つまりは、攻撃対象がNullの場合の処理が足りない。
ということで、
(6)Updateで攻撃対象があるかどうかを監視し、nullならリストから削除する。
(7)OnTriggerExitで対象がコライダーから外れたら、リストから削除する。

こんな感じの処理を、いざ、コーディング!

キャラクターから攻撃を受けたらリストに追加する

攻撃を受けるメソッドを作る

ダメージを受ける=HPが削られることなので、HPControllerクラスに追加していく。

public class HPController : MonoBehaviour
{
    [Header ("最大HP")][SerializeField] int max_hp;
    //現在のHP
    int now_hp;
    //攻撃相手
    [HideInInspector]public GameObject attack_target;

    void Start ()
    {
        now_hp = max_hp;
    }

    //ダメージの設定
    public int SetDamage (int damage, GameObject target)
    {
        if (!attack_target) {
            attack_target = target;
        }
        now_hp -= damage;
        if (now_hp <= 0) {
            now_hp = 0;
            Invoke ("OnDead", 0f);
        }
        return now_hp;
    }

    //死亡時の処理
    void OnDead ()
    {
        Destroy (gameObject);
    }
}

AがBに攻撃する場合
(1)AがBのHPController.SetDamage()を実行。
(2)引数に、与えるダメージとA自身を渡す。
(3)受け取ったBのHPControllerは、攻撃対象にAを設定し、自身のHPからダメージを引く。
(4)攻撃相手( attack_target )は、BのMobクラスから参照したいのでpublicにしておく。
(5)HPが0になったら、OnDead()を実行する。

とりあえずで、死亡後に即時OnDeadを実行してDestroyで破棄してある。
アニメーションなどは、追々。

Mobクラスで攻撃を受けたかどうか感知する

(1)自身のHPControllerクラスのattack_targetをUpdateで見張る。
(2)既に追加されている場合は、重複しないようにする。
(3)リストに追加したら、HPのattack_targetを初期化しておく。
   そうしておかないと、敵を倒したり攻撃範囲外になっても残り続けるから。

public class Mob : MonoBehaviour
{
    List<GameObject> attack_list = new List<GameObject> ();
    GameObject attack_target;
    HPController HP;

    void Start ()
    {
        HP = gameObject.GetComponent<HPController> ();
    }

    void Update ()
    {
        //攻撃を受けたらリストに追加
        if (HP.attack_target) {
            if (!attack_list.Contains (HP.attack_target)) {
                attack_list.Insert (0, HP.attack_target);
            }
            HP.attack_target = null;
        }
    }
}

お城に到達したら攻撃リストに追加する

Mob.csに追加。
※お城のタグは"castle"にしておくこと。

public class Mob : MonoBehaviour
{
    void OnTriggerEnter (Collider coll)
    {
        GameObject target = coll.gameObject;
        string tag = target.tag;

        if (tag == "castle") {
            attack_list.Add (target);
        }

    }
}

リストの追加方法と攻撃の順番について

forを使ってリストのindex=0から順に、攻撃対象をサーチする。 最初にヒットしたものが攻撃対象となるので、リスト順でどれが優先的に攻撃されるかが決まる。

キャラクターをリストの末尾に追加してしまうと、城を守るためにキャラを追加で配置しても、城が攻撃ターゲットのまま切り替わらないので、これではキャラクターが盾にならない。
キャラクターはInsertで最初(index=0)に挿入し、お城はaddで最後に追加する。

攻撃リストから攻撃対象を選ぶ

下の画像で、白いのが敵(キャラクター)で、黒いのが自身(Mob)です。
f:id:nico-taniku:20170810220655p:plain:w300
このように、敵の攻撃範囲にあるけど、自分の攻撃範囲に敵がない場合を想定して、
攻撃リストの中で、自身の攻撃範囲にあるものを攻撃ターゲットにする。

自身の攻撃範囲にあるかどうかを調べるには

(1)自分の攻撃範囲は、コライダーのサイズから調べる。
(2)敵の座標と自分の座標の距離を計る(2点間のベクトル長さを求めるメソッドを使う)
(3)攻撃範囲内で、新しい敵ならターゲットを切り替える。
(4)攻撃開始

※ i = attack_list.Count; は、for文を抜けるための処理。

Mob.csに追加

   //Mobのコライダー半径
    float my_area;

    void Start ()
    {
        my_area = gameObject.GetComponent<Collider> ().bounds.size.x / 2;
    }

    void Update ()
    {
        AttackManager ();
    }

    void AttackManager ()
    {
        for (int i = 0; i < attack_list.Count; i++) {
            GameObject target = attack_list [i];
            float distance = (target.transform.position - transform.position).magnitude;
            if (distance <= my_area) {
                if (attack_target != target) {
                    attack_target = target;
                    Attack ();
                } 
                i = attack_list.Count;
            }
        }
    }

    void Attack ()
    {

    }

攻撃処理

(1)Updateにて、攻撃対象があれば、攻撃速度に合わせて攻撃メソッドを実行する。
(2)攻撃範囲内にあれば、攻撃をする。
(3)倒れたら攻撃リストから抜いて、ターゲットをnullにする。
(4)再び、城に向かって進む。

   [SerializeField][Header ("攻撃速度")] float attack_speed;

    void Update ()
    {
        AttackManager ();
        if (attack_target) {
            attack_counter -= Time.deltaTime;
            if (attack_counter < 0.0f) {
                attack_counter = attack_speed;
                Attack ();
            }
        }
    }

    void Attack ()
    {
        float distance = (attack_target.transform.position - transform.position).magnitude;
        if (distance <= my_area) { 
            //停止
            is_stop = true;
            //攻撃処理
            int hp = attack_target.GetComponent<HPController> ().SetDamage (100, gameObject);
            if (hp <= 0) {
                ResetAttackTarget ();
            }
        }
    }

    //attack_targetが死亡したときの処理
    void ResetAttackTarget ()
    {
        attack_list.Remove (attack_target);
        attack_target = null;
        is_stop = false;
    }

候補者がnullでないかを見張る

上記コードのままでは、攻撃対象が他の要因で消滅した時にエラーがでる。
なのでnullチェックして、消滅していたら次のターゲットへ移るようにする。

   void AttackManager ()
    {
        for (int i = 0; i < attack_list.Count; i++) {
            GameObject target = attack_list [i];
            if (target) {
                float distance = (target.transform.position - transform.position).magnitude;
                if (distance <= my_area) {
                    if (attack_target != target) {
                        attack_target = target;
                        Attack ();
                    }
                    i = attack_list.Count;
                }
            } else {
                if (attack_target == target) {
                    ResetAttackTarget ();
                } else {
                    attack_list.Remove (target);
                }
                i = attack_list.Count;
            }
        }
    }

候補者が攻撃範囲から外れた時の処理

   void OnTriggerExit (Collider coll)
    {
        GameObject target = coll.gameObject;
        string tag = target.tag;

        //攻撃範囲から外れたらリストから削除する
        if (tag == "character") {
            if (attack_target == target) {
                ResetAttackTarget ();
            }
            if (attack_list.Contains (target)) {              
                attack_list.Remove (target);
            }
        }
    }

攻撃アニメーションを追加

アニメーションの作成

アニメーションを作成して、ステートを追加。
bool値のパラメーターを作成し、trueの時に攻撃アニメーションを再生するようにしておく。
falseでアイドリング状態に戻る。
※攻撃アニメーションは、攻撃するタイミングで合わせたいのでループしないようにしておく。

パラメーター : attack(bool)
idle → attack attack=true
attack → idle attack=false

f:id:nico-taniku:20170810130057p:plain:w600
f:id:nico-taniku:20170810130019p:plain:w600
f:id:nico-taniku:20170810130043p:plain:h250 f:id:nico-taniku:20170810130049p:plain:h250

スクリプト

再生速度について
attack_speed=0.5の時、アニメーションの速度は倍速になるので、
アニメーションの速度= 1 / attack_speed; となる。

AttackParameterについて
攻撃アニメーションは、再生ごとにアイドリング状態に戻さないと、攻撃のタイミングとアニメーションが合わなくなる。
そのための処理。
attack=true → 1回再生 → attack=false → アイドリング

次の攻撃の直前で呼び出して、falseにしておく。
Invoke (“AttackParameter”, attack_speed - 0.01f);

Attack()メソッドに追加

   void Attack ()
    {
        float distance = (attack_target.transform.position - transform.position).magnitude;
        if (distance <= my_area) { 
            //停止
            is_stop = true;
            //攻撃アニメーション処理
            animator.SetBool ("attack", true);
            animator.speed = 1 / attack_speed;
            Invoke ("AttackParameter", attack_speed - 0.01f);
            //攻撃処理
            int hp = attack_target.GetComponent<HPController> ().SetDamage (100, gameObject);
            if (hp <= 0) {
                ResetAttackTarget ();
            }
        }
    }

    //攻撃アニメーションが再生されたらアイドリングに戻す
    void AttackParameter ()
    {
        animator.speed = 1f;
        animator.SetBool ("attack", false);
    }

以上。