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

时间:2017-05-26 00:51:42

标签: javascript algorithm 2d collision-detection game-physics

已解决,请参阅帖子底部的最终算法

后台:我正在使用JS和HTML canvas元素处理2D平台游戏。关卡图是基于图块的,但播放器没有夹在图块上。我正在使用"Tiny Platformer" on Code inComplete中概述的碰撞检测算法。除了一个边缘情况(或者''''案例)之外,它在很大程度上有效。

问题:

gif of ledge issue

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

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

算法:

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

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

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

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

  5. 根据玩家碰撞的方向调整玩家的x,y和速度。

  6. 为什么算法失败:

    See this image.

    右下方的瓷砖是实心的,但是右上角是不是,所以(步骤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;
          }
    &#13;
    &#13;
    &#13;

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

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

    gif showing why special case is needed

    玩家是黑匣子,黄色方框表示如果玩家使用了其他测试(导致玩家移动更长距离的测试),玩家的位置。理想情况下,玩家不应该进入墙壁,而应该是黄色框所在的位置。因此,在这种情况下,应该使用长距离测试。

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

    &#13;
    &#13;
    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;
      }
    }
    &#13;
    &#13;
    &#13;

1 个答案:

答案 0 :(得分:1)

这是因为您首先检测两个方向上的碰撞,然后调整位置。 &#34;向上/向下&#34;首先更新(重力方向)。调整&#34;左/右&#34;首先只会使问题变得更糟(每次跌倒后你可能会被左右传送)。

我能想到的唯一快速而肮脏的修复(引力不变):

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

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

  3. 如果步骤1中没有冲突,则可以保留更改并返回。否则,计算距离dx + dy与步骤1之前的原始位置的距离。

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

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

  6. 编辑:示例

    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.