【Unity】六边形、菱形的AStar(A*)算法及死路优化

本文介绍了如何在六边形和菱形网格上实现A*算法,包括坐标转换和邻接节点查找。重点讨论了处理死路优化的方法,即在目标点不可达时,将障碍点加入openList并设置高移动消耗,确保玩家始终有行动路径。
摘要由CSDN通过智能技术生成

前言

上一篇[一篇文章搞定AStar(A*)算法]​​​​​​写了一下四边形的A*算法。但是在游戏开发中,战棋类游戏往往并不会采用四边形网格而是采用六边形或菱形网格,一方面六边形或菱形网格看上去更具美感,另一方面六边形或菱形网格可玩性比四边形网格更高。那么本篇再来写一写六边形、菱形网格的A*算法是如何实现的。

六边形A*算法

在开始实现发算法之前,要先弄明白六边形网格的地图应该如何去生成,因为算法也是基于地图的,没有一个正确的地图就无法对算法有一个正确的理解。

在四边形的A*算法中,把x,y分别加1减1就获取到了某点周围的4个邻接节点,代码是这样的:

FindDestNode(currX, currY + 1, xTo, yTo, currNode, openList);
FindDestNode(currX, currY - 1, xTo, yTo, currNode, openList);
FindDestNode(currX - 1, currY, xTo, yTo, currNode, openList);
FindDestNode(currX + 1, currY, xTo, yTo, currNode, openList);

但六边形有六条边,每个格子就有6个邻接节点,无论怎样去安排这六个点的坐标,都不能通过把x,y加1减1的方式全部都获取到。所以要生成一张六边形网格,首先就是要解决六边形网格的坐标问题。

不妨找一张六边形网格观察一下,会发现每行每列的格子都是交错排列的,如果逻辑坐标也能这样交错排列就完美的与网格对应上了。

所以解决这个问题的方法就是把x坐标扩大2倍+y坐标%2,形成奇数行x坐标就进行奇数排列,偶数行x坐标就进行偶排列,y坐标不变。

return new Vector2Int(2 * x + y % 2, y);

确定了格子的坐标,获取邻接节点就很容易了

FindDestNode(currX + 1, currY + 1, xTo, yTo, currNode, openList);//右上
FindDestNode(currX - 1, currY + 1, xTo, yTo, currNode, openList);//左上
FindDestNode(currX + 1, currY - 1, xTo, yTo, currNode, openList);//右下
FindDestNode(currX - 1, currY - 1, xTo, yTo, currNode, openList);//左下
FindDestNode(currX + 2, currY, xTo, yTo, currNode, openList);//右
FindDestNode(currX - 2, currY, xTo, yTo, currNode, openList);//左

好了,现在一切准备就绪,来定几个障碍点跑一下试试。

完美!

菱形A*算法

地图坐标的处理和六边形A*算法的处理是完全一样的。

死路优化

有没有想过这两种情景:

  1. 玩家在地图上点击了一个点,但这个点却是一个障碍点
  2. 玩家在地图上点击了一个点,但很遗憾,这个点四面楚歌,并没有通往这个点的路线

以上这两种情况,玩家操控的角色该如何行动?

显然,在这两个前提下,A*算法不会得出结果,因为在之前的代码里,对于不可行走的格子直接就跳过检测了。没有路线,玩家角色自然就会巍然不动。

我的前上司曾对我说过,一个具有良好体验的游戏,应该做到“逢点击,必响应”,只是因为点击到了一个障碍点就不做出反馈,这实属是犯了大忌啊!

那么苦哈哈的程序员必须要优化:即使点击到了障碍甚至没有通路,也要让玩家角色移动到距离障碍点最近的点。

聪明伶俐的你一定已经发现了,之所以没有通路是因为目标点不可达,所以把目标点替换到它附近的可以到达的点不就行了吗?

既然我已经写到这了,那么这种方法肯定就是不可以的!

确实,当目标点不可达时,从目标点开始广度优先遍历,直到找到一个不是障碍的点来代替原来的目标点这种方法看起来可行,但是,如果代替点也是个空心的呢?它的周围也被障碍团团围住,那不还是死路一条?这样想下去就没完没了了,可能这个广度优先的替代法会执行到遍历完整个地图为止。

其实回过头重新仔仔细细的看一遍算法就会发现,之所以得不出路线的根本原因就是因为那些障碍点直接被跳过了,根本就没有机会被放进openList中等待检测,那么只要打破这个规则:即使目标点不可达,也要把它放进openList中去检测,不就能得到路线了吗?

回想我们的Dijkstra大师对我们的教导:最短路径的求解是不断寻找移动消耗最低的点直到终点。

看到【移动消耗】这个词,你是否已经有了灵感?

没错,这个问题的终极解决办法就是:

  1. 我们要打破铁律,我们要为底层劳苦大众发声,障碍点低人一等吗?障碍点也有进入openList的资格
  2. 但障碍点终归是障碍点,在openList里它终归是个异类,即使允许它进入openList,它也是最后一个被检测的,只有实在没有其他选择的时候,A*才会垂怜它一下

把障碍点也放进openList,但它的移动消耗G是无穷大!这样一来,在没有任何通路的情况下,A*会回头来检测从这些障碍点身上通过是否能到达终点,无论如何都会给玩家一条路走。

private void FindDestNode(int x, int y, int xTo, int yTo, Node currNode, List<Node> openList)
{
    if (x >= 0 && x / 2 < m_MapWidth && y >= 0 && y < m_MapHeight)
    {
        int index = x / 2 + y * m_MapWidth + 1;
        int toIndex = xTo / 2 + yTo * m_MapWidth + 1;
        int mulity = 1;

        if (m_MapData[index - 1] == -1)
        {
            mulity = 1000;//如果是障碍点,把它的移动消耗G扩大一千倍
        }

        Node temp = openList.Find(obj => obj.index == index);

        if (temp == null)
        {
            temp = new Node(index, currNode, (currNode.g + 1) * mulity, GetDistance(index, toIndex));
            openList.Add(temp);
        }
        else if (temp.isOpen && currNode.g + 1 + temp.h < temp.f)
        {
            temp.g = (currNode.g + 1) * mulity;
            temp.f = temp.g + temp.h;
            temp.parentNode = currNode;
        }
    }
}

但路是有了,那回溯的时候不就把障碍点以及后面那些进不去的地方也算进来了吗?其实这个就更简单了,回溯的时候一但发现障碍点就把该障碍点以及前面的所有节点全部干掉就可以了。

while (currNode != null)
{
    if (m_MapData[currNode.index - 1] != -1)
    {
        m_QueuePath.Enqueue(currNode.index);
        currNode = currNode.parent;
    }
    else if (m_QueuePath.Count > 0)//遇到障碍点则把之前入队的点全部出队
    {
        m_QueuePath.Dequeue();
    }
    else
    {
        currNode = currNode.parent;
    }
}

这样一来即使玩家点击到了障碍物也有路可走,真正做到了“逢点击,必响应”。

结语

这几天A*也研究的差不多了,但是鄙人水平实在有限,代码效率并不是很高,只能期待以后再进步吧,祝大家新的一年不出bug,出了bug也能找到人背锅!

工程地址:

GitHub - WuWu03/AStar: A*算法Untiy工程A*算法Untiy工程. Contribute to WuWu03/AStar development by creating an account on GitHub.icon-default.png?t=N7T8https://github.com/WuWu03/AStar

 个人博客

六边形、菱形的AStar(A*)算法及死路优化 | 幻想乡の红魔馆 (ming-e.space)icon-default.png?t=N7T8https://ming-e.space/post/44f2b67e/

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Unity C#脚本来实现A*算法: ```csharp using UnityEngine; using System.Collections.Generic; public class AStar : MonoBehaviour { public Transform seeker, target; // 寻路的起点和终点 Grid grid; // 存储地图信息的网格 void Awake() { grid = GetComponent<Grid>(); } void Update() { FindPath(seeker.position, target.position); } void FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = grid.NodeFromWorldPoint(startPos); // 起点所在的网格节点 Node targetNode = grid.NodeFromWorldPoint(targetPos); // 终点所在的网格节点 List<Node> openSet = new List<Node>(); // 未处理的节点集合 HashSet<Node> closedSet = 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].hCost < currentNode.hCost)) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { // 已找到终点 RetracePath(startNode, targetNode); return; } foreach (Node neighbor in grid.GetNeighbors(currentNode)) { if (!neighbor.walkable || closedSet.Contains(neighbor)) { continue; } int newMovementCostToNeighbor = currentNode.gCost + GetDistance(currentNode, neighbor); if (newMovementCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor)) { neighbor.gCost = newMovementCostToNeighbor; neighbor.hCost = GetDistance(neighbor, targetNode); neighbor.parent = currentNode; if (!openSet.Contains(neighbor)) { openSet.Add(neighbor); } } } } } 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); } } } ``` 这个脚本依赖于一个名为`Grid`的组件,它存储了地图信息的网格。 `Grid`组件的实现不在本文的讨论范围内,你可以参考其他教程或者使用你自己的实现。 在`Update()`函数中,我们不断地调用`FindPath()`函数来执行A*算法。在`FindPath()`函数中,我们首先找到起点和终点所在的网格节点,然后使用A*算法找到从起点到终点的最短路径。最后,我们使用`RetracePath()`函数来反向遍历路径链表,并将路径保存在`Grid`组件的`path`变量中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值