Collision detection should not result in teleportation of the object

Solved, see bottom of post for final algorithm

Background: I am working on a 2D platformer using JS and an HTML canvas element. The level map is tile based, but the player is not tied to the tiles. I use the collision detection algorithm described in "Tiny Platformer" in Code inComplete . It basically works, with the exception of one case of the hem (or “protrusions”).

Problem:

gif of ledge issue

The player falls, and also moves to the right, into the wall. When he falls, he teleports to the height of the ledge. Instead, the player should normally fall without teleporting.

Is there a way to change the algorithm to prevent this behavior? If not, can you suggest an alternative collision detection algorithm? Ideally, any correction does not imply that the player always crashes, because in the game the player’s fall direction switches between up / down / left / right.

Algorithm:

  • The player’s new position is calculated in the absence of collisions. (Not shown in the code below)

  • A function called getBorderTiles takes an object (player) and returns tiles by touching each of the 4 players. Since the player is nothing more than a tile, these boundary tiles are necessarily the only tiles that the player touches. Please note that some of these fragments may be the same. For example, if a player occupies only one column, the tiles with the upper left / upper right layer will be the same as the lower left / right lower tiles. If this happens, getBorderTiles will still return all four fragments, but some of them will be the same.

  • He checks these boundary tiles on a level map (2D array) to make sure they are solid. If the tile is solid, the object collides with this tile.

  • It checks for a collision up / down / left / right. If the player moves down and collides with the bottom plate, but does not collide with the corresponding plate up, the player collides. If a player moves to the left and collides with the left tile, but does not collide with the corresponding right tile, he is faced to the left. Etc. Up / down checks are performed before a left / right check. Variables that store boundary tiles are adjusted if an up / down collision is performed before performing a left / right check. For example, if a player collides, he will be pressed into the upper tiles, so BL / BR tiles will now be the same as TL / TR.

  • Player x, y and speed are adjusted depending on the directions he is facing.

Why the algorithm fails:

See this image.

The lower right tile is solid, but the upper right is not, therefore (step 4) the player collides and (step 5) he pushes up. In addition, it collides with the BR plate, but not with the BL, so it collides to the right and moves to the left. Towards the end, the player is displayed just above and to the left of the ledge. In fact, he teleported.

Attempted solution: I tried to fix this, but it only created another problem. I added a check so that the player only collides with the tile if it was at some distance inside this tile (say, 3px). If the player was only in the BR plate, the algorithm would not have recorded a downward collision, so the player would not teleport. However, if the player fell to the ground in another scenario, he did not recognize the collision until the player hit the ground far. The player was trembling when he fell a little into the ground, he was pushed back to the ground, fell again, etc.

Thanks for reading this far. I really appreciate your feedback.

Current algorithm code:

 var borderTiles = getBorderTiles(object), //returns 0 (a falsy value) for a tile if it does not fall within the level tileTL = borderTiles.topLeft, tileTR = borderTiles.topRight, tileBL = borderTiles.bottomLeft, tileBR = borderTiles.bottomRight, coordsBR = getTopLeftXYCoordinateOfTile(tileBR), //(x, y) coordinates refer to top left corner of tile xRight = coordsBR.x, //x of the right tile(s) (useful for adjusting object position since it falls in middle of 4 tiles) yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object position since it falls in middle of 4 tiles) typeTL = tileTL ? level.map[tileTL.row][tileTL.col] : -1, //if tileTL is in the level, gets its type, otherwise -1 typeTR = tileTR ? level.map[tileTR.row][tileTR.col] : -1, typeBL = tileBL ? level.map[tileBL.row][tileBL.col] : -1, typeBR = tileBR ? level.map[tileBR.row][tileBR.col] : -1, collidesTL = typeTL == TILETYPE.SOLID, //true if the tile is solid collidesTR = typeTR == TILETYPE.SOLID, collidesBL = typeBL == TILETYPE.SOLID, collidesBR = typeBR == TILETYPE.SOLID, collidesUp = false, collidesDown = false, collidesLeft = false, collidesRight = false; //down and up if (object.vy < 0 && ((collidesTL && !collidesBL) || (collidesTR && !collidesBR))) { collidesUp = true; /*The object is pushed out of the bottom row, so the bottom row is now the top row. Change the collides__ variables as this affects collision testing, but is it not necessary to change the tile__ variables. */ collidesTL = collidesBL; collidesTR = collidesBR; } else if (object.vy > 0 && ((collidesBL && !collidesTL) || (collidesBR && !collidesTR))) { collidesDown = true; /*The object is pushed out of the bottom row, so the bottom row is now the top row. Change the collides__ variables as this affects collision testing, but is it not necessary to change the tile__ variables. */ collidesBL = collidesTL; collidesBR = collidesTR; } //left and right if (object.vx < 0 && ((collidesTL && !collidesTR) || (collidesBL && !collidesBR))) { collidesLeft = true; } else if (object.vx > 0 && ((collidesTR && !collidesTL) || (collidesBR && !collidesBL))) { collidesRight = true; } if (collidesUp) { object.vy = 0; object.y = yBottom; } if (collidesDown) { object.vy = 0; object.y = yBottom - object.height; } if (collidesLeft) { object.vx = 0; object.x = xRight; } if (collidesRight) { object.vx = 0; object.x = xRight - object.width; } 

UPDATE: Solution with a solution of Maraca. The algorithm is given below. It basically tests (x, y) and resolves collisions, and then checks (y, then x) and resolves conflicts this way. Regardless of the test results in a player moving a shorter distance, this is the one that is ultimately used.

Interestingly, this requires a special case when a player collides both in the upper and left directions. Perhaps this is due to the fact that the player’s coordinate (x, y) is in the upper left corner. In this case, a test should be used that leads the player moving the LONG distance to be used. In this gif it’s clear:

gif showing why special case is needed

The player is a black box, and the yellow box represents where the player would be if he used another test (a test that resulted in the player moving a greater distance). Ideally, the player should not move into the wall, but instead should be a yellow block. Therefore, a long distance test should be used in this scenario.

Here is a quick and dirty implementation. This is not optimized at all, but I hope it shows the algorithm quite clearly.

 function handleCollision(object) { var borderTiles = getBorderTiles(object), //returns 0 (a falsy value) for a tile if it does not fall within the level tileTL = borderTiles.topLeft, tileTR = borderTiles.topRight, tileBL = borderTiles.bottomLeft, tileBR = borderTiles.bottomRight, coordsBR = getTopLeftXYCoordinateOfTile(tileBR), //(x, y) coordinates refer to top left corner of tile xRight = coordsBR.x, //x of the right tile(s) (useful for adjusting object position since it falls in middle of 4 tiles) yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object position since it falls in middle of 4 tiles) typeTL = tileTL ? level.map[tileTL.row][tileTL.col] : -1, //if tileTL is in the level, gets its type, otherwise -1 typeTR = tileTR ? level.map[tileTR.row][tileTR.col] : -1, typeBL = tileBL ? level.map[tileBL.row][tileBL.col] : -1, typeBR = tileBR ? level.map[tileBR.row][tileBR.col] : -1, collidesTL = typeTL == TILETYPE.SOLID, //true if the tile is solid collidesTR = typeTR == TILETYPE.SOLID, collidesBL = typeBL == TILETYPE.SOLID, collidesBR = typeBR == TILETYPE.SOLID, collidesUp = false, collidesDown = false, collidesLeft = false, collidesRight = false, originalX = object.x, //the object coordinates have already been adjusted according to its velocity, but not according to collisions originalY = object.y, px1 = originalX, px2 = originalX, py1 = originalY, py2 = originalY, vx1 = object.vx, vx2 = object.vx, vy1 = object.vy, vy2 = object.vy, d1 = 0, d2 = 0, conflict1 = false, conflict2 = false, tempCollidesTL = collidesTL, tempCollidesTR = collidesTR, tempCollidesBL = collidesBL, tempCollidesBR = collidesBR; //left and right //step 1.1 if (object.vx > 0) { if (collidesTR || collidesBR) { vx1 = 0; px1 = xRight - object.width; conflict1 = true; tempCollidesTR = false; tempCollidesBR = false; } } if (object.vx < 0) { if (collidesTL || collidesBL) { vx1 = 0; px1 = xRight; conflict1 = true; tempCollidesTL = false; tempCollidesBL = false; collidesLeft = true; } } //step 2.1 if (object.vy > 0) { if (tempCollidesBL || tempCollidesBR) { vy1 = 0; py1 = yBottom - object.height; } } if (object.vy < 0) { if (tempCollidesTL || tempCollidesTR) { vy1 = 0; py1 = yBottom; collidesUp = true; } } //step 3.1 if (conflict1) { d1 = Math.abs(px1 - originalX) + Math.abs(py1 - originalY); } else { object.x = px1; object.y = py1; object.vx = vx1; object.vy = vy1; return; //(the player x and y position already correspond to its non-colliding values) } //reset the tempCollides variables for another runthrough tempCollidesTL = collidesTL; tempCollidesTR = collidesTR; tempCollidesBL = collidesBL; tempCollidesBR = collidesBR; //step 1.2 if (object.vy > 0) { if (collidesBL || collidesBR) { vy2 = 0; py2 = yBottom - object.height; conflict2 = true; tempCollidesBL = false; tempCollidesBR = false; } } if (object.vy < 0) { if (collidesTL || collidesTR) { vy2 = 0; py2 = yBottom; conflict2 = true; tempCollidesTL = false; tempCollidesTR = false; } } //step 2.2 if (object.vx > 0) { if (tempCollidesTR || tempCollidesBR) { vx2 = 0; px2 = xRight - object.width; conflict2 = true; } } if (object.vx < 0) { if (tempCollidesTL || tempCollidesTL) { vx2 = 0; px2 = xRight; conflict2 = true; } } //step 3.2 if (conflict2) { d2 = Math.abs(px2 - originalX) + Math.abs(py2 - originalY); console.log("d1: " + d1 + "; d2: " + d2); } else { object.x = px1; object.y = py1; object.vx = vx1; object.vy = vy1; return; } //step 5 //special case: when colliding with the ceiling and left side (in which case the top right and bottom left tiles are solid) if (collidesTR && collidesBL) { if (d1 <= d2) { object.x = px2; object.y = py2; object.vx = vx2; object.vy = vy2; } else { object.x = px1; object.y = py1; object.vx = vx1; object.vy = vy1; } return; } if (d1 <= d2) { object.x = px1; object.y = py1; object.vx = vx1; object.vy = vy1; } else { object.x = px2; object.y = py2; object.vx = vx2; object.vy = vy2; } } 
+5
source share
1 answer

This is because you first detect collisions in both directions, and then adjust the position. up / down is updated first (gravity direction). First, adjusting the "left / right" will only worsen the situation (after each posture, you can teleport to the right or left).

The only quick and dirty fix I could come up with (gravitationally invariant):

  • Calculate the collision of two relevant points in one direction (for example, when passing to the left only along the left two points). Then adjust the speed and position in that direction.

  • Calculate the collision of two (adjusted) relevant points in the other direction. Adjust the position and speed of this directin in a collision.

  • If there was no collision in step 1, you can save the changes and return. Otherwise, calculate the distance dx + dy compared to the starting position before step 1.

  • Repeat steps 1 through 3., but this time you will start from a different direction first.

  • Make the change with a shorter distance (if you have not already found a good change in step 3.).

EDIT: Example

 sizes: sTile = 50, sPlayer = 20 old position (fine, top-left corner): oX = 27, oY = 35 speeds: vX = 7, vY = 10 new position: x = oX + vX = 34, y = oY + vY = 45 => (34, 45) solid: tile at (50, 50) 1.1. Checking x-direction, relevant points for positive vX are the ones to the right: (54, 45) and (54, 65). The latter gives a conflict and we need to correct the position to p1 = (30, 45) and speed v1 = (0, 10). 2.1. Checking y-direction based on previous position, relevant points: (30, 65) and (50, 65). There is no conflict, p1 and v1 remain unchanged. 3.1. There was a conflict in step 1.1. so we cannot return the current result immediately and have to calculate the distance d1 = 4 + 0 = 4. 1.2. Checking y-direction first this time, relevant points: (34, 65) and (54, 65). Because the latter gives a conflict we calculate p2 = (34, 30) and v2 = (7, 0). 2.2. Checking x-direction based on step 1.2., relevant points: (54, 30) and (54, 50). There is no conflict, p2 and v2 remain unchanged. 3.2. Because there was a conflict in step 1.2. we calculate the distance d2 = 15. 5. Change position and speed to p1 and v1 because d1 is smaller than d2. 
+1
source

Source: https://habr.com/ru/post/1268247/


All Articles