碰撞检测不应该使对象传送出去

myo*_*ami 5 javascript algorithm 2d collision-detection game-physics

解决了,查看最后算法的帖子底部

背景:我正在使用JS和HTML canvas元素处理2D平台游戏.关卡图是基于图块的,但播放器没有夹在图块上.我在Code inComplete中使用了"Tiny Platformer"中概述的碰撞检测算法.它主要适用于一个边缘情况(或''ledge''情况).

问题:

壁架问题的GIF

玩家正在倒下并向右移动进入墙壁.当它下降时,它会传送到壁架的高度.相反,玩家应该在没有传送的情况下正常下降.

有没有办法改变算法来防止这种行为?如果没有,你能建议一个备用的碰撞检测算法吗?理想情况下,任何修复都不会假设玩家总是摔倒,因为在游戏中玩家的跌倒方向在上/下/左/右之间切换.

算法:

  1. 假设没有碰撞,计算玩家的新位置.(未在下面的代码中显示)

  2. 一个被调用的函数getBorderTiles接受一个对象(玩家)并返回触及每个玩家4个角落的图块.由于玩家不大于瓷砖,因此这些边框瓷砖必然是玩家正在触摸的唯一瓷砖.请注意,其中一些图块可能相同.例如,如果玩家仅占据一列,则左上/右上方的区块将与左下方/右下方区块相同.如果发生这种情况,getBorderTiles仍会返回所有四个图块,但有些图块会相同.

  3. 它会检查水平贴图(2D阵列)中的这些边框切片,看它们是否是实心的.如果图块是实心的,则该对象与该图块碰撞.

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

  5. 玩家的x,y和速度将根据其碰撞的方向进行调整.

算法失败的原因:

看到这个图像.

右下方的瓷砖是实心的,但是右上方没有,所以(步骤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;
      }
Run Code Online (Sandbox Code Playgroud)

更新:解决了maraca的解决方案.算法如下.基本上它测试(x然后是y)并解决了碰撞,然后测试(y然后是x)并以这种方式解决碰撞.无论哪个测试结果在播放器中移动较短的距离,最终都会被使用.

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

mar*_*aca 1

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

我能想到的唯一快速而肮脏的解决方案(引力不变):

  1. 计算两个相关点在一个方向上的碰撞(例如,向左行驶时,仅左侧两点重要)。然后调整该方向的速度和位置。

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

  3. 如果步骤 1 中没有发生冲突,那么您可以保留更改并返回。否则计算与步骤 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.
Run Code Online (Sandbox Code Playgroud)