【问题标题】:Collision detection shouldn't make object teleport up碰撞检测不应该使物体瞬移
【发布时间】:2017-05-26 00:51:41
【问题描述】:

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

背景:我正在使用 JS 和 HTML 画布元素开发 2D 平台游戏。关卡地图是基于瓷砖的,但玩家并没有被固定在瓷砖上。我正在使用"Tiny Platformer" on Code inComplete 中概述的碰撞检测算法。除了一种边缘情况(或“壁架”情况)外,它大部分都有效。

问题:

玩家正在摔倒并向右移动,撞到墙上。当它下落时,它会传送到壁架的高度。相反,玩家应该正常下落而不进行传送。

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

算法:

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

  2. 一个名为getBorderTiles 的函数接受一个对象(玩家)并返回接触玩家四个角的瓷砖。由于玩家不比牌大,那些边界牌必然是玩家接触的唯一牌。请注意,其中一些图块可能相同。例如,如果玩家只占据一列,则左上/右上图块将相同,左下/右下图块也将相同。如果发生这种情况,getBorderTiles 仍会返回所有四个图块,但有些会是相同的。

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

  4. 它测试上/下/左/右碰撞。如果玩家向下移动并与向下的瓷砖碰撞,但没有与相应的向上瓷砖碰撞,则玩家正在向下碰撞。如果玩家向左移动并与左侧瓷砖碰撞但未与相应的右侧瓷砖碰撞,则它正在向左碰撞。等等。在左/右检查之前执行上/下检查。如果在执行左/右检查之前存在上/下碰撞,则调整存储边界图块的变量。例如,如果玩家向下碰撞,它将被推入向上的瓷砖,因此 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 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


【解决方案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.

【讨论】:

  • 您好,感谢您的回复!不幸的是,当我遵循您的解决方案时,仍然会发生相同的行为。也许我误解了?我在第一篇文章中添加了我的实现。如果没有,我可能会一起切换到不同的算法。
  • 是的,您似乎误会了。您只是将支票分开,但没有遵循上述算法。基本上你必须复制原始位置两次,然后先纠正一个 x 方向,然后先纠正另一个 y 方向(并调整!)。根据这两个新位置,您检查两个位置的另一个方向(并调整)。现在你计算两种可能性到原始的距离,并取更好的一个(在你的例子中,一次玩家会稍微向左移动,另一次他向上移动更大的距离,所以它应该可以工作)
  • @myohmywhoami 添加了示例。
  • 糟糕!感谢您写出步骤。我现在似乎已经让算法工作了,我在原始帖子中写了一个快速而肮脏的版本。有趣的是,它需要一种特殊情况:当用户在左上角发生碰撞时(参见原始帖子)。在这种特殊情况下,应该使用更长的测试。我想知道这是否是因为玩家的 (x, y) 在其左上角?无论如何,它工作得很好,非常感谢你帮助我!
猜你喜欢
  • 2013-01-28
  • 1970-01-01
  • 2015-02-10
  • 2010-11-19
  • 1970-01-01
  • 1970-01-01
  • 2014-04-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多