如何在A*图搜索结果中消除不必要的转弯?

15

我一直在开发一个早期的JavaScript实现的90年代冒险游戏,并且专门绘制从英雄所处的位置到玩家点击的位置的路径。我的方法是首先确定是否可以绘制一条直线(没有障碍) , 如果不能,则使用 Brian Grinstead's出色的 javascript-astar搜索一个清晰的路点路径。然而,我面临的问题是路径(虽然最优),但会进入用户认为是不想要的空间。这是一个经典的例子:

The green path is the hero, the red dots are the turns

现在我知道A*只保证返回一条最简单的路径,但我正在努力实现一个权重转弯的启发式算法。以下是另外两条路径的图片,它们同样符合最简单的标准(拥有相等的步骤数量)

Alternate paths

蓝色路径和红色路径都符合步数相同且拐角较少的标准。在我的代码中,我有一个simplifyPath()函数,它会删除转向的步骤,因此如果我可以从astar获得所有可能的路径,那么我可以选择拐弯最少的路径,但这不是A*的基本工作方式,所以我正在寻找一种将简单性纳入启发式算法的方法。

以下是我的当前代码:

var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
            graphSettings = { size: size, mid: Math.ceil(size/2)},
            engine = {
                getPosition: function(event) {
                    var bounds = field.getBoundingClientRect(),
                        x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
                        y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
                        node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];

                    return {
                        x: x,
                        y: y,
                        node: node
                    }
                },
                drawObstructions: function() {
                    context.clearRect (0, 0, 320, 200);
                    if(img) {
                        context.drawImage(img, 0, 0);
                    } else {
                        context.fillStyle = 'rgb(0, 0, 0)';
                        context.fillRect(200, 100, 50, 50);
                        context.fillRect(0, 100, 50, 50);
                        context.fillRect(100, 100, 50, 50);
                        context.fillRect(0, 50, 150, 50);
                    }
                },
                simplifyPath: function(start, complexPath, end) {
                    var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
                    for(i = 1; i < (complexPath.length - 1); i++) {
                        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
                        
                        if(classification !== previousClassification) {
                            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
                        } else {
                            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
                        }
                        previous = complexPath[i];
                        previousClassification = classification;
                    }
                    simplePath.push(end);
                    return simplePath;
                },
                drawPath: function(start, end) {
                    var path, step, next;
                    if(this.isPathClear(start, end)) {
                       this.drawLine(start, end);
                    } else {
                        path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
                        if(path.length > 1) {
                            step = path[0];
                            for(next = 1; next < path.length; next++) {
                                this.drawLine(step, path[next]);
                                step = path[next];
                            }
                        }
                    }
                },
                drawLine: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;

                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            context.fillStyle = 'rgb(255, 0, 0)';
                        } else {
                            context.fillStyle = 'rgb(0, 255, 0)';
                        }
                        context.fillRect(x, y, 1, 1);
                        
                        if(x === end.x && y === end.y) {
                            break;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                },
                isPathClear: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;
                    
                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            return false;
                        }
                        
                        if(x === end.x && y === end.y) {
                            return true;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                }
            }, graph;
            engine.drawObstructions();
            graph = (function() {
                var x, y, rows = [], cols, js = '[';
                for(y = 0; y < 200; y += graphSettings.size) {
                    cols = [];
                    
                    for(x = 0; x < 320; x += graphSettings.size) {
                        cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
                    }
                    js += '['+cols+'],\n';
                    rows.push(cols);
                }
                js = js.substring(0, js.length - 2);
                js += ']';
                document.getElementById('Graph').value=js;
                return new Graph(rows, { diagonal: true });
            })();
            return engine;
        }, start, end, engine = EngineBuilder(field, 10);

field.addEventListener('click', function(event) {
    var position = engine.getPosition(event);
    if(!start) {
        start = position;
    } else {
        end = position;
    }
    if(start && end) {
        engine.drawObstructions();
        engine.drawPath(start, end);
        start = end;
    }
}, false);
#field {
border: thin black solid;
    width: 98%;
    background: #FFFFC7;
}
#Graph {
    width: 98%;
    height: 300px;
    overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
    
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>

在仔细研究了Michail Michailidis的优秀回答后,我将以下代码添加到我的simplifyPath()函数中(演示):

simplifyPath: function(start, complexPath, end) {
    var previous = complexPath[1],
        simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}],
        i,
        finalPath = [simplePath[0]],
        classification,
        previousClassification;

    for(i = 1; i < (complexPath.length - 1); i++) {
        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();

        if(classification !== previousClassification) {
            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
        } else {
            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
        }
        previous = complexPath[i];
        previousClassification = classification;
    }

    simplePath.push(end);
    previous = simplePath[0];
    for(i = 2; i < simplePath.length; i++) {
        if(!this.isPathClear(previous, simplePath[i])) {
            finalPath.push(simplePath[i-1]);
            previous = simplePath[i-1];
        }
    }
    finalPath.push(end);

    return finalPath;
}

基本上,在减少相同方向上的冗余步骤之后,它会尝试通过向前查看来平滑路径,以查看是否可以消除任何步骤。


1
据我所理解,你的simplifyPath仅检查了两个边缘,而不是更多的边缘...因此,这个算法是不正确的,你必须检查两个边缘、三个边缘,直到整个路径的长度才能使其正常工作。可能有比上面这个O(2^N)更好的算法,但肯定不是像你的那样使用基本启发式方法只检查连续的边缘,时间复杂度为O(N)。 - Michail Michailidis
6个回答

16

非常有趣的问题!感谢您的提问!首先,一些观察:

不允许对角线移动可以解决此问题,但由于您对对角线移动感兴趣,我必须进行更多搜索。

我看了一下路径简化算法,例如:

Ramer Douglas Peuker (http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) 和一个实现:https://gist.github.com/rhyolight/2846020。 我尝试将其添加到您的代码中,但没有成功。该算法不考虑障碍物,因此很难使其适应。

如果您使用Dijkstra而不是A*,或者使用“两个节点之间的所有最短路径”算法,然后按方向变化递增排序,那么对于对角线移动,行为会是什么样子呢?

在这里阅读有关A*的一些内容http://buildnewgames.com/astar/后,我认为您正在使用的A-star实现或启发式算法存在问题。我尝试了您代码中A-star的所有启发式算法,包括我自己编写的欧几里得距离,还尝试了http://buildnewgames.com/astar代码中的所有启发式算法,但所有允许对角线移动的启发式算法都存在您所描述的问题。

我开始使用他们的代码,因为它是一个一对一的网格,而您的代码在绘图方面给我带来了问题。我尝试删除您的simplifyPath函数,但这也会导致其他问题。请记住,由于您正在进行重新映射,因此这可能会成为一个问题。

  • 在允许4个方向移动的正方形网格上,使用曼哈顿距离(L1)。
  • 在允许8个方向移动的正方形网格上,使用对角线距离(L∞)。
  • 在允许任何方向移动的正方形网格上,您可能需要或不需要欧几里得距离(L2)。如果A*在网格上找到路径,但您允许在网格上以外移动,则可能需要考虑地图的其他表示形式。 (来自http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html

我的伪代码算法是:

var path = A-star();
for each node in path {
    check all following nodes till some lookahead limit
    if you find two nodes in the same row but not column or in the same column but not row { 
       var nodesToBeStraightened = push all nodes to be "straightened" 
       break the loop;
    }
    skip loop index to the next node after zig-zag
}

if nodesToBeStraightened length is at least 3 AND
   nodesToBeStraightened nodes don't form a line AND
   the resulting Straight line after simplification doesn't hit an obstruction

       var straightenedPath =  straighten by getting the first and last elements of nodesToBeStraightened  and using their coordinates accordingly

return straightenedPath;

以下是关于算法比较的可视化解释:
可视化解释: Visual Representation of the algorithm 这段代码将如何与你的代码一起使用(我已经做了大部分的更改 - 我尽力了,但还有很多问题,比如绘图方式和网格舍入等 - 你必须使用网格并保持路径比例准确 - 请参见下面的假设):
    var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
        graphSettings = { size: size, mid: Math.ceil(size/2)},
        engine = {
            //[...] missing code
            removeZigZag: function(currentPath,lookahead){
                //for each of the squares on the path - see lookahead more squares and check if it is in the path
                for (var i=0; i<currentPath.length; i++){

                    var toBeStraightened = [];
                    for (var j=i; j<lookahead+i+1 && j<currentPath.length; j++){         
                        var startIndexToStraighten = i;
                        var endIndexToStraighten = i+1;

                        //check if the one from lookahead has the same x xor the same y with one later node in the path
                        //and they are not on the same line
                        if( 
                           (currentPath[i].x == currentPath[j].x && currentPath[i].y != currentPath[j].y) ||
                           (currentPath[i].x == currentPath[j].y && currentPath[i].x != currentPath[j].x)   
                        ) { 
                            endIndexToStraighten = j;

                            //now that we found something between i and j push it to be straightened
                            for (var k = startIndexToStraighten; k<=endIndexToStraighten; k++){
                                toBeStraightened.push(currentPath[k]);
                            }
                            //skip the loop forward
                            i = endIndexToStraighten-1;
                            break;
                        }
                    }

                    if (toBeStraightened.length>=3                                   
                        && !this.formsALine(toBeStraightened) 
                        && !this.lineWillGoThroughObstructions(currentPath[startIndexToStraighten], currentPath[endIndexToStraighten],this.graph?????)
                        ){
                        //straightening:
                        this.straightenLine(currentPath, startIndexToStraighten, endIndexToStraighten);
                    }
                }
                return currentPath;
            },

            straightenLine: function(currentPath,fromIndex,toIndex){
                for (var l=fromIndex; l<=toIndex; l++){

                    if (currentPath[fromIndex].x == currentPath[toIndex].x){
                         currentPath[l].x = currentPath[fromIndex].x;
                    }
                    else if (currentPath[fromIndex].y == currentPath[toIndex].y){
                         currentPath[l].y = currentPath[fromIndex].y;       
                    }
                }
            },

            lineWillGoThroughObstructions: function(point1, point2, graph){
                var minX = Math.min(point1.x,point2.x);
                var maxX = Math.max(point1.x,point2.x);
                var minY = Math.min(point1.y,point2.y);
                var maxY = Math.max(point1.y,point2.y);

                //same row
                if (point1.y == point2.y){
                    for (var i=minX; i<=maxX && i<graph.length; i++){
                        if (graph[i][point1.y] == 1){ //obstacle
                            return true;
                        } 
                    }
                }
                //same column
                if (point1.x == point2.x){
                    for (var i=minY; i<=maxY && i<graph[0].length; i++){
                        if (graph[point1.x][i] == 1){ //obstacle
                            return true;
                        } 
                    }
                }
                return false;
            },

            formsALine: function(pointsArray){
                //only horizontal or vertical
                if (!pointsArray || (pointsArray && pointsArray.length<1)){
                    return false;
                }

                var firstY = pointsArray[0].y;
                var lastY = pointsArray[pointsArray.length-1].y;

                var firstX = pointsArray[0].x;
                var lastX = pointsArray[pointsArray.length-1].x;

                //vertical line
                if (firstY == lastY){
                    for (var i=0; i<pointsArray.length; i++){
                        if (pointsArray[i].y!=firstY){
                            return false;
                        }
                    }
                    return true;
                }

                //horizontal line
                else if (firstX == lastX){
                    for (var i=0; i<pointsArray.length; i++){
                        if (pointsArray[i].x!=firstX){
                            return false;
                        }
                    }
                    return true;
                }       
                return false;
            }
            //[...] missing code
        }
        //[...] missing code
    }

以上代码的假设和不兼容性:

  • 障碍物为1而不是0
  • 我在演示中使用的原始代码使用数组而不是{x: number, y:number}
  • 如果您使用其他a-star实现,则点1位置在第1列和第2行。
  • 转换为您的{x: number, y:number},但尚未检查所有部分:
  • 我无法访问图形对象以获取障碍物-请参见?????
  • 您必须在进行路径简化的地方使用一个前瞻,例如7(步骤),来调用我的removeZigZag
  • 诚然,与斯坦福大学的a-star实现相比,他们的代码并不那么好,但对于我们的目的来说,这应该是无关紧要的
  • 可能代码存在我不知道的错误,并且可以改进。此外,我仅在此特定的世界配置中进行了检查
  • 我认为代码的复杂度为O(N x lookahead)~O(N),其中N是输入A *最短路径中的步数。

这是我在github存储库中的代码(您可以运行演示) https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag

他们的clickHandling和world boundary代码已经损坏,因为当您单击地图的右侧时,路径计算有时不起作用。我没有时间找到他们的错误。结果,我的代码也存在相同的问题 可能是因为我从您的问题中放置的地图不是正方形-但无论如何,我的算法都不应受影响。如果我的remove ZigZag代码都没有运行,则会看到发生这种奇怪的行为。 (编辑:实际上是因为地图不是正方形-我现在更新了地图)

随意取消注释此行以查看之前的内容:

result = removeZigZag(result,7);

我已经附上了3个图像对比,以便于可以直观地看到结果: (请注意匹配起点和终点如果您尝试它们-方向很重要;))

案例1:之前 案例1:之前 案例1:之后 案例1:之后 案例2:之前 案例2:之前 案例2:之后 案例2:之后 案例3:之前 案例3:之前 案例3:之后 案例3:之后 案例4:之前 案例4:之前 案例4:之后 案例4:之后 资源:


我建议的是在现有的一个网格上使用,但是路径应该精确到像素级别才能使其正常工作。我不明白为什么这不是适用于任何具有这些属性且不会扭曲原始网格的通用方法 :)。如果您必须在扭曲的网格上进行操作,则将涉及计算几何中遇到的问题。这可能涉及到阅读学术论文等事项。我认为您的应用程序非常小,不需要完美的网格 :) - Michail Michailidis
1
以下是使用KQ3场景的代码 :) http://jsfiddle.net/sperske/q7er3sy7/(您还可以加载自己的图像(作为数据URI) - Jason Sperske

2
您可以使用修改过的A*算法来考虑方向变化。虽然简化标准A*算法的结果可能会产生良好的结果,但可能不是最优的。这个修改过的A*算法将返回一条长度最小、转弯最少的路径。
在修改后的A*算法中,每个位置对应着八个不同的节点,每个节点都有自己的朝向。例如,位置(1,1)对应着八个节点:
(1,1)-上、(1,1)-下、(1,1)-右、(1,1)-左、(1,1)-左上、(1,1)-右上、(1,1)-左下和(1,1)-右下。
从一个节点到目标的启发式距离是从相应点到目标的启发式距离。在这种情况下,您可能希望使用以下函数:
H(point)=max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))
与特定位置对应的节点与相邻位置的节点以适当的朝向连接。例如,对应于位置(1,1)的节点都连接到以下八个节点:
(1,2)-上、(1,0)-下、(2,1)-右、(0,1)-左、(0,2)-左上、(2,2)-右上、(0,0)-左下和(2,0)-右下。
任意两个连接的节点之间的距离取决于它们的朝向。如果它们具有相同的头,则距离为1,否则,我们已经转弯,因此距离为1+epsilon。epsilon表示任意小的值/数。
现在,我们需要为起点和终点设置一个特殊情况。起点和终点都表示为单个节点。在起点处,我们没有朝向,因此起点节点与任何连接的节点之间的距离为1。
现在我们可以在修改后的图上运行标准A*算法。我们可以通过忽略头部将返回的路径映射到原始网格中的路径。返回路径的总长度将是n+m*epsilon的形式。n是原始网格中对应路径的总长度,m是转弯次数。因为A*返回最短路径,所以原始网格中的路径是最短的路径,使转弯最少。

现在更仔细地查看您的算法。您将如何为算法的每个步骤加权距离和改变方向启发式?通常,G(成本)是非常短视的,并且它基于移动类型(例如对角线与非对角线移动)是恒定的。但是您的算法需要考虑先前的步骤才能弄清楚...这种方式肯定可以适应A*吗?如果是这样,为什么原始算法不这样做呢? - Michail Michailidis
此外,您必须证明您的启发式函数是可接受的(http://en.wikipedia.org/wiki/A*_search_algorithm#Admissibility_and_optimality),并且不会高估到达目标的成本,因为这可能会导致次优路径,因为从我所了解的情况来看,算法将开始考虑更少的候选节点。 - Michail Michailidis
1
@MichailMichailidis 在每个节点上,标题都被明确地分成八个位置-标题对。我的启发式是可接受的,因为它是网格中任意两点之间的最小距离,其中到任何相邻节点的距离为1。原始网格中的每条路径都对应于修改后图中的一条路径,其中修改后图中的路径至少与原始网格中的路径一样长,仅在没有转弯时才相等。我会尽快编写演示代码,这可能会使事情更清晰。 - Joshua
@MichailMichailidis 关于最优性,我认为你的算法并不总是返回具有最少转弯次数的最短路径。例如,在你的帖子中的第3种情况中,你可以找到一条仅有两个拐角的最短路径。 - Joshua
我只是在学习基本的A算法,它会紧贴障碍物。我的算法只是简化/直线化路径。如果A算法的路径不是最优的,那么我的算法就不会有可选的转弯次数。我很好奇你的实现方式 :) 有时间的话可以和我分享一下吗?另外,我想知道你所说的修改后的网格的复杂度是多少。我猜A*算法只需要运行一次,对吗? - Michail Michailidis

1

除了节点具有对其父节点的引用之外,还要在变量中存储该节点来自哪个方向。在我的情况下,只有两种可能性:水平或垂直。因此,我为每种可能性创建了两个公共静态常量。并创建了一个名为“toDirection”的辅助函数,它接受两个节点并返回应采取的方向,以便从一个节点到达另一个节点:

public class Node {
    final static int HORIZONTALLY = 0;
    final static int VERTICALLY = 1;

    int col, row;
    boolean isTravelable;
    int fromDirection;
    double hCost;
    double gCost;
    double fCost;

    Node parent;

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

    public static int toDirection(Node from, Node to) {
        return (from.col != to.col) ? Node.HORIZONTALLY : Node.VERTICALLY;
    }
}

那么您可以修改计算权重的函数,考虑轮次。现在可以给予轮次一定的惩罚,例如:

public double calcGCost(Node current, Node neighbor) {
    if(current.fromDirection == Node.toDirection(current, neighbor)) {
        return 1;
    } else{
        return 1.2;
    }
}

完整代码:https://github.com/tezsezen/AStarAlgorithm


1
我想到了一个解决方案,只需在您的原始代码中进行简单的添加即可,但它并不适用于所有情况(请参见下面的图像),因为我们受限于A*返回给我们的内容。您可以在这里查看我的jsfiddle
我在您的simplifyPath函数中添加了以下代码,就在返回之前。它的作用是通过查看非相邻步骤之间是否有明确的路径(首先查看较大的间隔),从而剥离额外的步骤。它可以被优化,但您应该能够从我所拥有的东西中获得要点。
do{
    shortened = false;
    loop:
    for(i = 0; i < simplePath.length; i++) {
        for(j = (simplePath.length - 1); j > (i + 1); j--) {
            if(this.isPathClear(simplePath[i],simplePath[j])) {
                simplePath.splice((i + 1),(j - i - 1));
                shortened = true;
                break loop;
            }
        }
    }
} while(shortened == true);

您可以看到下面的内容,它删除了左侧的路径(与问题中相同),但并非所有奇数转弯都被删除。该解决方案仅使用A*提供的点,而不是路径中间的点-例如,因为第二个点没有直线无障碍通往第四或第五个点,它无法优化第三个点。这种情况比原始代码少得多,但有时仍会产生奇怪的结果。 incorrect path

0

冒着可能被踩的风险,我会尽力提出一个答案。如果您没有使用第三方插件,我建议构建一个简单的弹出/推入堆栈对象,但由于您正在使用别人的插件,最好尝试与它一起工作而不是反对它。

话虽如此,我可能只是做一些简单的事情,比如跟踪我的输出结果并尝试逻辑确定正确的答案。我会为存储在所有可能路径数组中的简单实体类型对象文字创建一个简单的实体类型对象文字?因此,整个对象的生命周期仅用于保存位置信息。然后,您可以稍后解析该对象数组,寻找最小的转弯次数。

此外,由于这个第三方插件将在幕后完成大部分工作,并且似乎很难提取,您可能需要自己提供标准。例如,如果它增加了您想要的转弯次数,即在门内看到的正方形,则可能发送起点和终点的坐标不足够。也许最好在每个转弯处停下来,并发送新坐标以查看现在是否可能直线移动。如果这样做,那么每个转弯都有机会查看并查看是否有阻碍直线运动。

我觉得这个答案太简单了,所以可能是不正确的,但我仍然会尝试...

//Entity Type Object Literal
var pathsFound = function() {

    //Path Stats
    straightLine: false,
    turnCount: 0,
    xPos: -1, //Probably should not be instantiated -1 but for now it's fine
    yPos: -1,

    //Getters
    isStraightLine: function() { return this.straightLine; },
    getTurnCount: function() { return this.turnCount; },
    getXPos: function() { return this.xPos; },
    getYPos: function() { return this.yPos; },

    //Setters
    setStraightLine: function() { this.straightLine = true; },
    setCrookedLine: function() { this.straightLine = false; },
    setXPos: function(val) { this.xPos = val; },
    setYPos: function(val) { this.yPos = val; },

    //Class Functionality
    incrementTurnCounter: function() { this.turnCount++; },
    updateFullPosition: function(xVal, yVal) { 
        this.xPos = xVal;
        this.yPos = yVal. 
    },
}

这样,您可以在每个步骤报告所有数据,并在绘制到屏幕之前迭代遍历这些对象文字的数组,通过最低的turnCount找到正确的路径。


你的答案只是一个数据模型 - 它没有算法/启发式。此外,我不认为A*可以返回所有最短路径... 他可能需要使用Dijkstra或Johnson算法的改编来返回源和目标之间的所有最短路径,然后运行类似于你的东西。 - Michail Michailidis

-3

var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
            graphSettings = { size: size, mid: Math.ceil(size/2)},
            engine = {
                getPosition: function(event) {
                    var bounds = field.getBoundingClientRect(),
                        x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
                        y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
                        node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];

                    return {
                        x: x,
                        y: y,
                        node: node
                    }
                },
                drawObstructions: function() {
                    context.clearRect (0, 0, 320, 200);
                    if(img) {
                        context.drawImage(img, 0, 0);
                    } else {
                        context.fillStyle = 'rgb(0, 0, 0)';
                        context.fillRect(200, 100, 50, 50);
                        context.fillRect(0, 100, 50, 50);
                        context.fillRect(100, 100, 50, 50);
                        context.fillRect(0, 50, 150, 50);
                    }
                },
                simplifyPath: function(start, complexPath, end) {
                    var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
                    for(i = 1; i < (complexPath.length - 1); i++) {
                        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
                        
                        if(classification !== previousClassification) {
                            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
                        } else {
                            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
                        }
                        previous = complexPath[i];
                        previousClassification = classification;
                    }
                    simplePath.push(end);
                    return simplePath;
                },
                drawPath: function(start, end) {
                    var path, step, next;
                    if(this.isPathClear(start, end)) {
                       this.drawLine(start, end);
                    } else {
                        path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
                        if(path.length > 1) {
                            step = path[0];
                            for(next = 1; next < path.length; next++) {
                                this.drawLine(step, path[next]);
                                step = path[next];
                            }
                        }
                    }
                },
                drawLine: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;

                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            context.fillStyle = 'rgb(255, 0, 0)';
                        } else {
                            context.fillStyle = 'rgb(0, 255, 0)';
                        }
                        context.fillRect(x, y, 1, 1);
                        
                        if(x === end.x && y === end.y) {
                            break;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                },
                isPathClear: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;
                    
                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            return false;
                        }
                        
                        if(x === end.x && y === end.y) {
                            return true;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                }
            }, graph;
            engine.drawObstructions();
            graph = (function() {
                var x, y, rows = [], cols, js = '[';
                for(y = 0; y < 200; y += graphSettings.size) {
                    cols = [];
                    
                    for(x = 0; x < 320; x += graphSettings.size) {
                        cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
                    }
                    js += '['+cols+'],\n';
                    rows.push(cols);
                }
                js = js.substring(0, js.length - 2);
                js += ']';
                document.getElementById('Graph').value=js;
                return new Graph(rows, { diagonal: true });
            })();
            return engine;
        }, start, end, engine = EngineBuilder(field, 10);

field.addEventListener('click', function(event) {
    var position = engine.getPosition(event);
    if(!start) {
        start = position;
    } else {
        end = position;
    }
    if(start && end) {
        engine.drawObstructions();
        engine.drawPath(start, end);
        start = end;
    }
}, false);
#field {
border: thin black solid;
    width: 98%;
    background: #FFFFC7;
}
#Graph {
    width: 98%;
    height: 300px;
    overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
    
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>


发送的代码仍然存在不必要的转向问题。 - Michail Michailidis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接