【发布时间】:2017-05-26 00:51:41
【问题描述】:
已解决,最终算法见帖子底部
背景:我正在使用 JS 和 HTML 画布元素开发 2D 平台游戏。关卡地图是基于瓷砖的,但玩家并没有被固定在瓷砖上。我正在使用"Tiny Platformer" on Code inComplete 中概述的碰撞检测算法。除了一种边缘情况(或“壁架”情况)外,它大部分都有效。
问题:
玩家正在摔倒并向右移动,撞到墙上。当它下落时,它会传送到壁架的高度。相反,玩家应该正常下落而不进行传送。
有没有办法改变算法来防止这种行为?如果不是,您能否建议一种替代的碰撞检测算法?理想情况下,任何修复都不会假设玩家总是摔倒,因为在游戏中玩家的摔倒方向会在上/下/左/右之间切换。
算法:
玩家的新位置是在假设没有碰撞的情况下计算的。 (以下代码中未显示)
一个名为
getBorderTiles的函数接受一个对象(玩家)并返回接触玩家四个角的瓷砖。由于玩家不比牌大,那些边界牌必然是玩家接触的唯一牌。请注意,其中一些图块可能相同。例如,如果玩家只占据一列,则左上/右上图块将相同,左下/右下图块也将相同。如果发生这种情况,getBorderTiles仍会返回所有四个图块,但有些会是相同的。它会检查关卡地图(二维数组)中的这些边界图块,看看它们是否是实心的。如果图块是实心的,则对象正在与该图块发生碰撞。
它测试上/下/左/右碰撞。如果玩家向下移动并与向下的瓷砖碰撞,但没有与相应的向上瓷砖碰撞,则玩家正在向下碰撞。如果玩家向左移动并与左侧瓷砖碰撞但未与相应的右侧瓷砖碰撞,则它正在向左碰撞。等等。在左/右检查之前执行上/下检查。如果在执行左/右检查之前存在上/下碰撞,则调整存储边界图块的变量。例如,如果玩家向下碰撞,它将被推入向上的瓷砖,因此 BL/BR 瓷砖现在与 TL/TR 瓷砖相同。
玩家的 x、y 和速度会根据其碰撞方向进行调整。
算法失败的原因:
右下角是实心的,但右上角不是,所以(第 4 步)玩家向下碰撞,(第 5 步)它被向上推。此外,它与 BR 瓦片碰撞但不与 BL 碰撞,因此它向右碰撞并被向左推。到最后,玩家将被渲染到窗台的上方和左侧。实际上它被传送了。
尝试解决方案:我试图解决这个问题,但它只会产生另一个问题。我添加了一个检查,以便玩家仅在该图块内有一定距离(例如 3 像素)时才与该图块发生碰撞。如果玩家只是勉强在 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;
}
更新: 使用 maraca 的解决方案解决。算法如下。基本上它测试 (x then y) 并解决冲突,然后它测试 (y then x) 并以这种方式解决冲突。无论哪种测试结果让玩家移动更短的距离,最终都会被使用。
有趣的是,当玩家在顶部和左侧方向发生碰撞时,它需要一个特殊情况。也许这与玩家的 (x, y) 坐标位于其左上角这一事实有关。在这种情况下,应该使用导致玩家移动更长距离的测试。在这个 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;
}
}
【问题讨论】:
-
请注意,在瓷砖高度的下半部分也会发生向下的传送。如果您在示例中着陆的瓷砖丢失,问题将更加严重,那么您将被向下传送到半空中的位置,速度为 0,然后向左移动一点,之后重力将我猜又是一遍。
标签: javascript algorithm 2d collision-detection game-physics