javascript - 碰撞检测不应该使物体传送

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

已解决,最终算法见帖子底部

背景:我正在使用 JS 和 HTML Canvas 元素开发 2D 平台游戏。关卡 map 是基于图块的,但玩家不会被夹在图块上。我正在使用 "Tiny Platformer" on Code inComplete 中概述的碰撞检测算法.除了一种边缘情况(或“壁架”情况)外,它在很大程度上有效。

问题:

gif of ledge issue

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

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

算法:

  • 假设没有碰撞,计算玩家的新位置。 (以下代码中未显示)
  • 一个名为 getBorderTiles 的函数获取一个对象(玩家)并返回接触玩家 4 个 Angular 落中的每个 Angular 落的瓷砖。由于玩家不比瓷砖大,那些边界瓷砖必然是玩家接触的唯一瓷砖。请注意,其中一些图块可能相同。例如,如果玩家只占据一列,则左上/右上图块将相同,左下/右下图块也是如此。如果发生这种情况,getBorderTiles仍然返回所有四个图块,但有些是相同的。
  • 它会检查关卡 map (一个 2D 数组)中的这些边界图块以查看它们是否是实心的。如果瓷砖是实心的,则对象正在与该瓷砖发生碰撞。
  • 它测试上/下/左/右碰撞。如果玩家向下移动并与向下的瓷砖发生碰撞,但没有与相应的向上的瓷砖发生碰撞,则玩家正在向下碰撞。如果玩家向左移动并与左侧瓷砖发生碰撞,但没有与相应的右侧瓷砖发生碰撞,则它是在向左碰撞。等等。在左/右检查之前执行上/下检查。如果在执行左/右检查之前存在上/下碰撞,则将调整存储边界图块的变量。例如,如果玩家向下碰撞,它将被插入向上的瓷砖,因此 BL/BR 瓷砖现在与 TL/TR 瓷砖相同。
  • 玩家的 x、y 和速度根据它碰撞的方向进行调整。

  • 算法失败的原因:

    See this image.

    右下 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 中很清楚:

    gif showing why special case is needed

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

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

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

    最佳答案

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

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

  • 计算两个相关点在一个方向上的碰撞(例如,当向左行驶时,只有左边的两个点很重要)。然后在那个方向调整速度和位置。
  • 计算两个(调整后的)相关点在另一个方向上的碰撞。调整碰撞方向的位置和速度。
  • 如果步骤 1 中没有冲突,那么您可以保留更改并返回。否则计算与步骤 1 之前的原始位置相比的距离 dx + dy。
  • 重复步骤 1. 到 3. 但这次你先从另一个方向开始。
  • 以较小的距离进行更改(除非您已经在第 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.
    

    关于javascript - 碰撞检测不应该使物体传送,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44191923/

    相关文章:

    javascript - 调整大小时的高度检查会导致滞后

    javascript - 如何获取一个数在矩阵中的位置?

    java - 如何制作一个包含另一个数组的随机元素对的二维数组?

    c - 二维数组地址簿存在 fgets 问题

    javascript - Promise中间件

    javascript - Spotify web-api 暂停事件监听器

    javascript - react : pass ref through?

    algorithm - 热图的聚类地理数据

    algorithm - 什么数据结构或算法用于自动完成?

    c++ - 如何提取二维矩阵C++同一条对角线上的单元格索引