Inconsistent AABB collision detection - javascript
I asked this question on gamedev yesterday but nobody has replied.
I'm making a 2d endless jumper as my first game and I'm running into some problems with the one-way collision detection between the player and the platform.
Collisions are detected correctly most of the time but occasionally the player inexplicably falls through the platform.
I suspect that it is ghosting through the platforms but I can't see why and I'm too close to it to be able to identify the source of the problem.
My code:
for (i = 0, len = this._platforms.length; i < len; i++) {
var p = this._platforms[i];
// Get difference between center of player and center of platform
p.proximity = Math.abs(p.x / 2 - player.x / 2) + Math.abs(p.y / 2 - player.y / 2);
if (p.closest()) {
if (p.collision()) {
player.onplatform = true;
player.y = p.y - player_s_right.height;
} else {
player.onplatform = false;
}
}
...
}
It loops through the platform objects, running the collision test only if the current platform is the closest one to the player.
Here are the closest() and collision() checks respectively:
// Return whether platform is the closest one to the player
Platform.prototype.closest = function() {
var minprox = Math.min.apply(Math, platforms._platforms.map(function (obj) {
return obj.proximity;
}));
return this.proximity === minprox;
}
// Collision detection
Platform.prototype.collision = function() {
// offset of 15 pixels to account for player's bandana
var px = player.direction > 0 ? player.x + 15 : player.x,
px2 = player.direction > 0 ? player.x + player_s_right.width : player.x + player_s_right.width - 15,
py = player.y + player_s_left.height,
py2 = player.y + player_s_right.height + player.yvelocity,
platx2 = this.x + platform_s.width,
platy2 = this.y + platform_s.height;
if (((px > this.x && px < platx2) || (px2 > this.x && px2 < platx2)) && (py2 >= this.y && py2 <= platy2) && py <= this.y) {
return true;
}
};
If anyone can shed some light on this I'd really appreciate it.
Related
p5.js mouseclick move over Canvas
I am trying to solve a school assignment in p5 JavaScript. I want something to move over the canvas after one mouseclick. But it only moves a little bit and I have to click several times to get it all the way over. What have I done wrong? Shouldn't the loop make it move all the way? Can post the whole code if needed. function CanvasPressed() { if ( mouseX > 0 && mouseX < 638 && mouseY > 0 && mouseY < 100 ) { Bird.stop(); Bird.play(); for ( let b = 640; b > 0; b--) { x = x - 0.05; } }
Alright, so you've got a couple misunderstood things, here: // purely aesthetic but in javascript functions are usually written as (i think) camelCase // so: canvasPressed() rather than CanvasPressed(), Class-es start with upper case function CanvasPressed() { // you can check for width & height if you want if ( mouseX > 0 && mouseX < width) if ( mouseX > 0 && mouseX < 638 && mouseY > 0 && mouseY < height ) { for ( let b = 640; b > 0; b--) // this, in this case, does the same as for(let i = 0; i < width; i ++) { x += 0.05 // 0.05 is very little, only a very small part of a pixel } // here it moves 0.05 * 640 (0.05 + 0.05 + 0.05 ... ) } } javascript naming conventions thingy if you want and this is how i would make it move through the canvas: let mouseWasPressed = false; let x = 20 function draw() { background(20); ellipse(x, height / 2, 40) if(mouseWasPressed) // don't need {} for 1 line after the if() x ++; // x = x + 1 shortening in javascript // } } function mousePressed(){ mouseWasPressed = true } if you don't want the "animation" you could use your previous method, but change the 0.05 to 1: for(let i = 0; i <= width; i ++) // you don't have to add parentheses for 1 line x ++; // x = x + 1 just a shortening in javascript OR just x = width // or x += width (x = x + width)
How to detect the side on which collision occured
This is my first post so I'm trying to make my problem as clear as possible. I'm making a game and I want to improve my collision detection. This is because I want to check what side is being hit and stop the player from moving past it without using something general like if(collision(player, enemy)) player.x = enemy.x - player.w(width) because if the player were to collide with the top it wouldn't keep the player on top. In the code it checks if any one of the statements is true and then returns it but it doesn't tell me which statement was the one that was equal to true so I can stop the player from moving accordingly, if that makes sense. If you have a more efficient collision detection for me to use it would be greatly appreciated. I've already tried to make a position variable to be equal to whatever side gets collided into and then stop the player from moving past it but it only works for the left side and won't let my player jump over the enemy or block. function collision(object1, object2) { return !( object1.x > object2.x + object2.w || object1.x + object1.w < object2.x || object1.y > object2.y + object2.h || object1.y + object1.h < object2.y ) } //Only works for the left side if(collision(player, enemy)) player.x = enemy.x - player.w I expect it to be able to tell me what side is being collided into and then either stop the player from moving past/into it and for the player to be able to be on top of the block/enemy without just being pushed to the left.
You'll want to calculate the distance between the x's and y's and also use the minimum distance that they could be colliding along each axis to find the depth along both axes. Then you can pick the smaller depth and move along that one. Here's an example: if(collision(player, enemy)){ // Most of this stuff would probably be good to keep stored inside the player // along side their x and y position. That way it doesn't have to be recalculated // every collision check var playerHalfW = player.w/2 var playerHalfH = player.h/2 var enemyHalfW = enemy.w/2 var enemyHalfH = enemy.h/2 var playerCenterX = player.x + player.w/2 var playerCenterY = player.y + player.h/2 var enemyCenterX = enemy.x + enemy.w/2 var enemyCenterY = enemy.y + enemy.h/2 // Calculate the distance between centers var diffX = playerCenterX - enemyCenterX var diffY = playerCenterY - enemyCenterY // Calculate the minimum distance to separate along X and Y var minXDist = playerHalfW + enemyHalfW var minYDist = playerHalfH + enemyHalfH // Calculate the depth of collision for both the X and Y axis var depthX = diffX > 0 ? minXDist - diffX : -minXDist - diffX var depthY = diffY > 0 ? minYDist - diffY : -minYDist - diffY // Now that you have the depth, you can pick the smaller depth and move // along that axis. if(depthX != 0 && depthY != 0){ if(Math.abs(depthX) < Math.abs(depthY)){ // Collision along the X axis. React accordingly if(depthX > 0){ // Left side collision } else{ // Right side collision } } else{ // Collision along the Y axis. if(depthY > 0){ // Top side collision } else{ // Bottom side collision } } } } Working example Here's a working example that you can play around with. Use the arrow keys to move the player around. player = { x: 9, y: 50, w: 100, h: 100 } enemy = { x: 100, y: 100, w: 100, h: 100 } output = document.getElementById("collisionType"); canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d") function collision(object1, object2) { return !( object1.x > object2.x + object2.w || object1.x + object1.w < object2.x || object1.y > object2.y + object2.h || object1.y + object1.h < object2.y ) } function draw() { ctx.clearRect(0, 0, 400, 400) ctx.lineWidth = "5" ctx.beginPath(); ctx.strokeStyle = "red"; ctx.rect(player.x, player.y, player.w, player.h); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = "blue"; ctx.rect(enemy.x, enemy.y, enemy.w, enemy.h); ctx.stroke(); } function handleCollision() { if (collision(player, enemy)) { var playerHalfW = player.w / 2 var playerHalfH = player.h / 2 var enemyHalfW = enemy.w / 2 var enemyHalfH = enemy.h / 2 var playerCenterX = player.x + player.w / 2 var playerCenterY = player.y + player.h / 2 var enemyCenterX = enemy.x + enemy.w / 2 var enemyCenterY = enemy.y + enemy.h / 2 // Calculate the distance between centers var diffX = playerCenterX - enemyCenterX var diffY = playerCenterY - enemyCenterY // Calculate the minimum distance to separate along X and Y var minXDist = playerHalfW + enemyHalfW var minYDist = playerHalfH + enemyHalfH // Calculate the depth of collision for both the X and Y axis var depthX = diffX > 0 ? minXDist - diffX : -minXDist - diffX var depthY = diffY > 0 ? minYDist - diffY : -minYDist - diffY // Now that you have the depth, you can pick the smaller depth and move // along that axis. if (depthX != 0 && depthY != 0) { if (Math.abs(depthX) < Math.abs(depthY)) { // Collision along the X axis. React accordingly if (depthX > 0) { output.innerHTML = "left side collision" } else { output.innerHTML = "right side collision" } } else { // Collision along the Y axis. if (depthY > 0) { output.innerHTML = "top side collision" } else { output.innerHTML = "bottom side collision" } } } } else { output.innerHTML = "No collision" } } keyStates = [] function handleKeys() { if (keyStates[39]) { player.x += 2 //Move right } else if (keyStates[37]) { player.x -= 2 //Move left } if (keyStates[38]) { player.y -= 2 //Move up } if (keyStates[40]) { player.y += 2 //Move down } } function main() { handleKeys(); draw(); handleCollision(); window.requestAnimationFrame(main); } window.onkeydown = function(e) { keyStates[e.keyCode] = true } window.onkeyup = function(e) { keyStates[e.keyCode] = false } main(); <h2 id="collisionType"></h2> <canvas id="canvas" width='300' height='300'></canvas> Reacting to the collision Now that you know the side the collision happened on, it should be fairly trivial to decide how to react. It would be very similar to what you are currently doing for the left side just flip some signs around and change the axis. Other Considerations You may want to take into account your player's velocity (if it has one) otherwise the detection may fail. If the player's velocity is too high, it might 'tunnel' through the enemy and no collision will be detected. The player's movement can also look jittery if the velocity is not stopped upon collision Can your objects rotate or have more than 4 sides? If so, you'll probably want to use another method as described below. Here's a good answer to another post that talks in depth about collision engines Other Methods As for other collision detection methods, there's quite a few but one that comes to mind is Separating Axis Theorem which is a little more complex than what you have but will work with more complex convex shapes and rotation. It also tells you the direction and distance needed to move to resolve the collision. Here's a site that has interactive examples and goes in-depth on the subject. It doesn't appear to give a full implementation but those can be found other places.
JavaScript collision detection with objects in a multi-dimensional array
I'm currently coding a Pac-man clone with p5.js, and have ran into an issue. I have created a function which draws the map by using a multi-dimensional array, drawing a wall block where a 1 is, and nothing where a 0 is. This works fine, however i'm struggling to detect collision between the player and the walls. I have tried to use a for loop to go through the array, checking the x and y co-ordinates to see if there is a collision, however it doesn't register at all.This is the code i have used to detect collision: for(i=0;i<walls.length;i++){ walls[i].draw(); if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y > walls[i].y && player.y < walls[i].y + gridsize){ console.log('collision') } } I can't see where the issue is here, as it seems to have worked in other programs. This runs in the Draw() function, meaning it loops 30 times a second. This is the full code, incase the issue lies elsewhere: var gridsize = 20; var walls = []; var dots = []; var player; var score =0; var maps = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,1,1,0,1,1,1,1,1,1,0,1,1,0,1], [1,0,1,1,0,0,0,0,0,0,0,0,1,1,0,1], [1,0,0,0,0,1,1,1,1,1,1,0,0,0,0,1], [1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1], [1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,1], [1,0,1,0,0,1,0,0,0,0,1,0,0,1,0,1], [1,0,1,0,0,1,0,0,0,0,1,0,0,1,0,1], [1,0,1,0,0,1,1,1,1,1,1,0,0,1,0,1], [1,0,1,0,0,0,0,2,0,0,0,0,0,1,0,1], [1,0,1,1,1,0,1,1,1,1,0,1,1,1,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,1,1,1,1,1,1,1,1,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]; function setup(){ createCanvas(320,320); frameRate(30); createMap(); } function draw(){ background(51); for(i=0;i<walls.length;i++){ walls[i].draw(); if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y > walls[i].y && player.y < walls[i].y + gridsize){ console.log('collision') } } fill('white'); text('Score: ' + score, 5,10); for(i=0;i<dots.length;i++){ if(player.x == dots[i].x && player.y == dots[i].y && dots[i].collect == false){ dots[i].collect = true; score++; } dots[i].draw(); } player.update(); player.draw(); } function Block(x,y){ this.x = x; this.y = y; this.draw = function(){ fill('black'); rect(this.x,this.y,gridsize,gridsize); } } function Dot(x,y){ this.x = x; this.y = y; this.collect = false; this.draw = function(){ if(!this.collect){ fill('yellow'); ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/3,gridsize/3); }else if(this.collect){ noFill(); noStroke(); ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/3,gridsize/3); } } } function Player(x,y){ this.x = x; this.y = y; this.update = function(){ if(keyIsDown(UP_ARROW) && frameCount%5 == 0){ player.y -= gridsize; } if(keyIsDown(DOWN_ARROW) && frameCount%5 == 0){ player.y += gridsize; } if(keyIsDown(LEFT_ARROW) && frameCount%5 == 0){ player.x -= gridsize; } if(keyIsDown(RIGHT_ARROW) && frameCount%5 == 0){ player.x += gridsize; } } this.draw = function(){ fill('blue'); ellipse(this.x+gridsize/2,this.y+gridsize/2,gridsize/1.2,gridsize/1.2); } } function createMap(){ for(i=0;i<maps.length;i++){ for(j=0;j<maps[i].length;j++){ if (maps[i][j] == 1){ walls.push(new Block(j*gridsize,i*gridsize)); }else if(maps[i][j] == 0){ dots.push(new Dot(j*gridsize,i*gridsize)) }else if(maps[i][j] = 2){ player = new Player(j*gridsize,i*gridsize) } } } } I presume the issue lies with the fact that the walls are stored in an array, however i have done very similar programs in which the same code works.
PacMan controls The best way to check for this type of map is to use the player's input. The player must line up with the walls so assuming the player position is relative to the top left and the player is one map unit wide and deep. Key input requests a direction to move dx, dy hold the directions which could be more than one at a time. If dx or dy are not 0 then first check if the player is lined up with a passage, if so then check if a block is in the direction of travel. If the player is not lined up or blocked set the movement var to 0 After checking both x and y directions, then if dx or dy have a value then that must be a valid move. Code changes Remove the player collision checking code from the main loop and call the player update function with the current map as the 2D original. player.update(maps); // move the player Change the Player and update function function Player(x,y){ this.x = x; this.y = y; var dx = 0; // hold current movement var dy = 0; const speed = 1; // per Frame pixel speed best as an integer (whole number) and evenly divisible into gridSize // need the map so that must be passed to the update function this.update = function(map){ // assuming keys are held to move up to stop dx = 0; // stop by default dy = 0; if (keyIsDown(UP_ARROW)) { dy = -speed } if (keyIsDown(DOWN_ARROW)) { dy = speed } if (keyIsDown(LEFT_ARROW)) { dx = -speed } if (keyIsDown(RIGHT_ARROW)){ dx = speed } // get player map coords var x = Math.floor(this.x / gridSize); // get map coord var y = Math.floor(this.y / gridSize); // get map coord // the two if statement are best aas function // so you can select which one to call first. Depending on the latest // new keys down and if the result allows movement in that // direction then set the other direction to zero. if (dy !== 0) { // is moving up or down? if (this.y % gridsize === 0) { // only if lined up if (dy > 0){ // is moving down if (map[y + 1][x] === 1) { // down blocked dy = 0; } }else if (map[y - 1][x] === 1) { // up blocked dy = 0; } } else { // block move if not lined up with passage dy = 0; } } if(dx !== 0){ // is moving left or right? if (this.x % gridsize === 0) { // only if lined up if (dx > 0) { // is moving right if (map[y][x + 1] === 1) { // right blocked dx = 0; } } else if (map[y][x - 1] === 1) { // left blocked dx = 0; } } else { // block move if not lined up with passage dx = 0; } } // could have two moves, but can only move left right or up down // you need to add some input smarts to pick which one // this just favours up down if(dy !== 0) { dx = 0 }; // only valid moves will come through the filter above. // so move the player. this.x += dx; this.y += dy; } Adding more smarts Note I have changed the way the player moves, I set a speed per frame (1 pixel) that must be an even divisor of gridSize. The code above is the simplest implementation. This type of games needs some extra smarts in controls. You should check in the direction of the newest key down. Ie if the player traveling down and right is pressed then moving right should have priority. If player moving right and left is pressed then you should move left, not keep moving right. Extras While looking at this question I wanted to visualize the map. Maps as arrays are painful to create and modify, and very hard to find mistakes in. Much easier as a a set of strings that gets converted to an array at run time. As i have done the conversion no point wasting it. maps is identical to the original array but now easier to read and change. const maps = [ "################", "# #", "# ## ###### ## #", "# ## ## #", "# ###### #", "#### ####", "# ## ## #", "# # # # # #", "# # # # # #", "# # ###### # #", "# # 2 # #", "# ### #### ### #", "# #", "# ######## #", "# #", "################" ].map(row => row.split("").map(c => c === "#" ? 1 : c === " " ? 0 : 2));
I'm not quite sure why you're using rectangle-rectangle collision detection when you could just use grid-based collision detection. You could just use the array directly. But since you are using rectangle-rectangle collision, this line looks a little bit off: if(player.x > walls[i].x && player.x < walls[i].x + gridsize && player.y > walls[i].y && player.y < walls[i].y + gridsize){ You're checking whether the left edge of the player is inside the wall and whether the top edge of the player is inside the wall. But you aren't detecting the other edges. Usually you'd want to do something like this: if(rectOneRight > rectTwoLeft && rectOneLeft < rectTwoRight && rectOneBottom > rectTwoTop && rectOneTop < rectTwoBottom){ Notice how this if statement checks all of the edges, not just the top and left. But like I said, you might be better off just using grid collision detection, since you already have a grid of walls. Shameless self-promotion: here is a tutorial on collision detection. It's written for Processing, but everything should translate pretty directly to P5.js.
if the player is not sprite here then point-in-rect collision detection will be appropriate here. // point in rect collision detection function pointInRect (x, y, rect) { return inRange(x, rect.x, rect.x + gridsize) && inRange(y, rect.y, rect.y + gridsize); } // check a value is in range or not function inRange (value, min, max) { return value >= Math.min(min, max) && value <= Math.max(min, max); } // checking player is hitting the wall or not if(pointInRect(player.x,player.y,walls[i].x,walls[i].y)) { console.log('collision') }
how to stop an object when it collides with another object in javascript canvas
OK, so I am making a Pacman game using HTML5 . the problem is whenever I hit one of the brick blocks I want the sprite to stop moving but it keeps going until it hits the left most brick object. how do I fix this? please help... here is the code I'm using to make the sprite stop. here is all my code, if you have time, please parse it, and tell me what I have done wrong. function init(){ var canvas=document.getElementById("ctx"); var ctx = canvas.getContext("2d"); var player = {sx:6,sy:6,sw:15,sh:15,x:230,y:377,w:20,h:20} var ss = new Image(); ss.src="SS.png"; var right=false,left= true,up = false,down = false var b = [{x:0,y:0,w:25,h:((canvas.height/2)-25)},{x:0,y:((canvas.height/2)),w:25,h:((canvas.height/2))},{x:50,y:25,w:50,h:50},{x:125,y:25,w:75,h:50},{x:225,y:0,w:25,h:75},{x:275,y:25,w:75,h:50},{x:375,y:25,w:50,h:50},{x:50,y:100,w:50,h:25},{x:125,y:100,w:25,h:125},{x:125,y:150,w:75,h:25},{x:175,y:100,w:125,h:25},{x:225,y:125,w:25,h:50},{x:325,y:100,w:25,h:125},{x:275,y:150,w:75,h:25},{x:375,y:100,w:50,h:25},{x:25,y:150,w:75,h:75},{x:375,y:150,w:75,h:75},{x:375,y:250,w:75,h:75},{x:25,y:250,w:75,h:75},{x:125,y:250,w:25,h:75},{x:325,y:250,w:25,h:75},{x:175,y:300,w:125,h:25},{x:225,y:325,w:25,h:50},{x:50,y:350,w:50,h:25},{x:75,y:350,w:25,h:75},{x:125,y:350,w:75,h:25},{x:275,y:350,w:75,h:25},{x:375,y:350,w:50,h:25},{x:375,y:350,w:25,h:75},{x:25,y:400,w:25,h:25},{x:125,y:400,w:25,h:75},{x:50,y:450,w:150,h:25},{x:275,y:450,w:150,h:25},{x:325,y:400,w:25,h:50},{x:425,y:400,w:25,h:25},{x:175,y:400,w:125,h:25},{x:225,y:425,w:25,h:50},{x:450,y:0,w:50,h:((canvas.height/2)-25)},{x:450,y:(canvas.height/2),w:50,h:((canvas.height/2))}]; function gen(){ for(var i=0;i<b.length;i++){ ctx.fillStyle="blue" ctx.fillRect(b[i].x,b[i].y,b[i].w,b[i].h) } ctx.drawImage(ss,player.sx,player.sy,player.sw,player.sh,player.x,player.y,player.w,player.h) } function move(){ for(var i=0;i<b.length;i++){ //((a.x + a.width) < b.x) if(left && player.x > b[i].x && (player.x + player.w) < (b[i].x + b[i].w) && player.y > b[i].y && (player.y + player.h) < (b[i].y + b[i].h)) { // here you can tell that the user is colliding an object player.x-=1 } else { } } } function animate(){ ctx.save() ctx.clearRect(0,0,canvas.width,canvas.width); gen() move() ctx.restore(); } var ani = setInterval(animate, 30) } window.addEventListener("load",function(){ init() })
First, I can see a problem with the first part of your if condition: left = true && ... Should be left === true && ... Or even better left && ... Now for the collision part it usually is top-left or in the middle of the object I'd suggest this top-left origin collision check: if(left && (player.x >= b[i].x && player.x + player.w <= b[i].x + b[i].w) && (player.y >= b[i].y && player.y + player.h <= b[i].y + b[i].h) { // here you can tell that the user is colliding an object } It checks several cases, this part (player.x >= b[i].x && player.x + player.w <= b[i].x + b[i].w) Will meet requirements if the player's x (with its width component) inside the occupied x range of the current object The second part (player.y >= b[i].y && player.y + player.h <= b[i].y + b[i].h) Will meet requirements if the player's y (with its height component) is inside the occupied y range of the current object. It will only execute the if statement if the condition is satisfied for both of the above cases. You can tell if you should reposition the player on the left or on the right, by substracting the players x component to the object's x component, same goes for top or bottom with y component. The previous sentence is only valid if you move in a grid cell by cell.
html5 canvas object collisions and physics
I'm working on a simple html5 platformer, building of a previous game but having trouble with collisions and physics. below is a sample of code regarding the floor and how it handles collisions. the full code is here. http://www.ambitiongames.co.uk/dev/game.php with the game being here http://www.ambitiongames.co.uk/dev/ the problem I'm having is the collisions are based on events grabbing which sometimes leaves the player in the floor not on it. also, due to the way jumping and falling work there's no gravity, meaning a player can simply jump on a higher ledge and then walk off into the air. whats the a good way to set up a permanent state of gravity ? whats a good way to interact / collide with the floor or other objects ? that.draw = function(){ try { ctx.drawImage(that.image, that.x, that.y, that.width, that.height); } catch (e) { } } return that; }; var nrOffloors = 40, floors = [], floorWidth = 20, floorHeight = 40; var generatefloor = function(){ for (var i = 0; i < 10; i++) { floors[i] = new Floor(i * 20,280); } for (var i = 10; i < 12; i++) { floors[i] = new Floor(i * 20,260); } }(); var checkCollisionfloor = function(){ floors.forEach(function(e, ind){ if ( (player.X < e.x + floorWidth) && (player.X + player.width > e.x) && (player.Y + player.height > e.y) && (player.Y + player.height < e.y + floorHeight) ) { e.onCollide(); } }); }
You can try a library like Box2D.js. It handles everything from collision detection to gravity. Check out it's demos.