碰撞检测不应该让物体瞬移起来

Collision detection shouldn't make object teleport up

已解决,最终算法见post底部

背景: 我正在使用 JS 和 HTML canvas 元素开发 2D 平台游戏。关卡地图是基于瓦片的,但玩家不会被固定在瓦片上。我正在使用 "Tiny Platformer" on Code inComplete 中概述的碰撞检测算法。除了一种边缘情况(或“'ledge'”情况)外,它在很大程度上起作用。

问题:

玩家摔倒了,同时向右移动,撞到了墙上。当它掉落时,它会传送到窗台的高度。相反,玩家应该在没有传送的情况下正常掉落。

有什么方法可以改变算法来防止这种行为吗?如果不是,你能建议一个替代的碰撞检测算法吗? 理想情况下,任何修复都不会假设玩家总是跌倒,因为在游戏中玩家的跌落方向在 up/down/left/right.

之间切换

算法:

  1. 玩家的新位置是在没有碰撞的情况下计算出来的。 (以下代码中未显示)

  2. 一个名为 getBorderTiles 的函数获取一个对象(玩家)和 returns 接触玩家 4 个角的方块。由于玩家不比一个方块大,那些边界方块必然是玩家接触的唯一方块。请注意,其中一些图块可能是相同的。例如,如果玩家只占据一列,则 left-top/right-top 个方块将相同,left-bottom/right-bottom 个方块也相同。如果发生这种情况,getBorderTiles 仍然 returns 所有四个磁贴,但有些会相同。

  3. 它会检查关卡地图(二维数组)中的这些边界瓦片,看它们是否是实心的。如果瓷砖是实心的,则对象正在与该瓷砖发生碰撞。

  4. 测试up/down/left/right碰撞。如果玩家向下移动并与向下的瓦片碰撞但没有与相应的向上瓦片碰撞,则玩家正在向下碰撞。如果玩家向左移动并与左侧方块碰撞但未与相应的右侧方块发生碰撞,则它向左碰撞。等等 Up/down 检查在 left/right 检查之前执行。如果在执行 left/right 检查之前发生 up/down 碰撞,则存储边界图块的变量将被调整。例如,如果玩家向下碰撞,它将被推入向上的方块,因此 BL/BR 方块现在与 TL/TR 方块相同。

  5. 玩家的 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 然后 y)并解决冲突,然后它测试(y 然后 x)并以这种方式解决冲突。玩家移动距离较短的测试结果就是最终被使用的测试。

有趣的是,当玩家在顶部和左侧方向发生碰撞时,它需要一个特殊情况。也许这与玩家的 (x, y) 坐标位于其左上角这一事实有关。在这种情况下,应该使用导致玩家移动更长距离的测试。这个动图很清楚:

玩家是黑框,黄色框代表玩家在使用其他测试(导致玩家移动更远距离的测试)时的位置。理想情况下,玩家不应移动到墙内,而应该移动到黄色框所在的位置。因此在这种情况下应该使用更远距离的测试。

这是快速而肮脏的实施。它根本没有优化,但希望它能非常清楚地显示算法的步骤。

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;
  }
}

发生这种情况是因为您首先检测到两个方向的碰撞,然后调整位置。 "up/down" 首先更新(重力方向)。首先调整 "left/right" 只会使问题变得更糟(每次跌倒后,您可能会向右或向左传送)。

我能想出的唯一快速但肮脏的修复方法(引力不变):

  1. 计算两个相关点在一个方向上的碰撞(例如,当向左走时,只有左边的两个点很重要)。然后调整那个方向的速度和位置

  2. 计算两个(调整后的)相关点在另一个方向的碰撞。调整碰撞时该方向的位置和速度。

  3. 如果在步骤 1 中没有发生碰撞,那么您可以保留更改并 return。否则计算与步骤1之前的原始位置相比的距离dx + dy。

  4. 重复步骤 1 到 3。但这次你先从另一个方向开始。

  5. 用较小的距离进行更改(除非您已经在第 3 步中找到了好的更改)。

编辑:示例

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.