【自作ゲーム開発8】タワーからホーミング弾を発射する【Unity】


注意
本記事で掲載されている動作の実装方法及びプログラムのソースコードは最適な方法ではない可能性があります。
今後不具合等が判明した場合には修正及び改良をおこなう可能性があります。
また今後自分で同機能を実装する場合の参考にする可能性もあるためソースコードだけではなく説明しつつ記事を進めていきます。

はじめに

Unityによる自作ゲーム開発進捗その8になります!

今回は自作ゲーム開発5で作成したタワーから弾を発射するシステムを開発します。

新しいスクリプトが1つ既存のスクリプト2つを更新します。

以下にソースコードを掲載します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LockonBullet : MonoBehaviour{
    [SerializeField]
    private ParticleSystem[] m_bullet_effect;

    private GameObject m_target_position;

    private Collider m_collider;

    private string m_delete_tag;

    private float m_time_count = 0.0f;
    private float m_bullet_speed = 1.0f;

    enum STATE {
        WAIT,
        MOVE,
        DIE
    }; STATE e_state;

    public void Initialize(string a_target_tag, string a_bullet_tag) {
        m_collider = this.gameObject.GetComponent<BoxCollider>();
        m_delete_tag = a_target_tag;
        this.tag = a_bullet_tag;
    }

    private void FixedUpdate() {
        switch (e_state) {
            case STATE.WAIT:
            break;

            case STATE.MOVE:
                if (m_target_position.activeSelf == false) {
                    // タスク1
                    m_bullet_effect[0].Stop(); m_bullet_effect[1].Play();
                    e_state = STATE.DIE;
                    break;
                }

                Vector3 l_target_position = m_target_position.transform.position;
                l_target_position.y += 0.4f;
                this.transform.position = Vector3.MoveTowards(this.transform.position, l_target_position, Time.deltaTime * m_bullet_speed);
            break;

            case STATE.DIE:
                m_time_count += Time.deltaTime;

                if (m_time_count > 1.0f) {
                    // タスク1
                    m_time_count = 0.0f;
                    e_state = STATE.WAIT;
                    m_collider.enabled = false;
                    this.gameObject.SetActive(false);
                }
            break;
        }
    }

    public void BulletSetup(GameObject a_target, Transform a_position, float a_bullet_speed) {
        this.transform.position = a_position.position;
        this.gameObject.SetActive(true);
        m_target_position = a_target;
        m_bullet_speed = a_bullet_speed;
        // タスク1
        m_bullet_effect[0].Play();
        e_state = STATE.MOVE;
        m_collider.enabled = true;
    }

    private void OnTriggerEnter(Collider a_collider) {
        if (e_state == STATE.DIE) return;

        if(a_collider.gameObject.tag == m_delete_tag) {
            // タスク1
            m_bullet_effect[0].Stop(); m_bullet_effect[1].Play();
            e_state = STATE.DIE;
        }
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数
    e_ 列挙型

    [説明]

    [バージョン]
    2021-02-12 引数ターゲットを追尾し着弾時や休息時に爆発し自身を終了させる

    [タスク]
    タスク1 爆発範囲によってコライダーのサイズを可変させる
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TowerLockon : MonoBehaviour{
    [SerializeField]
    private ParticleSystem m_muzzle_flash;

    [SerializeField]
    private GameObject m_attack_object;

    [SerializeField]
    private Transform m_this_transform;

    private BoxCollider m_collider;

    private List<GameObject> m_object_list = new List<GameObject>();
    private List<Transform> m_position_list = new List<Transform>();
    private List<GameObject> m_bullet_list = new List<GameObject>();
    private List<LockonBullet> m_bullet_script = new List<LockonBullet>();

    private string m_target_tag = null;
    private string m_bullet_tag = null;

    private float m_lockon_speed = 0.05f;
    private float m_blazing_speed = 2.0f;
    private float m_time_count = 0.0f;

    private int m_array_set = 0;

    private bool m_lockon_state = false;

    private void Start(){
        m_collider = this.GetComponent<BoxCollider>();
        LockonStatusSet("Enemy", "PlayerSkill1", 20.0f, 0.5f); // タスク1
    }

    private void FixedUpdate(){
        if(m_lockon_state){
            m_this_transform.rotation = Quaternion.Slerp(m_this_transform.rotation, Quaternion.LookRotation(m_position_list[m_array_set].position - m_this_transform.position), m_lockon_speed);

            if(Vector3.Dot((m_this_transform.position - m_position_list[m_array_set].position).normalized, m_this_transform.forward.normalized) < -0.8f) {
                m_time_count += Time.deltaTime;

                if(m_time_count > m_blazing_speed) {
                    m_time_count = 0.0f;

                    int l_array = BulletArrayGet();
                    m_bullet_script[l_array].Initialize(m_target_tag, m_bullet_tag);
                    m_bullet_script[l_array].BulletSetup(m_object_list[m_array_set], m_muzzle_flash.transform, 3.0f);

                    m_muzzle_flash.Play();
                }
            }

            if ((m_collider.ClosestPoint(m_position_list[m_array_set].position) != m_position_list[m_array_set].position) || (m_object_list[m_array_set].activeSelf == false)) {
                m_object_list.Clear();
                m_position_list.Clear();
                m_array_set = 0;
                m_lockon_state = false;
                m_collider.enabled = false;
                m_collider.enabled = true;
            }
        }else{
            if(m_object_list.Count > 0){
                float l_distance_check = 999.0f;

                for(int l_loop = 0; l_loop < m_object_list.Count; l_loop += 1){
                    if((m_this_transform.position - m_position_list[l_loop].position).sqrMagnitude < l_distance_check){
                        m_array_set = l_loop;
                        l_distance_check = (m_this_transform.position - m_position_list[l_loop].position).sqrMagnitude;
                    }
                }

                m_lockon_state = true;
            }
        }
    }

    private int BulletArrayGet() {
        int l_array = -1;

        for (int l_loop = 0; l_loop < m_bullet_list.Count; l_loop += 1) {
            if (m_bullet_list[l_loop].activeSelf == false) {
                l_array = l_loop;
                break;
            }
        }

        if (l_array == -1) {
            m_bullet_list.Add(Instantiate(m_attack_object, this.transform));
            m_bullet_script.Add(m_bullet_list[m_bullet_list.Count - 1].GetComponent<LockonBullet>());
            l_array = m_bullet_list.Count - 1;
        }

        return l_array;
    }

    private void OnTriggerEnter(Collider a_collider){
        if(m_lockon_state) return;

        if(a_collider.gameObject.tag == m_target_tag){
            m_object_list.Add(a_collider.gameObject);
            m_position_list.Add(a_collider.gameObject.GetComponent<Transform>());
        }
    }

    public void LockonStatusSet(string a_target_tag, string a_bullet_tag, float a_area, float a_blazing){
        m_target_tag = a_target_tag;
        m_bullet_tag = a_bullet_tag;
        m_collider.size = new Vector3(a_area, 10.0f, a_area);
        m_blazing_speed = a_blazing;
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数
    e_ 列挙型

    [説明]

    [バージョン]
    2021-01-18 ロックオン機能とロックオンが外れた場合に一番近いターゲットに切り替える機能
    2021-02-12 弾のオブジェクトプーリング及びターゲットまでの角度を参照して発射

    [タスク]
    タスク1 キャラクターのタワー設置時に別スクリプトからセットする
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColliderManager : MonoBehaviour{
    private float m_main_player_impact = 30.0f; // タスク1
    private float m_main_player_impact_damage = 200.0f; // タスク1
    private float m_main_player_skill1_damage = 50.0f; // タスク1

    public void ColliderDataInput(Collider a_collider, GameObject a_object, ref Vector3 a_vector, ref float a_damage){
        switch (a_collider.gameObject.tag) {
            case "Player":
                a_vector.Set(a_object.transform.position.x - a_collider.transform.position.x, 0f, a_object.transform.position.z - a_collider.transform.position.z);
                a_vector.Normalize();
                a_vector *= m_main_player_impact;
                a_damage = Random.Range(m_main_player_impact_damage * 0.8f, m_main_player_impact_damage * 1.2f);
            break;

            case "Finish":
                a_vector.Set(0f, 0f, 0f);
                a_damage = 9999.0f;
            break;

            case "PlayerSkill1":
                a_vector.Set(0f, 0f, 0f);
                a_damage = Random.Range(m_main_player_skill1_damage * 0.8f, m_main_player_skill1_damage * 1.2f);
                break;

            default:
                a_vector.Set(0f, 0f, 0f);
                a_damage = 0.0f;
            break;
        }
        if(a_collider.gameObject.tag == "Player"){
            
        }else if(a_collider.gameObject.tag == "Finish"){
            
        }else{
            
        }
    }

    public void MainPlayerColliderSet(float a_impact, float a_impact_damage, float a_skill1_damage){
        m_main_player_impact = a_impact;
        m_main_player_impact_damage = a_impact_damage;
        m_main_player_skill1_damage = a_skill1_damage;
    }
}

/*
    [規則]
    p_ 外部アクセス
    m_ メンバー変数
    l_ ローカル変数
    a_ 引数
    e_ 列挙型

    [説明]

    [バージョン]
    2021-01-28 吹き飛ばし機能の実装
    2021-02-03 体力システムの追加とそれに伴うコライダーの分岐
    2021-02-12 操作キャラクターのスキル1を追加

    [タスク]タスク1 戦闘開始時にMainPlayerColliderSetでステータスを反映させる
    
*/

ターゲットの方向を向いているか確認

if(Vector3.Dot((m_this_transform.position - m_position_list[m_array_set].position).normalized, m_this_transform.forward.normalized) < -0.8f)

回転させているオブジェクトとターゲットオブジェクトの位置との内積を求めてターゲットが視野に入っているか判定しています。
求められた値が【-1】なら角度一致ですが【0】に近づける事でオブジェクトの視野角を調整しています。


弾の生成と発射

int l_array = BulletArrayGet();
m_bullet_script[l_array].Initialize(m_target_tag, m_bullet_tag);
m_bullet_script[l_array].BulletSetup(m_object_list[m_array_set], m_muzzle_flash.transform, 3.0f);

m_muzzle_flash.Play();

ロックオン状態かつ前述した視野にターゲットを捉えている状態で一定周期ごとに弾を生成するようにしています。

同ファイル内の【BulletArrayGet】関数(後述します)を使用してリスト内の使用可能な弾の配列番号を取得します。

使用可能な弾にアタッチされているスクリプト内の【Initialize】関数で目標のタグセットと自身のタグ変更をしています。

同じく弾オブジェクトのスクリプト内【BulletSetup】関数で目標のオブジェクトと発射開始位置を設定しています。


private int BulletArrayGet() {
    int l_array = -1;

    for (int l_loop = 0; l_loop < m_bullet_list.Count; l_loop += 1) {
        if (m_bullet_list[l_loop].activeSelf == false) {
            l_array = l_loop;
            break;
        }
    }

    if (l_array == -1) {
        m_bullet_list.Add(Instantiate(m_attack_object, this.transform));
        m_bullet_script.Add(m_bullet_list[m_bullet_list.Count - 1].GetComponent<LockonBullet>());
        l_array = m_bullet_list.Count - 1;
    }

    return l_array;
}

弾オブジェクトを格納するリスト内で使用出来る(停止中)のオブジェクトが有れば配列番号を返し、停止中のオブジェクトが存在しない場合は新しく作成し配列番号を返すようにしています。


弾の性質

今回の弾が生成及び停止中から起こされた場合に初期化や設定をおこない引数で指定されたターゲットを追尾するようにしています。

public void Initialize(string a_target_tag, string a_bullet_tag) {
    m_collider = this.gameObject.GetComponent<BoxCollider>();
    m_delete_tag = a_target_tag;
    this.tag = a_bullet_tag;
}

生成時の初期化には自信のコライダー取得や標的のタグを設定および自身のタグを書き換えています。

標的のタグは標的着弾時に爆発処理に移動するために使用し、自身のタグ変更はどのキャラの攻撃か相手側がダメージ計算するために使用します。


public void BulletSetup(GameObject a_target, Transform a_position, float a_bullet_speed) {
    this.transform.position = a_position.position;
    this.gameObject.SetActive(true);
    m_target_position = a_target;
    m_bullet_speed = a_bullet_speed;
    // タスク1
    m_bullet_effect[0].Play();
    e_state = STATE.MOVE;
    m_collider.enabled = true;
}

発射時に呼ばれる関数には発射位置の設定オブジェクトのアクティブ化ターゲットオブジェクトの代入弾の速度代入等を代入し追尾を開始するようにしています。


Vector3 l_target_position = m_target_position.transform.position;
l_target_position.y += 0.4f;
this.transform.position = Vector3.MoveTowards(this.transform.position, l_target_position, Time.deltaTime * m_bullet_speed);

弾の目標地点については上方向に【0.4】上げるようにしています。
目標ターゲットが基本地面とほぼ同じ高さのオブジェクトの基準を持っているため、上げない場合は弾が地面に埋もれてしまうためです。


m_time_count += Time.deltaTime;

if (m_time_count > 1.0f) {
    // タスク1
    m_time_count = 0.0f;
    e_state = STATE.WAIT;
    m_collider.enabled = false;
    this.gameObject.SetActive(false);
}

着弾後の処理は1秒間コライダーをその場に残して自身を休息させています。

今後爆発の範囲攻撃の弾の場合には着弾時にコライダーを広げ休息前にコライダーを戻す作業を追加しようと考えています。


case "PlayerSkill1":
    a_vector.Set(0f, 0f, 0f);
    a_damage = Random.Range(m_main_player_skill1_damage * 0.8f, m_main_player_skill1_damage * 1.2f);
break;

【ColliderManager.cs】をスイッチ文に変更して今回使用する弾のタグが接触した場合のダメージや吹き飛び量を追加しました。

今回使用する弾は仮で吹き飛び無しダメージのばらつきが【*0.8 – *1.2】としています。


今回の修正点

【自作ゲーム開発4】の記事で変更したFixedUpdateの周期を16msから20msに戻します

エディタの動作とビルド後の挙動の違いについて詳しく理解していないため一度初期値に戻す事にしました。


【自作ゲーム開発7】の記事で作成したMobPigに追加したDamageTextを削除しました。

スクリプト内で生成するためMobPigに残っているDamageTextは使用されていません


【自作ゲーム開発7】の記事でアセットを使用して作成した文字のアニメーションについて、Fadeの設定を変更していなかったため【0.1】に変更します。


これらの修正点は進捗動画開始の初期に変更します。


今回使用するアセット


動画


記事のリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です