草庐IT

javascript - 如何在 A* 图形搜索结果中拉直不需要的转弯?

coder 2024-05-08 原文

我一直在研究早期 90's adventure games 的 JavaScript 实现。并特别绘制一条从英雄站立的地方到玩家点击的位置的路径。我的方法是首先确定是否可以绘制一条海峡线(没有障碍物),如果不能,则使用 Brian Grinstead's 搜索一条清晰的航路点路径。优秀 javascript-astar .然而,我面临的问题是路径(而优化将转向用户认为无意的空间。这是我正在谈论的一个经典示例(绿色路径是生成的路径,红点是路径方向改变的每个转弯处):



现在我知道 A* 只能保证返回一条不能更简单的路径(就步骤而言),但我正在努力实现权重转换的启发式方法。这是一张图片,显示了另外两条同样简单的路径(步骤数相同)



蓝色路径的步数和转弯数相同,而红色路径的步数和转弯数相同。在我的代码中,我有一个 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() 中函数)( demo ):
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;
}

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

最佳答案

非常非常有趣的问题!谢谢这个问题!所以......首先进行一些观察:

不允许对 Angular 移动解决了这个问题,但由于您对对 Angular 移动感兴趣,我不得不进行更多搜索。

我查看了路径简化算法,例如:
Ramer Douglas Peuker
( http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm )
和一个实现:https://gist.github.com/rhyolight/2846020 .
我在您的代码中添加了一个实现,但没有成功。该算法没有考虑障碍,因此很难对其进行调整。

我想知道如果您使用 Dijkstra 而不是 A* 或者如果您使用“一对节点之间的所有最短路径”算法,然后通过增加方向变化对它们进行排序,那么行为会是什么(对于对 Angular 线运动)。

在这里阅读了一些关于 A* 的内容后 http://buildnewgames.com/astar/我认为您使用的A-star的实现是问题或启发式。我在您的代码的 a-star 上尝试了所有启发式方法,包括我自己编写的欧几里得方法,并且还尝试了 http://buildnewgames.com/astar 中的所有启发式方法代码不幸的是,所有允许启发式的对 Angular 线都遇到了您所描述的相同问题。

我开始使用他们的代码,因为它是一个一对一的网格,而你的代码给我带来了绘图问题。我试图删除的simplePath 也导致了其他问题。你必须记住,因为
您正在重新映射这可能是基于此的问题

  • 在允许 4 个移动方向的方形网格上,使用曼哈顿距离 (L1)。
  • 在允许 8 个移动方向的方形网格上,使用对 Angular 线距离 (L∞)。
  • 在允许任何移动方向的方形网格上,您可能需要也可能不需要欧几里得距离 (L2)。如果 A* 在网格上寻找路径,但您不允许在网格上移动,则您可能需要考虑 map 的其他表示形式。 (来自 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;
    

    以下是算法中比较内容的直观解释:

    视觉说明:


    这段代码将如何与您的代码一起使用(我做了大部分更改 - 我尽了最大努力,但是有很多问题,比如你如何绘制以及由于网格的四舍五入等 - 你必须使用网格并保持路径的比例准确 - 另请参见下面的假设):
        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} 但尚未检查所有部分:
  • 我无法访问图形对象来获取障碍物 - 请参阅 ?????
  • 你必须在你所在的地方提前调用我的 removeZigZag,例如 7(几步之遥)
    做你的路径简化
  • 诚然,与斯坦福大学的 a-star 实现相比,他们的代码并不是那么好,但就我们的目的而言,它应该无关紧要
  • 可能代码有我不知道的错误并且可以改进。我也只在这个特定的世界配置中做了我的检查
  • 我相信代码有复杂性 O(N x 前瞻)~O(N) 其中 N 是输入 A* 最短路径中的步数。

  • 这是我的github存储库中的代码(您可以运行演示)
    https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
  • 基于从这里下载的这个 A* Javascript 实现:http://buildnewgames.com/astar/

  • 他们的 clickHandling 和世界边界代码被破坏,因为当您单击 map 右侧时,路径计算有时不起作用。我没有时间找到他们的错误。结果我的代码有同样的问题
    可能是因为我从您的问题中放置的 map 不是方形的 - 但无论如何我的算法应该不受影响。如果我的 remove ZigZag 代码没有运行,您将看到这种奇怪的行为正在发生。 ( 编辑 :实际上是因为 map 不是方形的 - 我现在将 map 更新为方形)

    通过取消注释这一行来查看前后:
    result = removeZigZag(result,7);
    

    我在图像集之前附加了 3 个,以便可以可视化结果:
    (如果你尝试它们,请记住匹配开始和目标 - 方向很重要;))

    案例1:之前

    案例1:在之后

    案例2:之前

    案例2:之后

    案例3:之前

    案例3:之后

    案例4:之前

    案例4:之后

    资源:
  • 我的代码(A* 对 Angular 线运动之字形修复演示):https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
  • 我的演示的原始 Javascript A* 实现可以在上面(第一次提交)或这里找到:- http://buildnewgames.com/astar/
  • A* 解释:http://buildnewgames.com/astar/
  • 斯坦福大学的 A* 解释:http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
  • OP 问题 (Github) 使用的 JavaScript A* 实现:
  • Ramer Douglas Peuker 算法(维基百科):http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
  • Douglas Peuker 算法的 Javascript 实现:https://gist.github.com/rhyolight/2846020
  • A* 算法(维基百科):http://en.wikipedia.org/wiki/A*_search_algorithm
  • 关于javascript - 如何在 A* 图形搜索结果中拉直不需要的转弯?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26615312/

    有关javascript - 如何在 A* 图形搜索结果中拉直不需要的转弯?的更多相关文章

    1. ruby - 如何在 Ruby 中顺序创建 PI - 2

      出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

    2. ruby - 我需要将 Bundler 本身添加到 Gemfile 中吗? - 2

      当我使用Bundler时,是否需要在我的Gemfile中将其列为依赖项?毕竟,我的代码中有些地方需要它。例如,当我进行Bundler设置时:require"bundler/setup" 最佳答案 没有。您可以尝试,但首先您必须用鞋带将自己抬离地面。 关于ruby-我需要将Bundler本身添加到Gemfile中吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/4758609/

    3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

      如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

    4. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    5. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

      exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

    6. ruby - 如何在续集中重新加载表模式? - 2

      鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

    7. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

      我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

    8. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

      我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

    9. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

      我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

    10. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

      这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

    随机推荐