【Unity3D】学习笔记(第8记)游戏中的自动寻路算法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chy_xfn/article/details/79446320

  最近两天刚好有空研究了下游戏中的自动寻路功能,收获颇丰,感觉用相应的算法去解决相应的问题真的非常重要啊!至少比自己想的流水账逻辑流程管用。看来以后得花多点时间研究下算法方面的知识了。
  游戏中的自动寻路,顾名思义就是找路,从地图找到从起点到终点的可行最短路径。既然是从地图找路,那么地图就应该是可数据化的,要不怎么找呢。
  所以自动寻路的有两个重点是分别是地图数据化和搜索算法。
  地图数据化,3D游戏地图有点复杂,还没研究,今天先看看2D游戏的。比如,搞一张九宫格式的地图,那么每个格子至少需要有这样的数据,格子的坐标、是路还是墙(根据具体情况格子还可以拓展其他属性)。然后有了这份数据化的地图后,寻路就变成了从一个格子移动到另一个格子的过程。
  搜索算法,深度优先搜索(DFS)和广度优先搜索(BFS)算法,通常我们说DFS用栈方式实现,BFS用队列方式实现,为什么呢,下面我们来看看。

  1. 队列:起点P,将邻接点A、B、C、D存入队列,标记P为已查找,然后A出队,得邻接点A1、A2、A3、A4, 队列变成B、C、D、A1、A2、A3、A4,继续B出队得新队列C、D、A1、A2、A3、A4、B1、B2、B3、B4…。
  2. 栈:起点P,将邻接点A、B、C、D存入栈,标记P为已查找,然后D出栈,得邻接点D1、D2、D3、D4, 栈变成B、C、D1、D2、D3、D4,继续D4出栈得新栈B、C、D1、D2、D3、d1、d2、d3、d4…。
    邻接点:不考虑斜角方向,有上下左右四个。

      粗糙理解讲下就是队列是从根节点开始,搜索所有子节点,然后搜索所有子节点的所有子节点;而栈是从根节点开始,搜索所有子节点,选择某个子节点搜索所有子节点,找不到则回溯父节点继续操作(参考二叉树辅助图理解)。其实从上面来看,无论是栈还是队列,对某个点都是只能访问一次的,访问后立即出栈或出队。如果没处理好,出现重复访问某个点的话,容易出现死循环。
      运用到游戏中,那么起点就是二叉树的根节点,终点就是某个子节点,自动寻路就是找到从根节点到子节点的一条连接线。根节点每连接一个子节点时就记录下长度,最后根据长度就可以求出最短路径了。

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

public class CPathFind : MonoBehaviour {
    // 自定义地图,1可行,0障碍
    private const int Row = 8, Col = 9;
    private int[,] MapDataArr = new int[Row, Col]
    {
        {1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 1, 0, 0, 1, 0, 1, 1, 1},
        {1, 1, 1, 1, 1, 0, 0, 1, 1},
        {1, 1, 1, 0, 1, 1, 1, 1, 1},
        {1, 0, 0, 0, 1, 0, 1, 1, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1},
        {1, 0, 0, 1, 1, 0, 0, 0, 1},
        {1, 1, 1, 1, 1, 1, 1, 1, 1},
    };
    // 每个地图点数据化
    private class CMapPoint
    { 
        public int x;
        public int y;
        public int abstacle;
        public bool isFind = false;
        public int moveDis = 0;
        public CMapPoint fatherPoint;

        public CMapPoint(int x, int y, CMapPoint fp = null)
        {
            this.x = x;
            this.y = y;
            fatherPoint = fp;
        }
    }

    public GameObject wBlockPrefab;
    public GameObject bBlockPrefab;
    public GameObject playerPrefab;
    public GameObject blockGroupObj;

    private bool isMove = false;
    private int leastDis = 10000;
    private int findCount = 0;

    private CMapPoint endTargetPoint = new CMapPoint(0, 0);
    private CMapPoint moveTargetPoint = new CMapPoint(0, 0);
    private CMapPoint playerPoint = new CMapPoint(0, 0);

    private List<CMapPoint> MapPointList = new List<CMapPoint>();
    private List<CMapPoint> MovePointList = new List<CMapPoint>();

    private List<CMapPoint> OpenList = new List<CMapPoint>();
    private List<CMapPoint> CloseList = new List<CMapPoint>();

    void Start () {
        CreateMap();
    }

    void Update () {
        PlayerMove();
    }

    void OnDestroy()
    {
        for (int i = 0; i < blockGroupObj.transform.childCount; i++)
        {
            GameObject.Destroy(blockGroupObj.transform.GetChild(i).gameObject);
        }
    }

    void CreateMap()
    {
        for (int r = 0; r < MapDataArr.GetLength(0); r++)
        {
            for (int c = 0; c < MapDataArr.GetLength(1); c++)
            {
                CMapPoint mp = new CMapPoint(c, r) {
                    abstacle = MapDataArr[r, c]
                };
                MapPointList.Add(mp);

                GameObject blockPrefab = null;
                if (MapDataArr[r, c] == 1)
                    blockPrefab = wBlockPrefab;
                else
                    blockPrefab = bBlockPrefab;

                GameObject blockObj = GameObject.Instantiate(blockPrefab, Vector3.zero, Quaternion.identity) as GameObject;
                blockObj.name = r + "," + c;
                blockObj.transform.SetParent(blockGroupObj.transform);
                blockObj.transform.localScale = Vector3.one;
                // 左下角为原点
                blockObj.transform.localPosition = new Vector3(c * 60, r * 60, 0);
                blockObj.SetActive(true);
                blockObj.GetComponent<Button>().onClick.AddListener(delegate() { OnClickMapPoint(blockObj.name); });
            }
        }
    }

    void OnClickMapPoint(string name)
    {
        string[] nameArr = name.Split(',');
        int row = int.Parse(nameArr[0]);
        int col = int.Parse(nameArr[1]);
        Debug.Log("click point x=" +col + ", y=" + row);
        if (IsPass(col, row))
        {
            PathFinding(col, row);
        }
        else
        {
            Debug.Log("障碍点,无法通过!");
        }
    }

    void PlayerMove()
    {
        Vector3 targetPos = new Vector3(moveTargetPoint.x * 60, moveTargetPoint.y * 60, 0);
        float dValue = Vector3.Distance(playerPrefab.transform.localPosition, targetPos);
        if (dValue < 0.1)
        {
            playerPoint = new CMapPoint(moveTargetPoint.x, moveTargetPoint.y);
            GetNextMovePoint();
        }
        isMove = (dValue < 0.1) ? false : true;
        if (isMove)
        {
            playerPrefab.transform.localPosition = Vector3.MoveTowards(playerPrefab.transform.localPosition, targetPos, 2);
            Vector3 targetDir = (targetPos - playerPrefab.transform.localPosition).normalized;
            // 方向有变化,由Y轴指向目标方向
            if (targetDir != Vector3.zero)
            {
                playerPrefab.transform.up = targetDir;
            }
        }
    }

    void PathFinding(int x, int y)
    {
        leastDis = 10000;
        findCount = 0;
        endTargetPoint.x = x;
        endTargetPoint.y = y;
        endTargetPoint.fatherPoint = null;
        endTargetPoint.moveDis = 10000;
        MovePointList.Clear();

        //ShortestPathDFS();
        ShortestPathBFS();
    }

    void ShortestPathDFS()
    {
        // 传参数moveTargetPoint表示先移动完当前一格
        FindPathByDFS(moveTargetPoint, 0);
        GetLeastPath();
        Debug.Log("DFS Find Count: " + findCount);
    }

    // 使用深度优先搜索(利用堆栈)找出所有可能路线,求出最短路线
    void FindPathByDFS(CMapPoint curPoint, int dis)
    {
        int x = curPoint.x, y = curPoint.y;
        // (dis < leastDis)加这个条件可以优化计算量
        if (IsPass(x, y) && !GetIsFindStatus(curPoint) && dis < leastDis)
        {
            findCount += 1;
            //Debug.Log("find point x=" + x + ", y=" + y);
            if (x == endTargetPoint.x && y == endTargetPoint.y)
            {
                if (dis < leastDis)
                {
                    leastDis = dis;
                    endTargetPoint.fatherPoint = curPoint.fatherPoint;
                    //Debug.Log("end point x="+x + ", y="+y);
                }
                return;
            }
            // 记录已经查找过的点,避免重复查找导致堆栈溢出
            SetIsFindStatus(curPoint, true);
            // 利用递归方式搜索
            FindPathByDFS(new CMapPoint(x + 1, y, curPoint), dis + 1);
            FindPathByDFS(new CMapPoint(x - 1, y, curPoint), dis + 1);
            FindPathByDFS(new CMapPoint(x, y + 1, curPoint), dis + 1);
            FindPathByDFS(new CMapPoint(x, y - 1, curPoint), dis + 1);
            SetIsFindStatus(curPoint, false);
        }
    }

    // 根据终点倒序求出路线所有移动点
    void GetLeastPath()
    {
        CMapPoint mp = endTargetPoint;
        while (mp.fatherPoint != null)
        {
            MovePointList.Add(mp);
            mp = mp.fatherPoint;
        }
        MovePointList.Reverse();
    }

    //-------------------------------------------------------------//
    void ShortestPathBFS()
    {
        FindPathByBFS();
        GetLeastPath();
        Debug.Log("BFS Find Count: " + findCount);
    }

    // 使用广度优先搜索(利用队列),每次从队列取一个点出来,找出其可访问邻接点,直至找到终点
    void FindPathByBFS()
    {
        OpenList.Clear();
        CloseList.Clear();
        ClearMoveDisOfPoint();

        OpenList.Add(moveTargetPoint);
        CMapPoint mp = null;
        while(OpenList.Count > 0)
        {
            mp = OpenList[0];
            OpenList.RemoveAt(0);
            CloseList.Add(mp);
            FindAroundPoint(mp);
            mp = null;
        }
    }

    // 从访问点找出其可移动邻接点,记录从起点到该点的最短距离,存储该点到队列
    void FindAroundPoint(CMapPoint fatherPoint)
    {
        int x = fatherPoint.x, y = fatherPoint.y;
        for (int i = 0; i < 2; i++)
        {
            for (int j = 0; j < 2; j++)
            {
                int x1 = 0, y1 = 0;
                if (i == 0)
                {
                    x1 = x + (2 * j - 1);
                    y1 = y;
                }
                else
                {
                    x1 = x;
                    y1 = y + (2 * j - 1);
                }
                findCount += 1;
                //Debug.Log(findCount+ " find point x=" + x1 + ", y=" + y1);
                CMapPoint temp = new CMapPoint(x1, y1, fatherPoint);

                if (IsPass(x1, y1) && !InOpenList(temp) && !InCloseList(temp))
                {
                    //Debug.Log(findCount + " find point x=" + x1 + ", y=" + y1);
                    // 记录和起点之间的距离
                    temp.moveDis = fatherPoint.moveDis + 1;
                    if (x1 == endTargetPoint.x && y1 == endTargetPoint.y && (temp.moveDis < leastDis))
                    {
                        endTargetPoint.fatherPoint = temp.fatherPoint;
                        leastDis = temp.moveDis;
                        continue;
                    }
                    OpenList.Add(temp);
                }
            }
        }
    }

    // 清空所有格子记录的已移动距离
    void ClearMoveDisOfPoint()
    {
        foreach (var p in MapPointList)
        {
            p.moveDis = 0;
        }
    }

    // 设置搜索到某个格子时已移动距离
    void SetMoveDisOfPoint(int x, int y, int dis)
    {
        foreach (var p in MapPointList)
        {
            if (p.x == x && p.y == y)
            {
                p.moveDis = dis;
            }
        }
    }

    bool InOpenList(CMapPoint mp)
    {
        foreach (var p in OpenList)
        {
            if (p.x == mp.x && p.y == mp.y)
            {
                return true;
            }
        }
        return false;
    }

    bool InCloseList(CMapPoint mp)
    {
        foreach (var p in CloseList)
        {
            if (p.x == mp.x && p.y == mp.y)
            {
                return true;
            }
        }
        return false;
    }
    //-------------------------------------------------------------//

    // 获取当前格子查找状态
    bool GetIsFindStatus(CMapPoint mp)
    {
        foreach (var p in MapPointList)
        {
            if (p.x == mp.x && p.y == mp.y)
            {
                return p.isFind;
            }
        }
        return true;
    }

    // 设置当前格子是否已查找过了
    void SetIsFindStatus(CMapPoint mp, bool status)
    { 
        foreach(var p in MapPointList)
        {
            if (p.x == mp.x && p.y == mp.y)
            {
                p.isFind = status;
            }
        }
    }

    // 判断该点是否可通过(非障碍点且在地图内)
    bool IsPass(int x, int y)
    {
        if (0 <= x && x < Col && 0 <= y && y < Row)
        {
            foreach (var p in MapPointList)
            { 
                if (p.x == x && p.y == y)
                {
                    return (p.abstacle == 1);
                }
            }
        }
        return false;
    }

    // 曼哈顿距离
    int ManhattanDistance(int x1, int y1, int x2, int y2)
    {
        return (Mathf.Abs(x1 - x2) + Mathf.Abs(y1 - y2));
    }

    // 获取下一个移动点(每次移动一格)
    void GetNextMovePoint()
    {
        if (MovePointList.Count > 0)
        {
            // 不能直接赋值,注意对象的引用关系
            moveTargetPoint = new CMapPoint(MovePointList[0].x, MovePointList[0].y);
            MovePointList.RemoveAt(0);
            //Debug.Log("next move point x=" + moveTargetPoint.x + ", y=" + moveTargetPoint.y);
        }
    }
}

》》》》戳我下载完整项目》》》》

猜你喜欢

转载自blog.csdn.net/chy_xfn/article/details/79446320