已解决,最终算法见帖子底部
背景:我正在使用 JS 和 HTML Canvas 元素开发 2D 平台游戏。关卡 map 是基于图块的,但玩家不会被夹在图块上。我正在使用 "Tiny Platformer" on Code inComplete 中概述的碰撞检测算法.除了一种边缘情况(或“壁架”情况)外,它在很大程度上有效。
问题:
玩家正在跌倒并向右移动,进入墙壁。当它下落时,它会传送到壁架的高度。相反,玩家应该在没有传送的情况下正常坠落。
有没有办法改变算法来防止这种行为?如果没有,你能建议一个替代的碰撞检测算法吗? 理想情况下,任何修复都不会假设玩家总是摔倒,因为在游戏中玩家的摔倒方向会在上/下/左/右之间切换。
算法:
getBorderTiles
的函数获取一个对象(玩家)并返回接触玩家 4 个 Angular 落中的每个 Angular 落的瓷砖。由于玩家不比瓷砖大,那些边界瓷砖必然是玩家接触的唯一瓷砖。请注意,其中一些图块可能相同。例如,如果玩家只占据一列,则左上/右上图块将相同,左下/右下图块也是如此。如果发生这种情况,getBorderTiles
仍然返回所有四个图块,但有些是相同的。 算法失败的原因:
右下 Angular 的瓷砖是实心的,但右上 Angular 不是,所以(第 4 步)玩家向下碰撞,(第 5 步)它被向上推。此外,它与 BR 瓷砖碰撞,但不与 BL 碰撞,因此它向右碰撞并被向左推。最后,玩家被渲染在壁架的正上方和左侧。实际上它被传送了。
尝试解决:我试图解决这个问题,但它只会产生另一个问题。我添加了一个检查,以便玩家仅在该图块内有一定距离(例如 3px)时才与该图块发生碰撞。如果玩家仅在 BR 区块中,则算法不会记录下碰撞,因此玩家不会向上传送。但是,如果玩家在另一种情况下跌倒在地,则直到玩家落入地面很远时,它才会确认碰撞。当它掉到地上时,玩家会颤抖,被推回地面,再次跌倒,等等。
感谢您阅读到这里。我非常感谢您的反馈。
当前算法代码:
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's position since it falls in middle of 4 tiles)
yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object's 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;
}
更新:用马拉卡的解决方案解决。算法如下。基本上它测试(x 然后 y)并解决冲突,然后它测试(y 然后 x)并以这种方式解决冲突。无论哪种测试导致玩家移动更短的距离,最终都会被使用。
有趣的是,当玩家同时在顶部和左侧发生碰撞时,它需要一个特殊情况。也许这与玩家的 (x, y) 坐标在其左上 Angular 有关。在这种情况下,应该使用导致玩家移动更长距离的测试。在这个 gif 中很清楚:
玩家是黑框,黄框代表玩家在使用其他测试(导致玩家移动更远距离的测试)时所处的位置。理想情况下,玩家不应移入墙壁,而应移至黄色框所在的位置。因此,在这种情况下,应该使用更远距离的测试。
这是快速而肮脏的实现。它根本没有优化,但希望它非常清楚地显示了算法的步骤。
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's position since it falls in middle of 4 tiles)
yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object's 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's 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's 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;
}
}
最佳答案
发生这种情况是因为您首先检测两个方向的碰撞,然后调整位置。 “向上/向下”首先更新(重力方向)。首先调整“左/右”只会使问题变得更糟(每次跌倒后,您可能会被向右或向左传送)。
我能想到的唯一快速而肮脏的解决方法(重力不变):
编辑:示例
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.
关于javascript - 碰撞检测不应该使物体传送,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44191923/