服务器寻路03-a*代码实现

2021/03/25 Path

服务器寻路03-a*代码实现

目录

A*代码实现

1.网上有很多A*代码实现

网上有很多A*代码实现但是java版本的比较少并且代码质量参差不齐我这里找了个github上的还可以
https://github.com/marcelo-s/A-Star-Java-Implementation

代码不错并且逻辑正确思路清晰

2.加了中文注释后的代码

1.1.AStar类

package org.example;

import java.util.*;

/**
 * A Star Algorithm
 *
 * 1. 把起点加入 open list 。
 * 2. 重复如下过程:
 * 		a.  遍历 open list ,查找 F 值最小的节点,把它作为当前要处理的节点。
 * 		b.  把这个节点移到 close list 。
 * 		c.  对当前方格的 8 个相邻方格的每一个方格?
 * 			◆     如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作。
 * 			◆     如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。
 * 			◆     如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。
 * 		 	       如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。
 * 		d.  停止,当你:
 * 			◆     把终点加入到了 open list 中,此时路径已经找到了,或者
 * 			◆     查找终点失败,并且 open list 是空的,此时没有路径。
 * 3. 保存路径。从终点开始,每个方格沿着父节点移动直至起点,这就是你的路径。
 *
 * @author Marcelo Surriabre
 * @version 2.1, 2017-02-23
 */
public class AStar {
    /**
     * 水平和垂直移动代价常量
     */
    private static int DEFAULT_HV_COST = 10; // Horizontal - Vertical Cost
    /**
     * 对角移动代价常量
     */
    private static int DEFAULT_DIAGONAL_COST = 14;

    /**
     * 移动代价
     */
    private int hvCost;
    private int diagonalCost;

    /**
     * 搜索区域节点
     */
    private Node[][] searchArea;

    /**
     * 开放列表。PriorityQueue优先队列,底层其实就是小根堆
     */
    private PriorityQueue<Node> openList;

    /**
     * 关闭列表
     */
    private Set<Node> closedSet;

    /**
     * 初始节点
     */
    private Node initialNode;

    /**
     * 最终节点
     */
    private Node finalNode;

    public AStar(int rows, int cols, Node initialNode, Node finalNode, int hvCost, int diagonalCost) {
        this.hvCost = hvCost;
        this.diagonalCost = diagonalCost;

        //设置初始和最终节点,因为是起点和终点所以不需要计算当前节点的H值
        setInitialNode(initialNode);
        setFinalNode(finalNode);

        this.searchArea = new Node[rows][cols];

        //开放列表,优先队列,F值最小的优先弹出
        this.openList = new PriorityQueue<Node>(new Comparator<Node>() {
            @Override
            public int compare(Node node0, Node node1) {
                return Integer.compare(node0.getF(), node1.getF());
            }
        });
        setNodes();
        this.closedSet = new HashSet<>();
    }

    public AStar(int rows, int cols, Node initialNode, Node finalNode) {
        this(rows, cols, initialNode, finalNode, DEFAULT_HV_COST, DEFAULT_DIAGONAL_COST);
    }

    /**
     * 迭代搜索区域二维数组,并计算每个Node的H值
     */
    private void setNodes() {
        for (int i = 0; i < searchArea.length; i++) {
            for (int j = 0; j < searchArea[0].length; j++) {
                Node node = new Node(i, j);
                //计算h值大小
                node.calculateHeuristic(getFinalNode());
                this.searchArea[i][j] = node;
            }
        }
    }

    /**
     * 设置阻挡
     * @param blocksArray
     */
    public void setBlocks(int[][] blocksArray) {
        for (int i = 0; i < blocksArray.length; i++) {
            int row = blocksArray[i][0];
            int col = blocksArray[i][1];
            setBlock(row, col);
        }
    }

    /**
     * 寻路方法
     * @return
     */
    public List<Node> findPath() {
        //先将起点加入到开放列表里
        openList.add(initialNode);
        while (!isEmpty(openList)) {
            //弹出F值最小的节点
            Node currentNode = openList.poll();
            if (null == currentNode) {
                continue;
            }
            //加到关闭列表
            closedSet.add(currentNode);
            if (isFinalNode(currentNode)) {
                //如果当前点是终点,回溯获取移动路径
                return getPath(currentNode);
            } else {
                //当前点不是终点,添加相邻节点信息
                addAdjacentNodes(currentNode);
            }
        }
        return new ArrayList<Node>();
    }

    /**
     * 回溯获取路径
     * @param currentNode
     * @return
     */
    private List<Node> getPath(Node currentNode) {
        List<Node> path = new ArrayList<Node>();
        path.add(currentNode);
        Node parent;
        //寻找父节点
        while ((parent = currentNode.getParent()) != null) {
            path.add(0, parent);
            currentNode = parent;
        }
        return path;
    }

    /**
     * 添加相邻节点信息
     *
     * <>
     *     以当前点为中心:
     *        上方:2个格子
     *        中间(同一行): 2个格子
     *        下方:3个格子
     * </>
     * @param currentNode
     */
    private void addAdjacentNodes(Node currentNode) {
        addAdjacentUpperRow(currentNode);
        addAdjacentMiddleRow(currentNode);
        addAdjacentLowerRow(currentNode);
    }

    private void addAdjacentLowerRow(Node currentNode) {
        int row = currentNode.getRow();
        int col = currentNode.getCol();
        int lowerRow = row + 1;
        if (lowerRow < getSearchArea().length) {
            if (col - 1 >= 0) {
                checkNode(currentNode, col - 1, lowerRow, getDiagonalCost()); // Comment this line if diagonal movements are not allowed
            }
            if (col + 1 < getSearchArea()[0].length) {
                checkNode(currentNode, col + 1, lowerRow, getDiagonalCost()); // Comment this line if diagonal movements are not allowed
            }
            checkNode(currentNode, col, lowerRow, getHvCost());
        }
    }

    private void addAdjacentMiddleRow(Node currentNode) {
        int row = currentNode.getRow();
        int col = currentNode.getCol();
        int middleRow = row;
        if (col - 1 >= 0) {
            checkNode(currentNode, col - 1, middleRow, getHvCost());
        }
        if (col + 1 < getSearchArea()[0].length) {
            checkNode(currentNode, col + 1, middleRow, getHvCost());
        }
    }

    /**
     * 添加相邻的上方节点信息
     * @param currentNode 当前节点
     */
    private void addAdjacentUpperRow(Node currentNode) {
        int row = currentNode.getRow();
        int col = currentNode.getCol();
        //上方节点行索引
        int upperRow = row - 1;
        if (upperRow >= 0) {
            if (col - 1 >= 0) {
                //左上方节点, 传入的移动代价为对角移动代价也就是14
                checkNode(currentNode, col - 1, upperRow, getDiagonalCost()); // Comment this if diagonal movements are not allowed
            }
            if (col + 1 < getSearchArea()[0].length) {
                //右上方节点,传入的移动代价为对角移动代价也就是14
                checkNode(currentNode, col + 1, upperRow, getDiagonalCost()); // Comment this if diagonal movements are not allowed
            }
            //正上方节点,传入的移动代价为垂直移动代价也就是10
            checkNode(currentNode, col, upperRow, getHvCost());
        }
    }

    /**
     * c.  对当前方格的 8 个相邻方格的每一个方格?
     *   			◆     如果它是不可抵达的或者它在 close list 中,忽略它。否则,做如下操作。
     *  			◆     如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。
     *  			◆     如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。
     *  		 	       如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。
     * @param currentNode
     * @param col
     * @param row
     * @param cost
     */
    private void checkNode(Node currentNode, int col, int row, int cost) {
        //获取相邻节点信息
        Node adjacentNode = getSearchArea()[row][col];
        //节点不是阻挡并且不在关闭列表
        if (!adjacentNode.isBlock() && !getClosedSet().contains(adjacentNode)) {
            //如果开放列表里不包含当前相邻节点
            if (!getOpenList().contains(adjacentNode)) {
                adjacentNode.setNodeData(currentNode, cost);
                getOpenList().add(adjacentNode);
            } else {
                //开放列表里已经包含了当前相邻节点
                boolean changed = adjacentNode.checkBetterPath(currentNode, cost);
                if (changed) {
                    // Remove and Add the changed node, so that the PriorityQueue can sort again its
                    // contents with the modified "finalCost" value of the modified node
                    //先删除然后再添加。可以保证队列重新排序一次
                    getOpenList().remove(adjacentNode);
                    getOpenList().add(adjacentNode);
                }
            }
        }
    }

    private boolean isFinalNode(Node currentNode) {
        return currentNode.equals(finalNode);
    }

    private boolean isEmpty(PriorityQueue<Node> openList) {
        return openList.size() == 0;
    }

    private void setBlock(int row, int col) {
        this.searchArea[row][col].setBlock(true);
    }

    public Node getInitialNode() {
        return initialNode;
    }

    public void setInitialNode(Node initialNode) {
        this.initialNode = initialNode;
    }

    public Node getFinalNode() {
        return finalNode;
    }

    public void setFinalNode(Node finalNode) {
        this.finalNode = finalNode;
    }

    public Node[][] getSearchArea() {
        return searchArea;
    }

    public void setSearchArea(Node[][] searchArea) {
        this.searchArea = searchArea;
    }

    public PriorityQueue<Node> getOpenList() {
        return openList;
    }

    public void setOpenList(PriorityQueue<Node> openList) {
        this.openList = openList;
    }

    public Set<Node> getClosedSet() {
        return closedSet;
    }

    public void setClosedSet(Set<Node> closedSet) {
        this.closedSet = closedSet;
    }

    public int getHvCost() {
        return hvCost;
    }

    public void setHvCost(int hvCost) {
        this.hvCost = hvCost;
    }

    private int getDiagonalCost() {
        return diagonalCost;
    }

    private void setDiagonalCost(int diagonalCost) {
        this.diagonalCost = diagonalCost;
    }
}

2.Node类

package org.example;

/**
 * Node Class
 *
 * @author Marcelo Surriabre
 * @version 2.0, 2018-02-23
 */
public class Node {

    /**
     * 估值 f = g + h
     */
    private int g;
    private int f;
    private int h;

    /**
     * 行坐标索引
     */
    private int row;

    /**
     * 列坐标索引
     */
    private int col;

    /**
     * 是否阻挡
     */
    private boolean isBlock;

    /**
     * 父节点(回溯用,终点往起点找)
     */
    private Node parent;

    public Node(int row, int col) {
        super();
        this.row = row;
        this.col = col;
    }

    /**
     * 计算h值大小(曼哈顿算法)
     *
     * <>
     *     直接用终点和当前点坐计算
     * </>
     * @param finalNode 终点Node
     */
    public void calculateHeuristic(Node finalNode) {
        //这里其实不用太纠结实际走多少个格子,因为所有的h值用一个计算公式,多走一个或少一个意义最终没区别。
        this.h = Math.abs(finalNode.getRow() - getRow()) + Math.abs(finalNode.getCol() - getCol());
    }

    public void setNodeData(Node currentNode, int cost) {
        int gCost = currentNode.getG() + cost;
        setParent(currentNode);
        setG(gCost);
        calculateFinalCost();
    }

    /**
     * 检测是否有更好的g值
     * @param currentNode
     * @param cost
     * @return
     */
    public boolean checkBetterPath(Node currentNode, int cost) {
        int gCost = currentNode.getG() + cost;
        //当前节点走到相邻节点(假设:B),消耗的g值小于B本来的g值,说明路径更好。
        // 需要重新设置B节点父节点和重新计算B节点的g值、h值
        if (gCost < getG()) {
            setNodeData(currentNode, cost);
            return true;
        }
        return false;
    }

    private void calculateFinalCost() {
        int finalCost = getG() + getH();
        setF(finalCost);
    }

    @Override
    public boolean equals(Object arg0) {
        Node other = (Node) arg0;
        return this.getRow() == other.getRow() && this.getCol() == other.getCol();
    }

    @Override
    public String toString() {
        return "Node [row=" + row + ", col=" + col + "]";
    }

    public int getH() {
        return h;
    }

    public void setH(int h) {
        this.h = h;
    }

    public int getG() {
        return g;
    }

    public void setG(int g) {
        this.g = g;
    }

    public int getF() {
        return f;
    }

    public void setF(int f) {
        this.f = f;
    }

    public Node getParent() {
        return parent;
    }

    public void setParent(Node parent) {
        this.parent = parent;
    }

    public boolean isBlock() {
        return isBlock;
    }

    public void setBlock(boolean isBlock) {
        this.isBlock = isBlock;
    }

    public int getRow() {
        return row;
    }

    public void setRow(int row) {
        this.row = row;
    }

    public int getCol() {
        return col;
    }

    public void setCol(int col) {
        this.col = col;
    }
}

3.测试方法

用图片展示吧,上面有特殊符号会引起博客报错。

4.结果

//Search Area
//      0   1   2   3   4   5   6
// 0    -   -   -   -   -   -   -
// 1    -   -   -   B   -   -   -
// 2    -   I   -   B   -   F   -
// 3    -   -   -   B   -   -   -
// 4    -   -   -   -   -   -   -
// 5    -   -   -   -   -   -   -

//Expected output with diagonals
//Node [row=2, col=1]
//Node [row=1, col=2]
//Node [row=0, col=3]
//Node [row=1, col=4]
//Node [row=2, col=5]

//Search Path with diagonals
//      0   1   2   3   4   5   6
// 0    -   -   -   *   -   -   -
// 1    -   -   *   B   *   -   -
// 2    -   I*  -   B   -  *F   -
// 3    -   -   -   B   -   -   -
// 4    -   -   -   -   -   -   -
// 5    -   -   -   -   -   -   -

//Expected output without diagonals
//Node [row=2, col=1]
//Node [row=2, col=2]
//Node [row=1, col=2]
//Node [row=0, col=2]
//Node [row=0, col=3]
//Node [row=0, col=4]
//Node [row=1, col=4]
//Node [row=2, col=4]
//Node [row=2, col=5]

//Search Path without diagonals
//      0   1   2   3   4   5   6
// 0    -   -   *   *   *   -   -
// 1    -   -   *   B   *   -   -
// 2    -   I*  *   B   *  *F   -
// 3    -   -   -   B   -   -   -
// 4    -   -   -   -   -   -   -
// 5    -   -   -   -   -   -   -

3.总结

代码只是最基础的实现,可以帮助我们理解A星算法,但离真正的游戏线上寻路还有很大距离。
比如:
   1.代码上直接用了优先队列PriorityQueue,而实际上我们会自己实现一个类似四叉树的小根堆,以提高效率。
   2.真实的线上还会利用漏斗算法。
   。。。
   后面我们继续探索。

Search

    Table of Contents