日々是好日~every day is a good day~

日常の中の非日常の備忘録

【Unity】迷路探索のアルゴリズム

Glitchの移行作業でしばらく間を置いてしまいましたが 迷路ゲームのアルゴリズムについて
everydayisagoodday.hatenadiary.com
今回は迷路探索のアルゴリズムです
まず 迷路検索のアルゴリズムを調べようと思った経緯から
プレーヤーがスタートしたら10秒おきにゾンビを発生させています
初めはランダムに動き回るようにしていたのですが それだと行き止まりに迷い込んだり 同じ所を行ったり来たりして プレーヤーにとって少しも脅威ではないんですね(笑)
これではイカンと ゾンビがプレーヤーに最短距離でやってくるにはどうしたら良いか…
『迷路 最短経路』と検索すると幅優先探索というアルゴリズムが良さそう
次に『迷路 幅優先探索』と検索すると 非常に分かりやすい説明がありました
こちら
qiita.com
この投稿者さんもおっしゃってますが @E869120さんがの動画が天才的にすばらしいです(私ごときが僭越ですが)
動画を見終わった時点でコードがぼんやり見えた気がしました(笑)
この方法でゾンビをプレーヤーの元へ集結させようと思います

using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem.HID;
using static UnityEngine.ParticleSystem;

public class ZombieControll : MonoBehaviour
{
    public float zombieSpeed;   //ゾンビの移動スピード
    public bool die=false;         //ゾンビが有効か無効か
    public GameObject effectObj; //エフェクトプレファブ
    GameObject effect;            //Effectオブジェクト
    GameObject player;          //Playerオブジェクト
    GameObject gameManager;     //GameManagerオブジェクト
    GameObject soundManager;    //SoundManagerオブジェクト
    GameObject movePoint;       //MovePointオブジェクト
    Animator animator;          //アニメーター
    int pointCount;                //移動ポイントの数
    Vector3[] pointPosition;    //移動ポイントの位置
    Vector3 nextPosition;       //次に向かうポイントの位置
    
    Queue<int> queue = new Queue<int>();  //経路探索用キュー
    public int[] stepArray;     //Playerから各移動ポイントまでのステップ数
    bool isMoving=false;        //移動中かどうか
    GameObject child;           //Zombieオブジェクトの子オブジェクト(マテリアル変更用)
    
    // Start is called before the first frame update
    void Start()
    {
        //オブジェクトの取得
        player = GameObject.Find("Player");
        gameManager = GameObject.Find("GameManager");
        soundManager = GameObject.Find("SoundManager");
        movePoint = GameObject.Find("MovePoint");
        animator = GetComponent<Animator>();
        child = transform.GetChild(1).gameObject;

        //移動ポイントをpointPosition配列に設定
        pointCount = movePoint.transform.childCount;
        pointPosition = new Vector3[pointCount];
        for (int i = 0; i < pointCount; i++)
        {
            pointPosition[i] = movePoint.transform.GetChild(i).transform.position;
        }
        stepArray = new int[pointCount];
        GetNextPosition(0);
    }

    // Update is called once per frame
    void Update()
    {
        //ゲーム終了時
        if (!gameManager.GetComponent<GameManager>().isRunning)
        {
            animator.SetTrigger("Stop");
            child.GetComponent<Renderer>().material.color = new UnityEngine.Color(1f,1f,1f,0.6f);
            return;
        }
        //移動していないなら移動開始
        if (!isMoving)
        {
            StartCoroutine(Move(nextPosition));
            isMoving = true;
        }
        //PlayerとZombieの距離が0.17以下ならZombieの勝ち
        Vector3 pp = new Vector3(player.transform.position.x,GameManager.zombieY,player.transform.position.z);
        if (!die && Vector3.Distance(pp, transform.position) < 0.17f)
        {
            soundManager.GetComponent<SoundManager>().AttackSound();
            gameManager.GetComponent<GameManager>().zombieWin = true;
            transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward,
                         Camera.main.transform.rotation * Vector3.up);
            animator.SetTrigger("Stop");
        }
    }

    //次に移動するポイントを取得する
    private void GetNextPosition(int state)  //state:0->オブジェクト生成時 0以外->移動中
    {
        //移動前のZombieのポイントNo
        Vector3 beforePosition;
        int zombiePoint = -1;
        if (state == 0)
        {
            beforePosition = transform.position;
        }
        else
        {
            beforePosition = nextPosition;
        }
        for (int i = 0; i < pointCount; i++)
        {
            if (pointPosition[i].x == beforePosition.x && pointPosition[i].z == beforePosition.z)
            {
                zombiePoint = i;
                break;
            }
        }
        //StepArrayを初期化
        for (int i=0;i<stepArray.Length;i++)
        {
            stepArray[i] = -1;
        }
        queue.Clear();   //キュー初期化

        //Playerの位置に一番近いポイントのステップを0にする
        for (int i = 0; i < pointCount; i++)
        {
            int x = (int)Math.Round(player.GetComponent<Transform>().position.x);
            int z = (int)Math.Round(player.GetComponent<Transform>().position.z);
            if (pointPosition[i].x == x && pointPosition[i].z == z)
            {
                
                stepArray[i] = 0;
                queue.Enqueue(i);
                break;
            }
        }
        
        //Playから各移動ポイントまでのステップ数を設定
        while (queue.Count > 0) // キューが空でない限りループ
        {
            int no=queue.Dequeue();   //検索を開始するポイントNo
            int stepCount = stepArray[no] + 1;
            for (int i = 0; i < stepArray.Length  ; i++)
            {
                if (stepArray[i] != -1) continue;   //ステップが確定している場合はスキップ
                if ((pointPosition[i].x == pointPosition[no].x + 1f && pointPosition[i].z == pointPosition[no].z)
                 || (pointPosition[i].x == pointPosition[no].x - 1f && pointPosition[i].z == pointPosition[no].z)
                 || (pointPosition[i].z == pointPosition[no].z + 1f && pointPosition[i].x == pointPosition[no].x)
                 || (pointPosition[i].z == pointPosition[no].z - 1f && pointPosition[i].x == pointPosition[no].x))
                {
                    stepArray[i] = stepCount;
                    queue.Enqueue(i);
                }
            }
        }
        //Zombieの位置のステップ数-1で位置が1だけ違うポイントを次の位置に設定
        bool next=false;
        for (int i=0;i<stepArray.Length;i++)
        {
            if (zombiePoint == -1) break;   //Zombieの位置がポイントではない
            if (stepArray[i] == stepArray[zombiePoint]-1)
            {
                if ((pointPosition[i].x == beforePosition.x + 1f && pointPosition[i].z == beforePosition.z)
                 || (pointPosition[i].x == beforePosition.x - 1f && pointPosition[i].z == beforePosition.z)
                 || (pointPosition[i].z == beforePosition.z + 1f && pointPosition[i].x == beforePosition.x)
                 || (pointPosition[i].z == beforePosition.z - 1f && pointPosition[i].x == beforePosition.x))
                    nextPosition = pointPosition[i];
                next = true;
            }
        }
        if (!next)   //移動するポイントがない場合、移動先はPlayer
        {
            nextPosition = new Vector3(player.gameObject.transform.position.x, GameManager.zombieY, player.gameObject.transform.position.z);
        }
        //次の移動先によってZombieの向きを変える
        Vector3 worldAngle = transform.eulerAngles;
        
        if (nextPosition.x <transform.position.x)       //前向き
        {
            worldAngle.y = -90f;
            transform.eulerAngles = worldAngle;
        }
        else if (nextPosition.x >transform.position.x)  //後ろ向き
        {
            worldAngle.y = 90f;
            transform.eulerAngles = worldAngle;
        }
        else if (nextPosition.z < transform.position.z)  //左向き
        {
            worldAngle.y = 180f;
            transform.eulerAngles = worldAngle;
        }
        else if (nextPosition.z > transform.position.z)  //右向き
        {
            worldAngle.y = 0f;
            transform.eulerAngles = worldAngle;
        }
        else
        {
            transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward,
                         Camera.main.transform.rotation * Vector3.up);
        }
        return;
    }
    private void OnTriggerEnter(Collider other)
    {
        //弾に当たったとき
        if (other.gameObject.tag == "Bullet")
        {
            die = true;   //このZombieは無効
            soundManager.GetComponent<SoundManager>().audioSourceAlert.Stop();
            animator.SetTrigger("Die");   //アニメーションDie
            effect = Instantiate(effectObj);   //エフェクト生成
            effect.transform.position = transform.position;   //エフェクトの位置をZombieの位置に
            StartCoroutine(FadeOut());   //Zombieフェードアウト
        }
        //有効なZombieがPlayerに当たったとき
        else if (other.gameObject.tag == "Player" && !die)
        {
            animator.SetTrigger("Attack");   //アニメーションAttack
        }
    }

    //Zombieフェードアウト
    private IEnumerator FadeOut()
    {
        float alpha;
        float red, green, blue;
        red = child.GetComponent<Renderer>().material.color.r;
        green = child.GetComponent<Renderer>().material.color.g;
        blue = child.GetComponent<Renderer>().material.color.b;
        alpha = child.GetComponent<Renderer>().material.color.a;
        
        while (alpha >= 0)
        {
            alpha -= 0.1f;
            child.GetComponent<Renderer>().material.color = new UnityEngine.Color(red,green,blue,alpha);
            yield return new WaitForSeconds(0.1f);
        }
        
        StartCoroutine(EraseEfect());   //エフェクト削除
    }

    //エフェクトおよびZombieオブジェクトを1.5秒後に消す
    IEnumerator EraseEfect()
    {
        yield return new WaitForSeconds(1.5f);
        Destroy(effect);
        gameObject.SetActive(false);

    }
    //Zombie生成時フェードイン
    public IEnumerator FadeIn()
    {

        float alpha;
        float red, green, blue;
        child = transform.GetChild(1).gameObject;
        red = child.GetComponent<Renderer>().material.color.r;
        green = child.GetComponent<Renderer>().material.color.g;
        blue = child.GetComponent<Renderer>().material.color.b;
        alpha = child.GetComponent<Renderer>().material.color.a;
        while (alpha <= 1)
        {
            alpha += 0.01f;
            child.GetComponent<Renderer>().material.color = new UnityEngine.Color(red, green, blue, alpha);
            yield return new WaitForSeconds(0.2f);
        }
    }
    
    //Zombieの移動
    IEnumerator Move(Vector3 targetPos)   //targetPos:移動先の位置
    {
        //現在とターゲットの位置が違うなら、近づけ続ける
        while ((targetPos - transform.position).sqrMagnitude > Mathf.Epsilon)
        {
            if (!gameManager.GetComponent<GameManager>().isRunning) break;
            //移動
            transform.position = Vector3.MoveTowards(transform.position, targetPos, zombieSpeed * Time.deltaTime);   //目標に近づく
            if (targetPos == transform.position) break;   //目的地到達
           
            yield return null;
        }
        //次の移動先を取得
        GetNextPosition(1);
        isMoving = false;   //移動していない
    }
}

ゾンビの動きをZombieControllクラスにまとめているので アルゴリズムに関係ないコードも含まれています
プレーヤーに向かう最短経路の検索はGetNextPositionという関数内で求めています
ここに出てくる移動ポイントとは迷路を作成したときに壁ではない所 つまり移動可能な所に目印として置いた空のオブジェクトのことです
アルゴリズムのコードはこの辺です

//Playerの位置に一番近いポイントのステップを0にする
for (int i = 0; i < pointCount; i++)
{
    int x = (int)Math.Round(player.GetComponent<Transform>().position.x);
    int z = (int)Math.Round(player.GetComponent<Transform>().position.z);
    if (pointPosition[i].x == x && pointPosition[i].z == z)
    {  
         stepArray[i] = 0;
         queue.Enqueue(i);
         break;
     }
}
//Playから各移動ポイントまでのステップ数を設定
while (queue.Count > 0) // キューが空でない限りループ
{
     int no=queue.Dequeue();   //検索を開始するポイントNo
     int stepCount = stepArray[no] + 1;
     for (int i = 0; i < stepArray.Length  ; i++)
     {
          if (stepArray[i] != -1) continue;   //ステップが確定している場合はスキップ
          if ((pointPosition[i].x == pointPosition[no].x + 1f && pointPosition[i].z == pointPosition[no].z)
           || (pointPosition[i].x == pointPosition[no].x - 1f && pointPosition[i].z == pointPosition[no].z)
           || (pointPosition[i].z == pointPosition[no].z + 1f && pointPosition[i].x == pointPosition[no].x)
           || (pointPosition[i].z == pointPosition[no].z - 1f && pointPosition[i].x == pointPosition[no].x))
          {
                stepArray[i] = stepCount;
                queue.Enqueue(i);
           }
     }
}
//Zombieの位置のステップ数-1で位置が1だけ違うポイントを次の位置に設定
bool next=false;
for (int i=0;i<stepArray.Length;i++)
{
     if (zombiePoint == -1) break;   //Zombieの位置がポイントではない
     if (stepArray[i] == stepArray[zombiePoint]-1)
     {
          if ((pointPosition[i].x == beforePosition.x + 1f && pointPosition[i].z == beforePosition.z)
           || (pointPosition[i].x == beforePosition.x - 1f && pointPosition[i].z == beforePosition.z)
           || (pointPosition[i].z == beforePosition.z + 1f && pointPosition[i].x == beforePosition.x)
           || (pointPosition[i].z == beforePosition.z - 1f && pointPosition[i].x == beforePosition.x))
              nextPosition = pointPosition[i];
           next = true;
      }
}

これでゾンビがプレーヤーに確実に迫ってくるので ゲームとして面白くなりました
アルゴリズムたのしー