How to straighten unneeded turns in a A* graph search result? - javascript

I have been working on a JavaScript implementation of the early 90's adventure games and specifically plotting a path from the place the hero is standing to the location the player has clicked on. My approach is to first determine if a strait line (without obstructions) can be drawn, if not then to search for a path of clear way-points using Brian Grinstead's excellent javascript-astar. The problem I'm facing however is the path (while optimal will veer into spaces that would seem to the user an unintended. Here is a classic example of what I'm talking about (the green path is the generated path, the red dots are each turns where the direction of the path changes):
Now I know that A* is only guaranteed to return a path that cannot be simpler (in terms of steps), but I'm struggling to implement a heuristic that weights turns. Here is a picture that show two other paths that would also qualify as just as simple (with an equal number of steps)
The Blue path would present the same number of steps and turns while the red path has the same number of steps and fewer turns. In my code I have a simplifyPath() function that removes steps where the direction changes, so if I could get all possible paths from astar then I could select the one with the least turns, but that's not how A* fundamentally works, so I'm looking for a way to incorporate simplicity into the heuristic.
Here is my current code:
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>
After digging into Michail Michailidis' excellent answer I've added the following code to my simplifyPath() function) (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;
}
Basically after it reduces redundant steps in the same direction, it tries to smooth out the path by looking ahead to see if it can eliminate any steps.

Very very intresting problem! Thanks for this question! So...some observations first:
Not allowing diagonal movement fixes this problem but since you are interested in diagonal movement I had to search more.
I had a look at path simplifying algorithms like:
Ramer Douglas Peuker
(http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
and an implementation: https://gist.github.com/rhyolight/2846020.
I added an implementation to your code without success. This algorithm doesn't take into account obstacles so it was difficult to adapt it.
I wonder what would the behavior be (for diagonal movements) if you had used Dijkstra instead of A* or if you used an 'all shortest paths between a pair of nodes' algorithm and then you sorted them by increasing changes in direction.
After reading a bit about A* here http://buildnewgames.com/astar/ I thought that the implementation of A-star you are using is the problem or the heuristics. I tried all the heuristics on the a-star of your code including euclidean that I coded myself and tried also all the heuristics in the http://buildnewgames.com/astar code Unfortunately all of the diagonal allowing heuristics were having the same issue you are describing.
I started working with their code because it is a one-to-one grid and yours was giving me issues drawing. Your simplifyPath that I tried to remove was also causing additional problems. You have to keep in mind that since
you are doing a remapping this could be an issue based on that
On a square grid that allows 4 directions of movement, use Manhattan distance (L1).
On a square grid that allows 8 directions of movement, use Diagonal distance (Lāˆž).
On a square grid that allows any direction of movement, you might or might not want Euclidean distance (L2). If A* is finding paths on the grid but you are allowing movement not on the grid, you may want to consider other representations of the map. (from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html)
What is my pseudocode algorithm:
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;
Here is the visual explanation of what is being compared in the algorithm:
Visual Explanation:
How this code will be used with yours (I did most of the changes - I tried my best but there are so many problems like with how you do drawing and because of the rounding of the grid etc - you have to use a grid and keep the scale of the paths accurate - please see also assumptions below):
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
}
Assumptions and Incompatibilities of the above code:
obstacle is 1 and not 0
the orginal code I have in the demo is using array instead of {x: number, y:number}
in case you use the other a-star implementation, the point1 location is on the column 1 and row 2.
converted to your {x: number, y:number} but haven't checked all the parts:
I couldn't access the graph object to get the obstacles - see ?????
You have to call my removeZigZag with a lookahead e.g 7 (steps away) in the place where you were
doing your path simplification
admittedly their code is not that good compared to the a-star implementation from Stanford but for our purposes it should be irelevant
possibly the code has bugs that I don't know of and could be improved. Also I did my checks only in this specific world configuration
I believe the code has complexity O(N x lookahead)~O(N) where N is the number of steps in the input A* shortest path.
Here is the code in my github repository (you can run the demo)
https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
based on this A* Javascript implementation downloaded from here: http://buildnewgames.com/astar/
Their clickHandling and world boundary code is broken as when you click on the right side of the map the path calculation is not working sometimes. I didn't have time to find their bug. As a result my code has the same issue
Probably it is because the map I put from your question is not square - but anyway my algorithm should be unaffected. You will see this weird behavior is happening if non of my remove ZigZag code runs. (Edit: It was actually because the map was not square - I updated the map to be square for now)
Feel free to play around by uncommenting this line to see the before-after:
result = removeZigZag(result,7);
I have attached 3 before after image sets so the results can be visualized:
(Keep in mind to match start and goal if you try them - direction DOES matter ;) )
Case 1: Before
Case 1: After
Case 2: Before
Case 2: After
Case 3: Before
Case 3: After
Case 4: Before
Case 4: After
Resources:
My code (A* diagonal movement zig zag fix demo): https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
Original Javascript A* implementation of my demo can be found above (first commit) or here: - http://buildnewgames.com/astar/
A* explanation: http://buildnewgames.com/astar/
A* explanation from Stanford: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
JavaScript A* implementation used by OP's question (Github):
Ramer Douglas Peuker Algorithm (Wikipedia): http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Javascript implementation of Douglas Peuker Algorithm: https://gist.github.com/rhyolight/2846020
A* Algorithm (Wikipedia): http://en.wikipedia.org/wiki/A*_search_algorithm

You can use a modified A* algorithm to account for changes in direction. While simplifying the result of the standard A* algorithm may yield good results, it may not be optimal. This modified A* algorithm will return a path of minimal length with the least number of turns.
In the modified A* algorithm, each position corresponds to eight different nodes, each with their own heading. For example, the position (1, 1) corresponds to the eight nodes
(1,1)-up, (1,1)-down, (1,1)-right, (1,1)-left,
(1,1)-up-left, (1,1)-up-right, (1,1)-down-left, and (1,1)-down-right
The heuristic distance from a node to the goal is the the heuristic distance from the corresponding point to the goal. In this case, you probably want to use the following function:
H(point) = max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))
The nodes that correspond to a particular position are connected to the nodes of the neighboring positions with the proper heading. For example, the nodes corresponding to the position (1,1) are all connected to the following eight nodes
(1,2)-up, (1,0)-down, (2,1)-right, (0,1)-left,
(0,2)-up-left, (2,2)-up-right, (0,0)-down-left, and (2,0)-down-right
The distance between any two connected nodes depends on their heading. If they have the same head, then the distance is 1, otherwise, we have made a turn, so the distance is 1+epsilon. epsilon represents an arbitrarily small value/number.
We know need to have a special case for the both the start and goal. The start and goal are both represented as a single node. At the start, we have no heading, so the distance between the start node and any connected node is 1.
We can now run the standard A* algorithm on the modified graph. We can map the returned path to a path in the original grid, by ignoring the headings. The total length of the returned path will be of the form n+m*epsilon. n is the total length of the corresponding path in the original grid, and m is the number of turns. Because A* returns a path of minimal length, the path in the original grid is a path of minimal length that makes the least turns.

I have come up with somewhat of a fix that is a simple addition to your original code, but it doesn't work in all situations (see image below) because we are limited to what the A* returns us. You can see my jsfiddle here
I added the following code to your simplifyPath function right before the return. What it does is strips out extra steps by seeing if there is a clear path between non-adjacent steps (looking at larger gaps first). It could be optimized, but you should get the gist from what I've got.
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);
You can see below that this removes the path that goes in on the left (as in the question) but also that not all the odd turns are removed. This solution only uses the points provided from the A*, not points in between on the path - for example, because the 2nd point does not have a straight unobstructed line to the 4th or 5th points, it cannot optimize point 3 out. It happens a lot less than the original code, but it still does give weird results sometimes.

In edition to nodes having references to their parent nodes. Also store which direction that node came from inside a variable. In my case there was only two possibilities horizontally or vertically. So I created two public static constants for each possibility. And a helper function named "toDirection" which takes two nodes and return which direction should be taken in order to go from one to another:
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;
}
}
Then you can change your weight calculation function to take turns into account. You can now give a small punishment for turns like:
public double calcGCost(Node current, Node neighbor) {
if(current.fromDirection == Node.toDirection(current, neighbor)) {
return 1;
} else{
return 1.2;
}
}
Full code: https://github.com/tezsezen/AStarAlgorithm

At the risk of potential down voting, I will try my best to suggest an answer. If you weren't using a third party plugin I would suggest a simple pop/push stack object be built however since you are using someone else's plugin it might be best to try and work along side it rather than against it.
That being said I might just do something simple like track my output results and try to logically determine the correct answer. I would make a simple entity type object literal for storage within an array of all possible path's? So the entire object's life span is only to hold position information. Then you could later parse that array of objects looking for the smallest turn count.
Also, since this third party plugin will do most of the work behind the scenes and doesn't seem very accessible to extract, you might need to feed it criteria on your own. For example if its adding more turns then you want, i.e. inside the door looking square, then maybe sending it the coordinates of the start and end arent enouugh. Perhaps its better to stop at each turn and send in the new coordinates to see if a straight line is now possible. If you did this then each turn would have a change to look and see if there is an obstruction stopping a straight line movement.
I feel like this answer is too simple so it must be incorrect but I will try nonetheless...
//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.
},
}
This way you could report all the data every step of the way and before you draw to the screen you could iterate through your array of these object literals and find the correct path by the lowest turnCount.

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>

Related

Isometric topological sort issue

I've just implemented a topological sort algorithm on my isometric game using this guide: https://mazebert.com/2013/04/18/isometric-depth-sorting/
The issue
Here's a little example (this is just a drawing to illustrate my problem because as we say, a picture is worth a thousand words), what I'm expecting is in left and the result of the topological sorting algorithm is in right
So in the right image, the problem is that the box is drawn BEFORE the character and I'm expecting it to be drawn AFTER like in the left image.
Code of the topological sorting algorithm (Typescript)
private TopologicalSort2() {
// https://mazebert.com/2013/04/18/isometric-depth-sorting/
for(var i = 0; i < this.Stage.children.length; i++) {
var a = this.Stage.children[i];
var behindIndex = 0;
for(var j = 0; j < this.Stage.children.length; j++) {
if(i == j) {
continue;
}
var b = this.Stage.children[j];
if(!a.isoSpritesBehind) {
a.isoSpritesBehind = [];
}
if(!b.isoSpritesBehind) {
b.isoSpritesBehind = [];
}
if(b.posX < a.posX + a.sizeX && b.posY < a.posY + a.sizeY && b.posZ < a.posZ + a.sizeZ) {
a.isoSpritesBehind[behindIndex++] = b;
}
}
a.isoVisitedFlag = 0;
}
var _sortDepth = 0;
for(var i = 0; i < this.Stage.children.length; ++i) {
visitNode(this.Stage.children[i]);
}
function visitNode(n: PIXI.DisplayObject) {
if(n.isoVisitedFlag == 0) {
n.isoVisitedFlag = 1;
if(!n.isoSpritesBehind) {
return;
}
for(var i = 0; i < n.isoSpritesBehind.length; i++) {
if(n.isoSpritesBehind[i] == null) {
break;
} else {
visitNode(n.isoSpritesBehind[i]);
n.isoSpritesBehind[i] = null;
}
}
n.isoDepth = _sortDepth++;
}
}
this.Stage.children.sort((a, b) => {
if(a.isoDepth - b.isoDepth != 0) {
return a.isoDepth - b.isoDepth;
}
return 0;
});
}
Informations
Player:
posX: [the x coordinate of the player]
posY: [the y coordinate of the player]
posZ: 0
sizeX: 1
sizeY: 1
sizeZ: 1
Box:
posX: [the x coordinate of the box]
posY: [the y coordinate of the box]
posZ: 0
sizeX: 3
sizeY: 1
sizeZ: 1
X and Y axis
Do you have any idea of the source of this problem? and maybe how to solve it?
The way to determine whether one object is before the other requires a bit more linear algebra.
First of all, I would suggest to translate the coordinates from the "world" coordinates to the "view" 2D coordinates, i.e. to the rows and columns of the display.
Note also that the original Z coordinate does not influence the sort order (imagine that an object would be lifted up along the Z axis: we can find a sort order where this move would not have any impact). So the above-mentioned translation could assume all points are at Z=0.
Let's take this set-up, but depicted from "above", so when looking along the Z axis down to the game floor:
In the picture there are 7 objects, numbered from 0 to 6. The line of view in the game would be from the bottom-left of this picture. The coordinate system in which I would suggest to translate some points is depicted with the red row/col axis.
The white diagonals in each object link the two points that would be translated and used in the algorithm. The assumption is that when one object is in front of another, their diagonal lines will not intersect. If they would, it would mean that objects are overlapping each other in the game world, which would mean they are like gasses, not solids :) I will assume this is not the case.
One object A could be in front of another object B when in the new coordinate system, the left-most column coordinate of B falls between the two column coordinates of A (or vice versa). There might not really be such an overlap when their Z coordinates differ enough, but we can ignore that, because when there is no overlap we can do no harm in specifying a certain order anyway.
Now, when the coordinates indicate an overlap, the coordinates of diagonals (of A and B) must be compared with some linear algebra formula, which will determine which one is in front of the other.
Here is your adapted function that does that:
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
I add a snippet where I completed the class with a minimum of code, enough to make it run and output the final order in terms of object index numbers (as they were originally inserted):
class Game {
constructor() {
this.Stage = { children: [] };
}
addObject(posX, posY, posZ, sizeX, sizeY, sizeZ) {
this.Stage.children.push({posX, posY, posZ, sizeX, sizeY, sizeZ,
id: this.Stage.children.length}); // add a unique id
}
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
toString() { // Just print the ids of the children
return JSON.stringify(this.Stage.children.map( x => x.id ));
}
}
const game = new Game();
game.addObject( 2, 2, 0, 1, 1, 1 );
game.addObject( 1, 3, 0, 3, 1, 1 );
game.addObject( 6, 1, 0, 1, 3, 1 );
game.addObject( 9, 3, 0, 1, 1, 1 );
game.addObject( 5, 3, 0, 1, 3, 1 );
game.addObject( 7, 2, 0, 1, 1, 1 );
game.addObject( 8, 2, 0, 3, 1, 1 );
game.topologicalSort();
console.log(game + '');
The objects in the snippet are the same as in the picture with the same numbers. The output order is [0,1,4,2,5,6,3] which is the valid sequence for drawing the objects.

Three.js performance very slow using onMouseMove with RayCaster

I'm building an application in three.js, however I'm having real problems with performance. This part of the application is based upon the Voxel Painter example. In my version, the user clicks on a cell to begin placement, drags the cursor to where they wish to end placement, and clicks to end.
function onDocumentMouseMove(event) {
//set up mouse and raycaster
event.preventDefault();
mouse.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1);
raycaster.setFromCamera(mouse, camera);
switch (buildMode) {
case buildModes.CORRIDOR:
scene.add(rollOverFloor);
var intersects = raycaster.intersectObjects(gridObject);
if (intersects.length > 0) {
var intersect = intersects[0];
if (beginPlace == true) {
//store the intersection position
var endPlace = new THREE.Vector3(0, 0, 0);
endPlace.copy(intersect.point).add(intersect.face.normal);
endPlace.divideScalar(step).floor().multiplyScalar(step).addScalar(step / step);
endPlace.set(endPlace.x, 0, endPlace.z);
corridorDrag(endPlace);
}
//if user hasn't begun to place the wall
else {
//show temporary wall on grid
rollOverFloor.position.copy(intersect.point).add(intersect.face.normal);
rollOverFloor.position.divideScalar(step).floor().multiplyScalar(step).addScalar(step / step);
rollOverFloor.position.set(rollOverFloor.position.x, 0, rollOverFloor.position.z);
}
}
break;
}
render();
}
The code above is called when the user moves the mouse (there are many buildmodes in the main application, but I have not included them here). This function simply gets a start and end point, the corridorDrag() function fills in the cells between the start and end points:
function corridorDrag(endPlace) {
deleteFromScene(stateType.CORRIDOR_DRAG);
var startPoint = startPlace;
var endPoint = endPlace;
var zIntersect = new THREE.Vector3(startPoint.x, 0, endPoint.z);
var xIntersect = new THREE.Vector3(endPoint.x, 0, startPoint.z);
var differenceZ = Math.abs(startPlace.z - zIntersect.z);
var differenceX = Math.abs(startPlace.x - xIntersect.x);
var mergedGeometry = new THREE.Geometry();
for (var i = 0; i <= (differenceZ / step); i++) {
for (var j = 0; j <= (differenceX / step); j++) {
var x = startPlace.x;
var y = startPlace.y;
var z = startPlace.z;
if (endPoint.x <= (startPlace.x )) {
if (endPoint.z <= (startPlace.z)) {
x = x - (step * j);
z = z - (step * i);
}
else if (endPoint.z >= (startPlace.z)) {
x = x - (step * j);
z = z + (step * i);
}
} else if (endPoint.x >= (startPlace.x)) {
if (endPoint.z <= (startPlace.z)) {
x = x + (step * j);
z = z - (step * i);
}
else if (endPoint.z >= (startPlace.z)) {
x = x + (step * j);
z = z + (step * i);
}
}
floorGeometry.translate(x, y, z);
mergedGeometry.merge(floorGeometry);
floorGeometry.translate(-x, -y, -z);
}
}
var voxel = new THREE.Mesh(mergedGeometry, tempMaterial);
voxel.state = stateType.CORRIDOR_DRAG;
scene.add(voxel);
tempObjects.push(voxel);
}
Firstly, the deleteFromScene() function removes all current highlighted cells from the scene (see below). The code then (I believe), should create a number of meshes, depending on the start and end points, and add them to the scene.
function deleteFromScene(state) {
tempObjects = [];
var i = scene.children.length;
while (i--) {
if (scene.children[i].state != undefined)
if (scene.children[i].state == state)
scene.children.splice(i, 1);
}
}
As I said, it is very, very slow. It also appears to be adding an obscene amount of vertices to the renderer, as seen in the WebGLRenderer stats window. I have no idea why it's adding so many vertices, but I'm assuming that's why it's rendering so slowly.
The application can be viewed here - the problem can be seen by clicking on one cell, dragging the cursor to the other end of the grid, and observing the time taken to fill in the cells.
Thank you in advance, this really is a last resort.
A few years ago Twitter put out an update. In this update they had just introduced infinite scrolling and on the day of its release the update was crashing users browsers. Twitter engineers did some investigating and found that the crashes were the result of the scroll event firing hundreds of times a second.
Mouse events can fire many MANY times a second and can cause your code to execute too often, which slows down the browser and (in many cases) crashes it. The solution for Twitter (and hopefully you) was simple: Poll your event.
Inside your mousemove event handler check that it has been some number of milliseconds since the last move event.
var lastMove = Date.now();
function onDocumentMouseMove(event) {
if (Date.now() - lastMove < 31) { // 32 frames a second
return;
} else {
lastMove = Date.now();
}
// your code here
}
I hope that helps!

How do I reuse objects in an array for a particle system in JavaScript/jQuery?

I'm in the process of building an entity system for a canvas game. This started from a simple particle emitter/updater which I am altering to accommodate a multi-particle/entity generator. Whilst I am usually ok with JavaScript/jQuery I am running into the limits of my experience as it concerns arrays and would gratefully accept any help on the following:
When I need a new particle/entity my current system calls a function to push an object into an array which contains variables for the entity updates.
Then the update function runs a for loop over the array, checking on the type variable to update the particle (position/colour/etc...). Previously I would then [array.splice] the particle, based on some condition. When I needed further particles/entities I would then push new particles.
What I would like to achieve here is:
In the makeParticle function, check over the particle array for any "dead" particles and if any are available reuse them, or push a new particle if not I have created a particleAlive var as a flag for this purpose.
var particles = [];
var playing = false;
function mousePressed(event) {
playing = !playing;
}
if(playing) {
makeParticle(1, 200, 200, 10, "blueFlame");
makeParticle(1, 300, 200, 10, "redFlame");
}
function makeParticle(numParticles, xPos, yPos, pRadius, pType) {
var i;
for (i = 0; i < numParticles; i++) {
var p = {
type : pType,
x : xPos,
y : yPos,
xVel : random(-0.5, 0.5),
yVel : random(-1, -3),
particleAlive : true,
particleRender : true,
size : pRadius
}; // close var P
particles.push(p);
// instead of pushing fresh particles all the time I would like the function, here, to check for free objects in the array
} // close for loop
} // close function makeParticle
function runtime() {
for(var i=0; i<particles.length; i++) {
var p = particles[i];
var thisType = p.type;
switch (thisType) {
case "blueFlame":
c.fillStyle = rgb(100,100,255);
c.fillCircle(p.x,p.y,p.size);
p.x += p.xVel;
p.y += p.yVel;
p.size*=0.9;
if (particles.size < 0.5) {
particleAlive = false;
particleRender = false;
} // close if
break;
case "redFlame":
c.fillStyle = rgb(255,100,100);
c.fillCircle(p.x,p.y,p.size);
p.x -= p.xVel;
p.y -= p.yVel;
p.size*=0.95;
if (particles.size < 0.5) {
particleAlive = false;
particleRender = false;
} // close if
break;
} // close switch
} // close function runtime
I've found previous answers to relate questions, but I've been unable to get it working within the makeParticle function, like how to assign the attributes of p to particle[j]:
var particleUseOldOrNew = function() {
for (var j = 0, len = particles.length; j < len; j++) {
if (particles[j].particleAlive === false)
// particles[j] = p;
return particle[j];
}
return null; // No dead particles found, create new "particles.push(p);" perhaps?
}
My personal opinion on the matter is that if you are making a new particle, it should be a new object, not a "re-using" of an old one with properties changed. Each new object should have a unique identifier, so if you need to track them (for development purposes, debugging, or later re-use), it is easy to do. Or at least keep a counter of the number of times you've re-used a particle object to represent a "new" particle! Though I guess if you've found that "re-using" improves performance (have you?), that's the way to go.
Anyway, enough pontificating, here is how I would do what you're asking (I assume speed is your main concern, so I did this with only native JS):
var particles = [];
//Function to create brand spanking new particle
function makeNewParticle(xPos, yPos, pRadius, pType){
return {
type : pType,
x : xPos,
y : yPos,
xVel : random(-0.5, 0.5),
yVel : random(-1, -3),
particleAlive : true,
particleRender : true,
size : pRadius
};
};
//Function to change the properties of an old particle to make a psuedo-new particle (seriously, why do you want to do this?)
function changeExistingParticle(existing, xPos, yPos, pRadius, pType){
existing.x = xPos;
existing.y = yPos;
existing.size = pRadius;
existing.type = pType;
return existing;
};
//Figure out the keys of dead particles in the particles[] array
function getDeadParticleKeys() {
var keys = [];
for(var p = 0; P < particles.length; p++) {
if (!particles[p].particleAlive) {
keys.push(p);
}
}
};
function makeParticle(numParticles, xPos, yPos, pRadius, pType) {
var d, i, deadParticles;
//Grab the "dead" particle keys
deadParticleKeys = getDeadParticleKeys();
numParticles -= deadParticleKeys.length;
//Replace each dead particle with a "live" one at a specified key
for (d = 0; d < deadParticleKeys.length; d++) {
particles[ deadParticleKeys[d] ] = changeExistingParticle(particles[ deadParticleKeys[d] ], xPos, yPos, pRadius, pType)
}
//If we had more particles than there were dead spaces available, add to the array
for (i = 0; i < numParticles; i++) {
particles.push( makeNewParticle(xPos, yPos, pRadius, pType) );
}
};
Now, here's how I recommend doing it: abandon the idea or "re-using" particles, make a separate constructor for each particle (will help immensely if you add methods to your particles in the future), and just scrap dead particles every time one is added:
//Make a constructor for a particle
var Particle = function(props){
if (typeof props === 'function') {
props = props();
}
this.type = props.type;
this.x = props.x;
this.y = props.y;
this.size = props.size;
};
Paticle.prototype.particleAlive = true;
Paticle.prototype.particleRender = true;
//Global particles list
var particles = [];
//Remove all dead element from a ParticleList
particles.clean = function(){
var p, keys;
for (p = this.length; p >= 0; p--) {
if (!p.particleAlive) {
this.splice(p, 1);
}
}
};
//Method for adding x amount of new particles - if num parameter isn't provided, just assume it to be 1
particles.add = function(props, num){
//First, clean out all the garbage!
this.clean();
//Now, append new particles to the end
var n, limit = (num && typeof num === 'number') ? num : 1;
for (n = 0; n < limit; n++){
particles.push( new Particle(props) );
}
};
//A couple examples
particles.add({ //Add a single blueFlame
type: "blueFlame",
size: 10,
x: 200,
y: 200
});
particles.add({ //Add 4 redFlames
type: "redFlame",
size: 10,
x: 300,
y: 200
}, 4);
particles.add(function(){//Add 4 greenFlames, with randomized XY cooridinates
this.x = Math.round(Math.random() * 1000);
this.y = Math.round(Math.random() * 1000);
this.size = 20;
this.type = "greenFlame";
}, 4);
Way less code to manage. I'm not sure which way is faster, but I'd bet the speed difference is negligible. Of course, you could check for yourself by making a quick jsPerf.

Comparing values within an array to each other

I am new to JavaScript and HTML and am working on a small game.
I have four 'enemies' whose position on the canvas is determined by the values in the arrays 'enemyX' and 'enemyY'.
Very simply, I want to detect if the enemies have 'collided', i.e have moved within 30px of each other (the enemy image is 30px by 30px).
What I want to do is subtract the value of the i th value in the array with the other values in the same array and see if this value is less than 30. The less than 30 part is an if statement, so how do I go about subtracting all the values from each other without many lines of code?
Here's what I have tried based on the answers below:
var count = 0;
var innercount = 0;
while (count <= 3) {
while (innercount<=3) {
collisionDetect(count, innercount, enemyX[count], enemyY[count], enemyX[innercount], enemyY[innercount])
innercount++
}
count++
}
var i = 0;
while (i < enemyX.length) {
if (collisionX[i] == 1) {
directionX = directionX*-1;
}
if (collisionY[i] == 1) {
direction = directionY*-1;
}
}
}
}
function collisionDetect(count, innercount, x, y, xX, yY ) {
if ((Math.abs(x-xX)) <=30) {
collisionX[count] = 1
collisionX[innercount] = 1
}
if ((Math.abs(y - yY)) <=30) {
collisionY[count] = 1
collisionY[innercount] = 1
}
return collisionX, collisionY;
}
This code gives me a blank canvas.
Detection of an intersection between two objects (assuming rectangular shape) and the position defines the center of the object.
function getRect(x, y, w, h)
{
var left = x - Math.floor(w / 2),
top = y - Math.floor(h / 2);
return {
left: left,
top: top,
right: left + w,
bottom: top + h
};
}
function intersects(A, B)
{
return A.right >= B.left &&
A.left <= B.right &&
A.bottom >= B.top &&
A.top <= B.bottom;
}
alert(intersects(getRect(12, 56, 30, 30), getRect(30, 40, 30, 30))); // true
The getRect() function can be modified to accommodate for a different anchor position, e.g. top left.
You could use a function:
function colliding(x1, y1, x2, y2){
return Math.abs(x1-x2) <= 30 && Math.abs(y1-y2) <= 30;
}
then use the function to test the different combination of enemies: (1,2), (1,3), (1,4), (2,3), (2,4), and (3,4).
So, for example, you would use: colliding(enemyX[2], enemyY[2], enemyX[3], enemyY[3]) to check if enemy 2 and 3 are colliding. Do that with all the combinations above.
EDIT: to make it more readable, you could define an additional function:
function enemiesColliding(e1, e2){
return colliding(enemyX[e1], enemyY[e1], enemyX[e2], enemyY[e2])
}
and then use it:
enemiesColliding(1,2) || enemiesColliding(1,3) || enemiesColliding(1,4) ||
enemiesColliding(2,3) || enemiesColliding(2,4) || enemiesColliding(3,4)
I am going to restate my understanding of you question, just so there is no confusion.
You have two arrays in paralleled , one for x cords, and one for y cords. Each ship has a element in both arrays.
So, for example, ship 12 could be found at xPos[12] and yPos[12], where xPos and yPos are the arrays from above.
It also stands to reason that this a communicative. If ship[a] collides with ship[b] then ship[b] has collided with ship[a]. And I think that hold for 3 + ships.
I would start by writing a distance function.
dist(x1,y1,x2,y2)
{
return Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
Then I would write code run through the arrays.
Every element must be compared to every other element, one time.
var counter = 0;
var indexOfShipThatHaveCollided = [];
while(counter < Xpos.length)
{
var innerCounter = counter;
while(innerCounter < Xpos.length)
{
t = dist(Xpos[counter],Ypos[counter],Xpos[innerCounter],Ypos[innerCounter])
if(t < 30)
{
indexOfShipThatHaveCollided.push(counter)
indexOfShipThatHaveCollided.push(innerCounter)
}
}
}
The code above compares every ship to every other ship ONCE.
It will compare ship[1] to ship[8], but it will not compare ship[8] to ship[1].
I did not test ANY of this code, but I hope it moves you in the right direction.
If you have anymore question just give me a comment.
According my understanding of your question below is my solution:
I think you need to find distance between two points. by using distance you can apply your logic.
To find distance following is the formula:
Given the two points (x1, y1) and (x2, y2),
the distance between these points is given by the formula:

HTML5 Canvas game development complication

I'm building a game using HTML5 canvas.
You can find it here, along with the source code: www.techgoldmine.com.
I'd make a jsFiddle, but in all honesty my attention span is too short (and myself mostly too stupid) to learn how it works.
I'm currently stuck at a function that looks at the positioning of certain elements on either side of the canvas and moves them so that the y-axis area they cover does not overlap. I call them turbines, but thin white rectangles would be more accurate. I suggest refreshing a few times to visually understand what's going on.
This is the function that spawns the turbines:
function gameStateNewLevel(){
for (var i = 0; i < 4; i++){
turbine = {};
turbine.width = 10;
turbine.height = 150;
turbine.y = Math.floor(Math.random()*600)
if (Math.random()*10 > 5){
turbine.side = leftSide;
}else{
turbine.side = rightSide;
}
turbine.render = function (){
context.fillStyle = "#FFFFFF"
context.fillRect(turbine.side, turbine.y, turbine.width,turbine.height);
}
turbine.PositionTop = turbine.y;
turbine.PositionBottom = turbine.y + turbine.height;
turbines.push(turbine);
}
context.fillStyle = "#FFFFFF"
switchGameState(GAME_STATE_PLAYER_START);
}
So far I've built (with the help of you wonderful people) a function (that is part of a loop) picking out each of these turbines, and starts comparing them to one another. I'm completely stumped when it comes to understanding how I'll get them to move and stop when needed:
function updateTurbines(){
var l = turbines.length-1;
for (var i = 0; i < l; i++){
var tempTurbine1 = turbines[i];
tempTurbine1.PositionTop = tempTurbine1.y;
tempTurbine1.PositionBottom = tempTurbine1.y + tempTurbine1.height;
for (var j = 0; j < l; j++) {
var tempTurbine2 = turbines[j];
tempTurbine2.PositionTop = tempTurbine2.y;
tempTurbine2.PositionBottom = tempTurbine2.y + tempTurbine2.height;
if ((tempTurbine1 !== tempTurbine2) && FIXME == true){
if(tempTurbine1.PositionBottom >= tempTurbine2.PositionTop){
turbines[j].y -=2;
//A while loop breaks the browser :(
}
}
}FIXME = false;
}
}
Any ideas or requests for additional explanation and info are more than welcome. I also have a feeling I'm severely over complicating this. Goddamn my head hurts. Bless you.
I'm afraid your code is a little bit messy do I decided to begin with a clean slate.
Use getters/setters for bottom and right. You can calculate them given the left/width and top/height values, respectively. This will save you from altering the complementary variable right when modifying e.g. left.
You seem to be looking for a collison detection algorithm for rectangles. This is quite easy if the rectangles have the same x-coordinate - two such rectangles do not collide if the bottom of the first is above the top of the other, or if the top of the first is under the bottom of the other. Use this algorithm along with a while loop to generate a new turbine as long as they collide.
This is what I ended up with (it's a separate piece of code as I stated, so you'll have to blend it into your game): http://jsfiddle.net/eGjak/249/.
var ctx = $('#cv').get(0).getContext('2d');
var Turbine = function(left, top, width, height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
};
Object.defineProperties(Turbine.prototype, {
bottom: {
get: function() {
return this.top + this.height;
},
set: function(v) {
this.top = v - this.height;
}
},
right: {
get: function() {
return this.left + this.width;
},
set: function(v) {
this.left = v - this.width;
}
}
});
var turbines = [];
function turbineCollides(tn) {
for(var i = 0; i < turbines.length; i++) {
var to = turbines[i];
// they do not collide if if one's completely under
// the other or completely above the other
if(!(tn.bottom <= to.top || tn.top >= to.bottom)) {
return true;
}
}
return false; // this will only be executed if the `return true` part
// was never executed - so they the turbine does not collide
}
function addTurbine() {
var t, h;
do { // do while loop because the turbine has to be generated at least once
h = Math.floor(Math.random() * 300);
t = new Turbine(0, h, 15, 100);
} while(turbineCollides(t));
turbines.push(t);
}
// add two of them (do not add more than 4 because they will always collide
// due to the available space! In fact, there may be no space left even when
// you've added 2.)
addTurbine();
addTurbine();
function draw() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 400, 400);
ctx.fillStyle = "white";
for(var i = 0; i < turbines.length; i++) {
var turbine = turbines[i];
ctx.fillRect(turbine.left, turbine.top,
turbine.width, turbine.height);
}
}
draw();ā€‹

Categories

Resources