文章目录

  • 前言
    • A*的算法原理
    • 实现网格系统
    • A*寻路算法实现
    • 使用堆优化节点查找
    • 路径运动单元
    • 运用权重
    • 使权重更平滑(Smooth Weights)
    • 路径平滑

前言

好久不见!今天使用Unity照着Sebastian Lague大佬的视频做一个A寻路算法的实例项目,包括A寻路的介绍,网格系统的创建,以及一些实际使用案例。

A*的算法原理

简单来说,A是一种寻路算法,通过A可以找到一系列网格中从A到B的最短路线。假如说,我们想得到下图所示A到B的最短路线,第一步我们需要得到关于A节点的所有相邻节点和这些相邻节点的的一些参数。第一个参数是相邻节点到A的距离,叫做G cost。第二个参数是该相邻节点到B节点的距离,叫做H cost,基本上可以说H cost是G cost的反面。最后一个参数为G cost和H cost的和,叫做F cost。

将这些相邻节点存入一个集合,然后算法看一圈,并拿到F cost最低的节点,对拿到的点重复上诉过程。

直到在相邻节点中找到了B点,算法就完成工作了。在没有障碍加入的情况下,路径向着终点就去了。

当然,如果你的游戏里面没有障碍物,还需要什么寻路算法,接下来我们看看加入障碍物以后的情况。如下图所示,在执行了一步操作以后,出现了三个F cost相同的相邻节点,这时候我们选择H cost,也就是离B点最近的节点

然后,依旧是选择F cost最小的两个中的一个,这一次两个节点从参数上看完全一样,所以随便选一个,反正如果选错的话得到的相邻节点的F cost最后都不可能大于另一个选择。

现在我们可以看到,鼠标指向的54是下一个选择,但是这里有个小细节,他左边相邻节点的G cost为38,而不是最短距离30,这是因为我们第一次将该节点考虑进来是通过鼠标指针上面的48和其相邻。所以说G cost得到的是根据最近一次路径运算得到的最小值,而如果我们马上考虑鼠标指向的54,我们就会发现到达这个左边相邻节点的G cost出现更小值,所以我们进行更新。这个节点现在G cost为更小值30,F cost为60。总结一下:G cost当前的值不一定是最小值,而是当前所以算过的路径下的最小值。其实说到这里,我们就知道节点还需要存储一个父节点,表明目前这个最小的G cost是从与哪个节点相邻得来的。

说句题外话,既然G cost不一定是最短, H cost呢?H cost一定是最短,H cost通过某节点先直线到B节点的同一横向或纵向,再直线到B得来的,所以一定是最短。

我们继续,现在更新后的60是最短,我们选60。

就这样一直选下去,最后我们就能得到这条路径,对了,别忘了前面说的每个节点需要记录自己是通过哪个父节点走到的,这样才能得到路径

保存父节点,就像这样

来看一下A* 算法的伪代码。

可以将OPEN理解为上图中的绿色节点,意味着候选的路径节点,在算法起始时起点为OPEN中的唯一选择,CLOSED理解为红色的点过的节点,意味着算法已经算出了到这个点的最短距离了,以后也不需要再看他了。每次循环开始时从OPEN里面找到F cost最小的作为当前节点,对于当前节点的相邻节点,如果这个节点不可移动,或者已经找到最短路径了,就跳过,否则查看这个相邻节点是否不在OPEN里面或者能得到一个更短的G cost,一个更短的G cost意味着找到了新的到这个相邻节点的更短路径,需要更新这个节点的G cost F cost以及设置当前节点为该相邻节点新的父节点,而不在OPEN意味着该相邻节点从未被考虑,现在要被考虑进来。如此循环往复,当某次循环发现当前节点就是目标节点时,寻路结束。
接下来要做的就是找到目标节点的父节点,再找到这个父节点的父节点,直到找回起始点,得到路径。

实现网格系统

要在项目中使用A*我们首先需要网格,这里大佬直接教我们自制一套网格系统。我们的网格包含一个节点类,一个网格类。节点负责定义网格中的一个位置以及该位置的信息(目前只有unwalkable)。节点将由网格负责创建。可以自定网格整体大小,单个节点大小,可以显示在Scene面板(OnDrawGizmos),可以通过给定(网格中的)某个位置获得网格中的单个节点,由此可以获得玩家所在的网格节点。

节点类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Node
{// node能走吗?会根据是否与障碍物重合判断public bool walkable;// node中心的世界坐标位置public Vector3 worldPosition;// 在Grid二维坐标中的位置public int gridX;public int gridY;// 节点到起点的距离public int gCost;// 节点到终点的距离public int hCost;// 父节点 也就是路径中该节点的上一个节点public Node parent;// 构造函数public Node(bool _walkable, Vector3 _worldPos, int _gridX, int _gridY){walkable = _walkable;worldPosition = _worldPos;gridX = _gridX;gridY = _gridY;}// fCost为gCost + hCost 所以写个属性就行public int fCost{get { return gCost + hCost; }}
}

网格类

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;// Grid的GameObject X、Y轴要在场景正中
public class Grid : MonoBehaviour
{// 用来存储unwalkable的layermaskpublic LayerMask unwalkableMask;// grid的大小public Vector2 gridWorldSize; // Vector2的y对应世界坐标中的z轴// grid中的node的半径(node立方体边长的一半)public float nodeRadius;// 玩家的位置public Transform player;// grid是二位的node数组Node[,] grid;// grid中的node的直径float nodeDiameter;// Grid中的node数量int gridSizeX, gridSizeY;// 路径public List<Node> path;private void Start(){// 根据grid的尺寸和node的尺寸计算node的数量并填入二维数组nodeDiameter = nodeRadius * 2;gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter);gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter);CreateGrid();}// 创建grid实例private void CreateGrid(){grid = new Node[gridSizeX, gridSizeY];// 计算得到grid(从上往下看)左下角的世界坐标位置Vector3 worldButtomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.forward * gridWorldSize.y / 2; //forward没错,y对应node的z坐标for (int x = 0; x < gridSizeX; x++){for (int y = 0; y < gridSizeY; y++){// 计算每一个node的世界坐标位置Vector3 worldPoint = worldButtomLeft +Vector3.right * (x * nodeDiameter + nodeRadius) +Vector3.forward * (y * nodeDiameter + nodeRadius);// 判断是否有obstacles,如果有就将node设置为unwalkablebool walkable = !(Physics.CheckSphere(worldPoint, nodeRadius, unwalkableMask));// 创建每个node实例并给成员赋值grid[x, y] = new Node(walkable, worldPoint, x, y);}}}// 获取节点的相邻节点public List<Node> GetNeighbours(Node node){List<Node> neighbours = new List<Node>();for (int x = -1; x <= 1; x++){for (int y = -1; y <= 1; y++){if (x == 0 && y == 0){                  continue;// 这就是节点自己}int checkX = node.gridX + x;int checkY = node.gridY + y;// 结果不能超出Grid的范围if(checkX >= 0 && checkY < gridSizeY && checkX < gridSizeX && checkY >= 0){neighbours.Add(grid[checkX, checkY]);}}}return neighbours;}// 通过世界坐标获得Nodepublic Node GetNodeFromWorldPoint(Vector3 worldPosition){// 通过将坐标换算为Grid中的百分比位置来获取Nodefloat percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x;float percentY = (worldPosition.z + gridWorldSize.y / 2) / gridWorldSize.y; //grid的y长对应世界坐标系的zpercentX = Mathf.Clamp01(percentX);percentY = Mathf.Clamp01(percentY);int x = Mathf.RoundToInt((gridSizeX - 1) * percentX); //减一是因为gridSize是1开始,我们需要indexint y = Mathf.RoundToInt((gridSizeY - 1) * percentY);return grid[x, y];}// 在Scene面板中显示gridprivate void OnDrawGizmos(){Gizmos.DrawWireCube(transform.position, new Vector3(gridWorldSize.x, 1, gridWorldSize.y));if (grid != null){Node playerNode = GetNodeFromWorldPoint(player.position);foreach(Node node in grid){Gizmos.color = node.walkable ? Color.white : Color.red;if(playerNode == node){Gizmos.color = Color.cyan;}if(path != null){if (path.Contains(node))Gizmos.color = Color.green;}Gizmos.DrawCube(node.worldPosition, Vector3.one * (nodeDiameter - 0.1f));}}}
}

效果

A*寻路算法实现

再开始之前说一句,VS2022的人工智能代码提示真好用,快进到人工智能写代码淘汰我这种废物程序员。

上图除了函数签名、变量名和第七行,第17行以外都拿给它提示对了

继续咱们的A*算法,按照先前的算法思路,代码实现如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Pathfinding : MonoBehaviour
{public Transform seeker, target;public Grid grid;private void Awake(){grid = this.GetComponent<Grid>();}private void Update(){FindPath(seeker.position, target.position);}void FindPath(Vector3 startPos, Vector3 targetPos){// 获取起点终点Node startNode = grid.GetNodeFromWorldPoint(startPos);Node targetNode = grid.GetNodeFromWorldPoint(targetPos);// Open列表 存放所有预选的节点List<Node> openSet = new List<Node>();HashSet<Node> closeSet = new HashSet<Node>();openSet.Add(startNode);while (openSet.Count > 0){Node currentNode = openSet[0];for (int i = 1; i < openSet.Count; i++){// 寻找一个比当前节点更优的节点 晚点再来做优化if(openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].gCost < currentNode.gCost){currentNode = openSet[i];}}openSet.Remove(currentNode);closeSet.Add(currentNode);// 碰到终点了if (currentNode == targetNode){// 回溯节点以获取路径RetracePath(startNode, targetNode);return;}// 查看每个相邻节点foreach (Node neighbourNode in grid.GetNeighbours(currentNode)){// 如果相邻节点unwalkable或者已经在closeSet里面了 啥也不干if(!neighbourNode.walkable || closeSet.Contains(neighbourNode)){continue;}// 计算从当前节点来看的neighbourNode的gCostint newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbourNode);// 如果新的gCost更小 或者这是第一次考虑此neighbourNodeif(newMovementCostToNeighbour < neighbourNode.gCost || !openSet.Contains(neighbourNode)){// 更新此neighbourNode的CostneighbourNode.gCost = newMovementCostToNeighbour;neighbourNode.hCost = GetDistance(neighbourNode, targetNode);neighbourNode.parent = currentNode;if(!openSet.Contains(neighbourNode)){openSet.Add(neighbourNode);}}}}}void RetracePath(Node startNode, Node endNode){List<Node> path = new List<Node>();Node currentNode = endNode;while(currentNode != startNode){path.Add(currentNode);currentNode = currentNode.parent;}path.Reverse();grid.path = path;}int GetDistance(Node nodeA, Node nodeB){int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX);int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY);if(dstX > dstY){return 14 * dstY + 10 * (dstX - dstY);}else{return 14 * dstX + 10 * (dstY - dstX);}}
}

效果:

Victory is ours!

使用堆优化节点查找

在先前的代码中,我们采用遍历大法来寻找fcost更低的节点,现在我们实现一个效率更好的办法。

        while (openSet.Count > 0){Node currentNode = openSet[0];for (int i = 1; i < openSet.Count; i++){// 寻找一个比当前节点更优的节点 晚点再来做优化if(openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].gCost < currentNode.gCost){currentNode = openSet[i];}}openSet.Remove(currentNode);
// ... ...

我好像一直都没了解过堆(heap)这种数据结构,在Sebastian大佬的描述中,堆就是 二叉树(

A*寻路实例项目实践笔记相关推荐

  1. Taro 多端项目实践笔记

    源创会 2018 年终盛典深圳前端会场. Taro 多端项目实践思维导图 原图在这: 链接:https://pan.baidu.com/s/1bk04... 提取码:ipls 全部笔记在这:https ...

  2. Unix编程需要学习的内容(3)《精通Unix下C语言与项目实践》读书笔记(13)

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八 ...

  3. 巨杉数据库学习笔记+巨杉数据库实操项目实践

    @TOC巨杉数据库学习笔记+项目实践心得 SequoialDB简介 SequoiaDB 巨杉数据库是一款金融级分布式数据库,主要面对高并发实时处理型场景提供高性能.可靠稳定以及无限水平扩展的数据库服务 ...

  4. 学习Unix,可从事什么样的工作(1)《精通Unix下C语言与项目实践》读书笔记(3)...

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读 不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八的 ...

  5. 《精通Unix下C语言与项目实践》读书笔记(16)

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八 ...

  6. Java编程比C编程好吗?《精通Unix下C语言与项目实践》读书笔记(15)

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八 ...

  7. 学习Unix,可从事什么样的工作(3)《精通Unix下C语言与项目实践》读书笔记(5)...

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八 ...

  8. Unix编程要学习的内容(2)《精通Unix下C语言与项目实践》读书笔记(12)

    文章试读  不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八的职场感悟吧.不值钱的软件人才 精力充沛与事业成功   让系分来得更猛 ...

  9. 学习Unix,可从事什么样的工作(2)《精通Unix下C语言与项目实践》读书笔记(4)...

    <精通Unix下C语言编程与项目实践>读书笔记(new) 文章试读 不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八的 ...

最新文章

  1. java继承对象转换_java 继承的基础(转)
  2. 大学生java考试题库6_《JAVA程序设计》期末考试试题_(六)
  3. 注册assembly的问题
  4. android icu4c 7.1编译报错,android4.0编译系统时候遇到的错误集
  5. java play database_Play Framework连接到数据库
  6. JAVA中两个数组比较可以使用Arrays.equals()
  7. 道路照明智能监控用5G智慧灯杆网关
  8. HP Networking/Comware NETCONF interface quick tutorial (using python’s ncclient and pyhpecw7)
  9. Mac 上 QuickTime Player 播放器以 1.1、1.2 倍速等更精确速度快进/快退播放的方法
  10. 服务器虚拟化的重要性,服务器虚拟化:虚拟机迁移的重要性
  11. 如何查看计算机关闭原因,电脑总是自动重启关机怎么样查找原因
  12. localhost和127.0.0.1的区别
  13. 联系微信ID服务器失败,微信小程序-新用户获取微信手机号登录服务端获取不到unionid情况...
  14. 四 微信公众号 基础参数说明
  15. 计算机关机重启后黑屏,电脑重启黑屏强制关机后才能开怎么办
  16. RN中热更新CodePush使用
  17. 「GitLab篇」如何用Git平台账号登录建木CI
  18. 如何将npm升级到最新版本
  19. 俞敏洪 :阻碍你成长的,其实是你自己
  20. Python replace()方法

热门文章

  1. 加息力度仍有悬念将决定后市金价走势
  2. Flink读写系列之-读Kafka并写入Kafka
  3. python分数序列求和_Python实现分数序列求和
  4. 自学python买什么书比较好-这些都是Python官方推荐的最好的书籍(推荐)
  5. 年度特辑 | 2017 开源中国新增开源项目排行榜 TOP 100
  6. HTTP 请求状态码大全
  7. Ribbon负载均衡策略、懒加载及饥饿加载
  8. Airsim(1.3.1版本)setting.json帮助文档解析
  9. 【云原生 | 从零开始学Docker】六、如何写出自己的镜像——Docker file
  10. 远程桌面全屏(快捷键)