Unity3D仿星露谷物语开发61之寻路

1、目标

我们将使用AStar算法来生成一个场景中两点之间的路径,并使用测试脚本显示这条路径。

要实现的效果如下:

2、AStart算法概念

(1)寻路的定义

寻路就是通过计算机程序找到两点之间的最短路线,在游戏中比如让角色从一个位置移动到另一个位置,需要规划出合理的移动路线 。可以把它理解为解决迷宫问题的实际应用,在迷宫里找从入口到出口的最短路线,在游戏场景中就是找角色起点到终点的最短路径。

(2)AStar(A*)算法

其中G是确定的移动步数,每次计算时增加一步。

H是不确定的,可采用曼哈顿距离或者欧式距离两种方式进行计算。

节点信息表示如下:

举例如下:

【采用欧式距离计算H的情况】

单独查看终点附近的点,即可以定位到通往终点的最小总代价的点:

然后通过该点逆推即可找到最佳路径:

【采用曼哈顿距离计算H的情况】

单独查看终点附近的点,即可以定位到通往终点的最小总代价的点:

然后通过该点逆推即可找到最佳路径:

上述的解释参考:六分钟!带你掌握A星算法的原理!!_哔哩哔哩_bilibili

上述是原理,实际计算时我们按照曼哈顿距离计算,规定横向及纵向为10,斜向为14(每个方格距离为1,一般乘以10计算结果)

另外增加2个概念:

  • Open List:代检测节点 容器(装候选节点的容器)
  • Close List:已检测节点 容器(防止同一个节点多次出现在路径中)

例子1(没有障碍的情况):

例子2(有障碍的情况):

3、编写代码环节

(1)修改GridPropertiesManager.cs脚本

增加获取网格维度信息的方法,给AStar算法使用。

添加函数如下:

    /// <summary>
    /// for sceneName this method returns a Vector2Int with the grid dimensions for that scene, or Vector2Int.zero if scene not found
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="gridDimensions"></param>
    /// <param name="gridOrigin"></param>
    /// <returns></returns>
    public bool GetGridDimensions(SceneName sceneName, out Vector2Int gridDimensions, out Vector2Int gridOrigin)
    {
        gridDimensions = Vector2Int.zero;
        gridOrigin = Vector2Int.zero;

        // loop through scenes
        foreach(SO_GridProperties so_GridProperties in so_gridPropertiesArray)
        {
            if(so_GridProperties.sceneName == sceneName)
            {
                gridDimensions.x = so_GridProperties.gridWidth;
                gridDimensions.y = so_GridProperties.gridHeight;

                gridOrigin.x = so_GridProperties.originX;
                gridOrigin.y = so_GridProperties.originY;

                return true;
            }
        }

        return false;
    }

(2)创建Node.cs脚本

在Assets -> Scripts下创建目录命名为AStar,在该目录下创建脚本命名为Node.cs。

用于存储每个寻径节点的详细信息。

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

public class Node : IComparable<Node>
{
    public Vector2Int gridPosition;
    public int gCost = 0; // distance from starting node
    public int hCost = 0; // distance from finishing node
    public bool isObstacle = false;
    public int movementPenalty;
    public Node parentNode;

    public Node(Vector2Int gridPosition)
    {
        this.gridPosition = gridPosition;
        parentNode = null;
    }

    public int FCost
    {
        get
        {
            return gCost + hCost;
        }
    }


    public int CompareTo(Node nodeToCompare)
    {
        int compare = FCost.CompareTo(nodeToCompare.FCost);

        if (compare == 0) 
        {
            compare = hCost.CompareTo(nodeToCompare.hCost);
        }

        return compare;
    }


}

(3)创建GridNodes.cs脚本

在Assets -> Scripts -> AStar下创建GridNodes.cs脚本,用于存储grid的二维网格信息。

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

public class GridNodes
{
    private int width;
    private int height;

    private Node[,] gridNode;

    public GridNodes(int width, int height)
    {
        this.width = width;
        this.height = height;

        gridNode = new Node[width, height];

        for(int x = 0; x < width; x++)
        {
            for(int y = 0; y < height; y++)
            {
                gridNode[x, y] = new Node(new Vector2Int(x, y));
            }
        }
    }

    public Node GetGridNode(int xPosition, int yPosition)
    {
        if(xPosition < width && yPosition < height)
        {
            return gridNode[xPosition, yPosition];
        }
        else
        {
            Debug.Log("Requested grid node is out of range");
            return null;
        }
    }
}

(4)创建NPCMovementStep.cs脚本

在Assets -> Scripts下创建目录命名为NPC,在该目录下创建新的脚本命名为NPCMovementStep.cs。

把NPC走过的每一步记录在这个数据容器里。

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

public class NPCMovementStep
{
    public SceneName sceneName;
    public int hour;
    public int minute;
    public int second;
    public Vector2Int gridCoordinate;
}

(5)创建AStar.cs脚本

在Assets -> Scripts -> AStar下创建AStar.cs脚本

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

public class AStar : MonoBehaviour
{
    [Header("Tiles & Tilemap References")]
    [Header("Options")]
    [SerializeField] private bool observeMovementPenalties = true;


    [Range(0, 20)]
    [SerializeField] private int pathMovementPenalty = 0;
    [Range(0, 20)]
    [SerializeField] private int defaultMovementPenalty = 0;

    private GridNodes gridNodes;
    private Node startNode;
    private Node targetNode;
    private int gridWidth;
    private int gridHeight;
    private int originX;
    private int originY;

    private List<Node> openNodeList;
    private HashSet<Node> closedNodeList;

    private bool pathFound = false;

    /// <summary>
    /// Builds a path for the given sceneName, from the startGridPosition to the endGridPosition, and adds movement steps to the passed in npcMovementStack,
    /// Also returns true if path found or false if no path found
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="startGridPosition"></param>
    /// <param name="endGridPosition"></param>
    /// <param name="npcMovementStepStack"></param>
    /// <returns></returns>
    public bool BuildPath(SceneName sceneName, Vector2Int startGridPosition, Vector2Int endGridPosition, Stack<NPCMovementStep> npcMovementStepStack)
    {
        if(PopulateGridNodesFromGridPropertiesDictionary(sceneName, startGridPosition, endGridPosition))
        {
            if (FindShortestPath())
            {
                UpdatePathOnNPCMovementStepStack(sceneName, npcMovementStepStack);

                return true;
            }
        }

        return false;
    }

    private void UpdatePathOnNPCMovementStepStack(SceneName sceneName, Stack<NPCMovementStep> npcMovementStepStack)
    {
        Node nextNode = targetNode;

        while (nextNode != null)
        {
            NPCMovementStep nPCMovementStep = new NPCMovementStep();

            nPCMovementStep.sceneName = sceneName;
            nPCMovementStep.gridCoordinate = new Vector2Int(nextNode.gridPosition.x + originX, nextNode.gridPosition.y + originY);

            npcMovementStepStack.Push(nPCMovementStep);

            nextNode = nextNode.parentNode;
        }
    }


    /// <summary>
    /// return true if a path has been found
    /// </summary>
    private bool FindShortestPath()
    {
        // Add start node to open list
        openNodeList.Add(startNode);

        // Loop through open node list until empty
        while(openNodeList.Count > 0)
        {
            // Sort List
            openNodeList.Sort();

            // current node = the node in the open list with the lowest fCost
            Node currentNode = openNodeList[0];
            openNodeList.RemoveAt(0);

            // add current node to the closed list
            closedNodeList.Add(currentNode);

            // if the current node = target node then finish
            if(currentNode == targetNode)
            {
                pathFound = true;
                break;
            }

            // evaluate fcost for each neighbour of the current node
            EvaluateCurrentNodeNeighbours(currentNode);
        }

        if (pathFound)
        {
            return true;
        }
        else
        {
            return false;
        }
    }


    private void EvaluateCurrentNodeNeighbours(Node currentNode)
    {
        Vector2Int currentNodeGridPosition = currentNode.gridPosition;

        Node validNeighbourNode;

        // Loop through all direction
        for(int i = -1; i <= 1; i++)
        {
            for(int j = -1; j <=1; j++)
            {
                if (i == 0 && j == 0)
                    continue;

                validNeighbourNode = GetValidNodeNeighbour(currentNodeGridPosition.x + i, currentNodeGridPosition.y + j);

                if(validNeighbourNode != null)
                {
                    // Calculate new gcost for neighbour
                    int newCostToNeighbour;

                    if (observeMovementPenalties)
                    {
                        newCostToNeighbour = currentNode.gCost + GetDistance(currentNode, validNeighbourNode) + validNeighbourNode.movementPenalty;
                    }
                    else
                    {
                        newCostToNeighbour = currentNode.gCost + GetDistance(currentNode, validNeighbourNode);
                    }

                    bool isValidNeighbourNodeInOpenList = openNodeList.Contains(validNeighbourNode);

                    if(newCostToNeighbour < validNeighbourNode.gCost || !isValidNeighbourNodeInOpenList)
                    {
                        validNeighbourNode.gCost = newCostToNeighbour;
                        validNeighbourNode.hCost = GetDistance(validNeighbourNode, targetNode);
                        validNeighbourNode.parentNode = currentNode;

                        if (!isValidNeighbourNodeInOpenList)
                        {
                            openNodeList.Add(validNeighbourNode);
                        }
                    }

                }
            }
        }
    }


    private int GetDistance(Node nodeA, Node nodeB)
    {
        int dstX = Mathf.Abs(nodeA.gridPosition.x - nodeB.gridPosition.x);
        int dstY = Mathf.Abs(nodeA.gridPosition.y - nodeB.gridPosition.y);

        if(dstX > dstY)
            return 14 * dstY + 10 * (dstX - dstY);
        return 14 * dstX + 10 * (dstY - dstX);
    }


    private Node GetValidNodeNeighbour(int neighbourNodeXPosition, int neighbourNodeYPosition)
    {
        // If neighbour node position is beyond grid then return null
        if(neighbourNodeXPosition >= gridWidth || neighbourNodeXPosition < 0 || neighbourNodeYPosition >= gridHeight || neighbourNodeYPosition < 0)
        {
            return null;
        }

        // if neighbour is an obstacle or neighbour is in the closed list then skip
        Node neighbourNode = gridNodes.GetGridNode(neighbourNodeXPosition, neighbourNodeYPosition);

        if(neighbourNode.isObstacle || closedNodeList.Contains(neighbourNode))
        {
            return null;
        }
        else
        {
            return neighbourNode;
        }
    }



    private bool PopulateGridNodesFromGridPropertiesDictionary(SceneName sceneName, Vector2Int startGridPosition, Vector2Int endGridPosition)
    {
        // Get grid properties dictionary for the scene
        SceneSave sceneSave;

        if(GridPropertiesManager.Instance.GameObjectSave.sceneData.TryGetValue(sceneName.ToString(), out sceneSave)) // 获取场景数据
        {
            // Get Dict grid property details
            if(sceneSave.gridPropertyDetailsDictionary != null)
            {
                // Get grid height and width
                if(GridPropertiesManager.Instance.GetGridDimensions(sceneName, out Vector2Int gridDimensions, out Vector2Int gridOrigin))
                {
                    // Create nodes grid based on grid properties dictionary
                    gridNodes = new GridNodes(gridDimensions.x, gridDimensions.y);
                    gridWidth = gridDimensions.x;
                    gridHeight = gridDimensions.y;
                    originX = gridOrigin.x;
                    originY = gridOrigin.y;

                    // Create openNodeList
                    openNodeList = new List<Node>();

                    // Create closed Node List
                    closedNodeList = new HashSet<Node>();
                }
                else
                {
                    return false;
                }

                // Populate start node(网格的原点在地图的中间,通过如下转化可以使原点位于左下角)
                startNode = gridNodes.GetGridNode(startGridPosition.x - gridOrigin.x, startGridPosition.y - gridOrigin.y);  

                // Populate target node
                targetNode = gridNodes.GetGridNode(endGridPosition.x - gridOrigin.x, endGridPosition.y - gridOrigin.y);

                // Populate obstacle and path info for grid
                for(int x = 0; x < gridDimensions.x; x++)
                {
                    for(int y = 0; y < gridDimensions.y; y++)
                    {
                        GridPropertyDetails gridPropertyDetails = GridPropertiesManager.Instance.GetGridPropertyDetails(x + gridOrigin.x, y + gridOrigin.y,
                            sceneSave.gridPropertyDetailsDictionary);

                        if(gridPropertyDetails != null)
                        {
                            // If NPC obstacle
                            if(gridPropertyDetails.isNPCObstacle == true)
                            {
                                Node node = gridNodes.GetGridNode(x, y);
                                node.isObstacle = true;
                            }
                            else if(gridPropertyDetails.isPath == true) // 有移动惩罚
                            {
                                Node node = gridNodes.GetGridNode(x, y);
                                node.movementPenalty = pathMovementPenalty;
                            }
                            else
                            {
                                Node node = gridNodes.GetGridNode(x, y);
                                node.movementPenalty = defaultMovementPenalty;
                            }
                        }
                    }
                }


            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }

        return true;
    }


}

(6)创建AStarTest.cs脚本

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.SceneManagement;


[RequireComponent(typeof(AStar))]
public class AStarTest : MonoBehaviour
{
    private AStar aStar;
    [SerializeField] private Vector2Int startPosition;
    [SerializeField] private Vector2Int finishPosition;
    [SerializeField] private Tilemap tileMapToDisplayPathOn = null;
    [SerializeField] private TileBase tileToUseToDisplayPath = null;
    [SerializeField] private bool displayStartAndFinish = false;
    [SerializeField] private bool displayPath = false;

    private Stack<NPCMovementStep> npcMovementSteps;

    private void Awake()
    {
        aStar = GetComponent<AStar>();
        npcMovementSteps = new Stack<NPCMovementStep>();
    }

    private void Update()
    {
        if(startPosition != null && finishPosition != null && tileMapToDisplayPathOn != null && tileToUseToDisplayPath != null)
        {
            // Display start and finish tiles
            if (displayStartAndFinish)
            {
                // Display start tile
                tileMapToDisplayPathOn.SetTile(new Vector3Int(startPosition.x, startPosition.y, 0), tileToUseToDisplayPath);

                // Display finish tile
                tileMapToDisplayPathOn.SetTile(new Vector3Int(finishPosition.x, finishPosition.y, 0), tileToUseToDisplayPath);
            }
            else  // Clear start and finish
            {
                // clear start tile
                tileMapToDisplayPathOn.SetTile(new Vector3Int(startPosition.x, startPosition.y, 0), null);

                // clear finish tile
                tileMapToDisplayPathOn.SetTile(new Vector3Int(finishPosition.x, finishPosition.y, 0), null);
            }

            // Display path
            if (displayPath)
            {
                // Get current scene name
                Enum.TryParse<SceneName>(SceneManager.GetActiveScene().name, out SceneName sceneName);

                // Build path
                aStar.BuildPath(sceneName, startPosition, finishPosition, npcMovementSteps);

                // Display path on tilemap
                foreach(NPCMovementStep npcMovementStep in npcMovementSteps)
                {
                    tileMapToDisplayPathOn.SetTile(new Vector3Int(npcMovementStep.gridCoordinate.x, npcMovementStep.gridCoordinate.y, 0), tileToUseToDisplayPath);
                }
            }
            else
            {
                // Clear path
                if(npcMovementSteps.Count > 0)
                {
                    // Clear path on tilemap
                    foreach(NPCMovementStep npcMovementStep in npcMovementSteps)
                    {
                        tileMapToDisplayPathOn.SetTile(new Vector3Int(npcMovementStep.gridCoordinate.x, npcMovementStep.gridCoordinate.y, 0), null);
                    }

                    // Clear movement steps
                    npcMovementSteps.Clear();   
                }
            }
            
        }
    }
}

4、创建对象

在Hierarchy -> PersistentScene下创建NPCManager对象,

NPCManager对象挂载AStar、AStarTest组件如下所示:

再创建TileMap对象

与Scene1_Farm创建的TilemapGrid的Tilemap类型保持一致,否则网格布局不一致,会导致坐标错乱

然后给NPCManager对象赋值如下:

5、运行测试1

修改NPCManager的AStarTest组件的Start Position / Finish Position的值,然后勾选Display,可以看到效果如下:

调整Start Position的值:

存在的问题:路径直接穿过了不可行走的区域。

6、创建障碍物

在Scene1_Farm场景中,添加Obstacle后如下:

此时重新运行程序查看路径如下:

此时的路径不会穿过不可行走区域了,但是又有新的问题:上边的路径非常靠近路的边缘,一般情况下我们希望NPC能够行走在马路的中央。所以我们想要做的是让路径更倾向于在路的中间。

7、创建参考路径

在Scene1_Farm场景中,添加Path后如下

此时运行程序,当defaultMovementPenalty=5时路径如下:

当defaultMovementPenalty=20时路径如下:

解释一下:

首先,如果网格的isPath是true,则取pathMovementPenalty为0,其他为defaultMovementPenalty(大于0)。

然后,计算gCost的时候,非isPath的节点gCost更大,此时会倾向于有isPath属性的路径。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值