Jas*_*ske 14 javascript algorithm graph a-star
我一直致力于90年代早期冒险游戏的JavaScript实现,并专门绘制从英雄站立位置到玩家点击位置的路径.我的方法是首先确定是否可以绘制一条海峡线(没有障碍物),如果没有,那么使用Brian Grinstead的优秀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);Run Code Online (Sandbox Code Playgroud)
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}Run Code Online (Sandbox Code Playgroud)
<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>Run Code Online (Sandbox Code Playgroud)
在深入了解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;
}
Run Code Online (Sandbox Code Playgroud)
基本上,在它减少了相同方向的冗余步骤之后,它会尝试通过向前看来平滑路径以查看它是否可以消除任何步骤.
Mic*_*dis 15
非常非常有趣的问题!谢谢你的回答!所以......首先是一些观察:
不允许对角线移动解决了这个问题,但由于你对对角线移动感兴趣,我不得不搜索更多.
我看了一下路径简化算法,如: 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的实现是问题或启发式.我尝试了你的代码的一个明星的所有启发式方法,包括我自己编写的欧几里德,并尝试了http://buildnewgames.com/astar代码中的所有启发式方法.不幸的是,所有的对角线允许启发式算法都有同样的问题你正在描述.
我开始使用他们的代码,因为它是一对一的网格,而你的代码正在给我绘制问题.我试图删除的simplifyPath也导致了其他问题.你必须记住,因为你正在重新映射,这可能是一个基于此的问题
什么是我的伪代码算法:
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;
Run Code Online (Sandbox Code Playgroud)
以下是算法中比较内容的直观解释:
视觉说明:

这段代码将如何与你一起使用(我做了大部分的改动 - 我尽力了但是有很多问题,比如你如何绘图,以及因为网格的四舍五入等等 - 你必须使用一个网格并保持路径的规模是准确的 - 请参见下面的假设:
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
}
Run Code Online (Sandbox Code Playgroud)
上述代码的假设和不兼容性:
这是我的github存储库中的代码(你可以运行演示) https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
他们的clickHandling和世界边界代码被破坏,因为当您单击地图右侧时,路径计算有时不起作用.我没有时间找到他们的错误.结果我的代码有相同的问题可能是因为我从你的问题中提出的地图不是正方形 - 但无论如何我的算法应该不受影响.如果我的删除ZigZag代码没有运行,你会发现这种奇怪的行为正在发生.(编辑:实际上是因为地图不是正方形 - 我现在将地图更新为正方形)
通过取消注释这条线来随意看看之前的游戏:
result = removeZigZag(result,7);
Run Code Online (Sandbox Code Playgroud)
我在图像集之前附加了3,因此可以看到结果:(如果你尝试的话,请记住匹配开始和目标 - 方向很重要;))
案例1:之前
案例1:之后
案例2:之前
案例2:之后
案例3:之前
案例3:之后
案例4:之前
案例4:之后
资源: