質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.32%

【Unity】自動で迎撃する固定砲台の処理について

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 1,045

shukrin

score 11

前提・実現したいこと

Unityを用いて2Dゲームを作っています。固定砲台の索敵範囲内に敵がいればその中からランダムに1体を選んでそちらを向いて発砲する、ということを実現したいと思っています。

該当のソースコード

まずオブジェクト同士の親子構造として、Turret.csがアタッチされたゲームオブジェクトTurretがあり、その子オブジェクトEnemySearchAreaには索敵用のスクリプトEnemySearchArea.csとサークルコライダーがアタッチされています。

索敵から攻撃までの流れは以下の通りです。
1、EnemySearchAreaのサークルコライダーに敵が接触したら、その敵のゲームオブジェクトをリストに追加
2、EnemySearchAreaのサークルコライダーから敵が出たら、その敵のゲームオブジェクトをリストから削除
3、リストの長さが0でない(=索敵範囲内に敵がいる)場合、その中からランダムに1体を選んで、そちらの方を向いて発砲
4、発砲するとゲームオブジェクトBulletが生成され、敵に当たるとダメージ処理を行う

以下がEnemySearchArea.csとなります。このスクリプトは索敵および索敵範囲内にいる敵リストの保持と、親オブジェクトであるTurretへの回転と発砲の指示を担当しています。

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

public class EnemySearchArea : MonoBehaviour
{
    public bool setTarget; //ターゲットが設定されているか否か
    private List<GameObject> targetList = new List<GameObject>(); //索敵範囲内にいるターゲットのリスト

    // Start is called before the first frame update
    void Start()
    {
        setTarget = false;
    }

    // Update is called once per frame
    void Update()
    {
        if (!setTarget && targetList.Count > 0) //ターゲットがセットされておらず、かつ索敵範囲内に敵がいる場合
        {
            GameObject target = targetList[Random.Range(0, targetList.Count)]; //ターゲットをランダムに選択
            setTarget = true;
            this.transform.parent.GetComponent<Turret>().CoRotate(target);
            this.transform.parent.GetComponent<Turret>().CoShot();
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        targetList.Add(collision.gameObject);
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        targetList.Remove(collision.gameObject);
    }
}

EnemySearchAreaからの指示を受けてTurret.csではゲームオブジェクトTurretの回転と発砲(ゲームオブジェクトBulletの生成)を行います。

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

public class Turret : MonoBehaviour
{
    public GameObject bullet;
    public float coolTime; //発砲間隔
    public int shotsNum; //連射数
    public float turningSpeed; //旋回速度

    IEnumerator Shot() //発砲コルーチン
    {
        for(int i = 0; i <= shotsNum; i++){ //0.1秒ごとに発射
            Instantiate(bullet, transform.position, transform.rotation, transform);
            yield return new WaitForSeconds(0.1f);
        }
        yield return new WaitForSeconds(coolTime); //連射後のクールタイム
        this.transform.GetChild(1).GetComponent<EnemySearchRange>().setTarget = false; //ターゲットを未セットにしてコルーチン終了
        yield break;
    }

    IEnumerator Rotate(GameObject target) //targetの方を向くまで一定速度で回転
    {
        while (Vector2.Angle(target.transform.position - this.transform.position, transform.up.normalized) > 5f)
        {
            this.GetComponent<Rigidbody2D>().angularVelocity = (Vector2.SignedAngle(transform.up.normalized, target.transform.position - this.transform.position) < 0 ? -1f : 1f) * turningSpeed;
            yield return null;
        }
        this.GetComponent<Rigidbody2D>().angularVelocity = 0f;
        yield break;
    }

    public void CoShot() //外部からShotコルーチンを呼ぶためのメソッド
    {
        StartCoroutine("Shot");
    }

    public void CoRotate(GameObject target) //外部からRotateコルーチンを呼ぶためのメソッド
    {
        StartCoroutine(Rotate(target));
    }
}

Turretから発射されたゲームオブジェクトBulletは敵と接触するとダメージ処理を行います。ダメージ処理の内容は敵にアタッチされたスクリプトCharacter.csのDamaged()メソッドに書かれています。

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

public class Bullet : MonoBehaviour
{
    public float speed; //弾速
    public float damage; //1発当たりのダメージ量

    private void OnTriggerEnter2D(Collider2D collision)
    {
        string layerName = LayerMask.LayerToName(collision.gameObject.layer); //衝突したオブジェクトのレイヤー名を取得

        if (layerName == "Enemy" || layerName == "Player" )
        {
            collision.GetComponent<Character>().Damaged(damage, GetComponent<Rigidbody2D>().velocity);
            Destroy(this.gameObject);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Character : MonoBehaviour
{
    public float maxHP;
    private float HP;


    public void Damaged(float damage, Vector2 dmgDirection) //ダメージ処理
    {
        this.HP = this.HP - damage;

        if (this.HP < 0)
        {
            Destroy(gameObject);
        }

        this.GetComponent<Rigidbody2D>().AddForce(dmgDirection.normalized, ForceMode2D.Impulse); //ノックバック

    }

    // Start is called before the first frame update
    void Start()
    {
        this.HP = maxHP;
    }

}

発生している問題・エラーメッセージ

実際に上記のコードを実行してみたところEnemySearchAreaの中のCoRotate()にtargetを渡すところで以下のようなエラーが発生しました。

MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.

先ほどのEnemyearchArea.csでは索敵範囲内の敵がHP0になって消滅するパターンを考えていなかったため、既にDestroyされた敵をターゲットにしようとしてエラーが発生したようです。

教えていただきたいこと

それぞれのEnemySearchAreaクラスのインスタンスのtargetListからある敵のゲームオブジェクトを一斉に削除するような方法はないでしょうか?もしそのような方法があればCharacter.csのDamaged()メソッドのDestroyの直前に行うことで、上記の問題が解決するのではないかと思っています。
またもし別の解決方法(コードを根本的に変更する必要のあるものでも構いません)があればぜひとも教えていただけないでしょうか。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

GameObjectがいつの間にかいなくなってたはゲーム制作あるあるですね。

基本的にGameObjectをDestroyするときはGameObject自身でDestroyせず、DestroyManagerのようなオブジェクトにDestroyを依頼する形にします。DestroyManagerはフレームの最後(GameObject同士のやり取りがなくなったとき)にまとめてDestroyします。
その場合、EnemySearchAreaはUpdate毎に範囲内のGameObjectを探すようにします。

別の方法としてはGameObjectがDestroyするときにEnemySearchAreaにリストから自分自身を削除するよう依頼するようにします。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/12/30 00:35

    DestroyManagerにDestroyを依頼したとしても、結局targetListにはDestroyされたゲームオブジェクトが残ったままではないでしょうか?

    キャンセル

  • 2019/12/30 00:48

    スミマセン。まさにその通りでございます。回答を修正しました。

    キャンセル

  • 2019/12/30 16:27

    何度もすみません。

    >別の方法としてはGameObjectがDestroyするときにEnemySearchAreaにリストから自分自身を削除するよう依頼するようにします。

    こちらの方法に関してなのですが、すべてののインスタンスのリストから自身を削除するには具体的にどうすればよろしいのでしょうか。

    キャンセル

  • 2020/01/01 13:40

    ゲームの進行を管理するGameObject(GameMasterとする)が存在すると思いますが、そのGameMasterに配列を持たせて
    DestroyするGameObjectはGameMasterの配列に自身を追加します。その後、フレームの最後でGameMasterが配列に登録されているGameObjectをDestroyします。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.32%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る