AABB碰撞解决方法中滑动的侧面问题

7
所以,我正在重新发明轮子(并学到了很多),尝试制作一个简单的物理引擎用于我的游戏引擎。我一直在搜索互联网,尝试修复目前的问题(但不成功)。有很多相关资源可供使用,但是我找到的所有内容似乎都不能适用于我的情况。
简而言之,当两个矩形相撞时,碰撞解决方法在某些角落处无法正常运作。失败的方式根据矩形的尺寸而异。我正在寻找一种“最短重叠”类型的碰撞解决方案或另一种相当简单的解决方案(欢迎提出建议!)。下面会有更好的解释和插图。
首先,这是我的物理循环。它仅循环所有游戏实体并检查它们是否与任何其他游戏实体碰撞。虽然效率不高(n^2之类的),但现在可以使用。
updatePhysics: function(step) {
  // Loop through entities and update positions based on velocities
  for (var entityID in Vroom.entityList) {
    var entity = Vroom.entityList[entityID];
    if (entity.physicsEnabled) {
      switch (entity.entityType) {
        case VroomEntity.KINEMATIC:
          entity.pos.x += entity.vel.x * step;
          entity.pos.y += entity.vel.y * step;
          break;

        case VroomEntity.DYNAMIC:
          // Dynamic stuff
          break;
      }
    }
  }
  // Loop through entities and detect collisions. Resolve collisions as they are detected.
  for (var entityID in Vroom.entityList) {
    var entity = Vroom.entityList[entityID];
    if (entity.physicsEnabled && entity.entityType !== VroomEntity.STATIC) {
      for (var targetID in Vroom.entityList) {
        if (targetID !== entityID) {
          var target = Vroom.entityList[targetID];
          if (target.physicsEnabled) {
            // Check if current entity and target is colliding
            if (Vroom.collideEntity(entity, target)) {
              switch (entity.collisionType) {
                case VroomEntity.DISPLACE:
                  Vroom.resolveTestTest(entity, target);
                  break;
              }
            }
          }
        }
      }
    }
  }
},

这是实际碰撞检测的代码。这个代码似乎也可以正常工作。

collideEntity: function(entity, target) {
  if (entity.getBottom() < target.getTop() || entity.getTop() > target.getBottom() ||  entity.getRight() < target.getLeft() ||  entity.getLeft() > target.getRight()) {
    return false;
  }

  return true;
},

这里是问题开始出现的地方。我希望实体能够被简单地“推”出目标实体,并将速度设为0。只要实体和目标都是完美的正方形,这样做就没问题。但如果实体(gif中的玩家角色)是一个矩形,那么在与目标(正方形)碰撞时,碰撞将会在最长的边(X轴)上“滑动”。如果我交换玩家的尺寸,使其变得短而宽,则同样的问题会出现在Y轴上。
resolveTestTest: function(entity, target) {
  var normalizedX = (target.getMidX() - entity.getMidX());
  var normalizedY = (target.getMidY() - entity.getMidY());
  var absoluteNormalizedX = Math.abs(normalizedX);
  var absoluteNormalizedY = Math.abs(normalizedY);

  console.log(absoluteNormalizedX, absoluteNormalizedY);

  // The collision is comming from the left or right
  if (absoluteNormalizedX > absoluteNormalizedY) {
    if (normalizedX < 0) {
      entity.pos.x = target.getRight();
    } else {
      entity.pos.x = target.getLeft() - entity.dim.width;
    }

    // Set velocity to 0
    entity.vel.x = 0;

    // The collision is comming from the top or bottom
  } else {
    if (normalizedY < 0) {
      entity.pos.y = target.getBottom();
    } else {
      entity.pos.y = target.getTop() - entity.dim.height;
    }

    // Set velocity to 0
    entity.vel.y = 0;
  }

},

这些形状在Y轴上的碰撞有效 GIF

这些形状在X轴上的碰撞会滑动 GIF

我应该如何解决这个滑动问题?我已经为此头痛了5天,如果有人能帮我指点迷津,我将不胜感激!

谢谢 :)

-- 编辑:--

如果只沿左侧或右侧移动,也会发生滑动。

GIF

-- 编辑2 工作代码:-- 请参见我的答案,其中包括一个工作示例!


2
作为一般性建议,请参考Gamedev上的这个问题 - Etheryte
@Nit 谢谢,我最近刚开始学习游戏开发,所以我一定会把一本关于游戏物理的书加入到我的阅读清单中 :) - Tim Eriksen
非常好的替代方案。它的分支较少,因此更整洁(如果在编译语言中实现,则可能更快)。 - meowgoesthedog
@meowgoesthedog 谢谢,虽然我真的不能为此负责 :) 你使用实际位标志的方法也很有趣,我以前从未见过这种方法。我会进一步研究使用这种方法与使用数组或对象相比可能带来的性能提升。 - Tim Eriksen
1
请考虑添加一个回答,展示您的工作代码,而不是在问题中发布答案。 - Ethan Field
@EthanField 好主意,我会这么做的! - Tim Eriksen
3个回答

4
您犯了一个重要的逻辑错误,就在于这一行代码: if (absoluteNormalizedX > absoluteNormalizedY) { 如果两个实体都是正方形,那么这个代码是有效的。但考虑到您关于X滑动的示例几乎达到了极限情况:如果它们几乎在角落处接触,则绝对规范化的X值小于绝对规范化的Y值 - 您的实现将继续解决垂直碰撞而不是水平碰撞。
另一个错误在于无论碰撞发生在哪一侧,您总是将相应的速度分量归零:只有在速度分量与碰撞法线相反方向时,您才能将其归零,否则无法从表面移开。
解决这个问题的好方法是在进行碰撞检测时也记录碰撞面。
collideEntity: function(entity, target) {
   // adjust this parameter to your liking
   var eps = 1e-3;

   // no collision
   var coll_X = entity.getRight() > target.getLeft() && entity.getLeft() < target.getRight();
   var coll_Y = entity.getBottom() > target.getTop() && entity.getTop() < target.getBottom();
   if (!(coll_X && coll_Y)) return 0;

   // calculate bias flag in each direction
   var bias_X = entity.targetX() < target.getMidX();
   var bias_Y = entity.targetY() < target.getMidY();

   // calculate penetration depths in each direction
   var pen_X = bias_X ? (entity.getRight() - target.getLeft())
                      : (target.getRight() - entity.getLeft());
   var pen_Y = bias_Y ? (entity.getBottom() - target.getUp())
                      : (target.getBottom() - entity.getUp());
   var diff = pen_X - pen_Y;

   // X penetration greater
   if (diff > eps)
      return (1 << (bias_Y ? 0 : 1));

   // Y pentration greater
   else if (diff < -eps) 
      return (1 << (bias_X ? 2 : 3));

   // both penetrations are approximately equal -> treat as corner collision
   else
      return (1 << (bias_Y ? 0 : 1)) | (1 << (bias_X ? 2 : 3));
},

updatePhysics: function(step) {
   // ...
            // pass collision flag to resolver function
            var result = Vroom.collideEntity(entity, target);
            if (result > 0) {
              switch (entity.collisionType) {
                case VroomEntity.DISPLACE:
                  Vroom.resolveTestTest(entity, target, result);
                  break;
              }
            }
   // ...
}

为了提高效率,使用位标记而不是布尔数组。然后可以重新编写解析器函数:

resolveTestTest: function(entity, target, flags) {
  if (!!(flags & (1 << 0))) {  // collision with upper surface
      entity.pos.y = target.getTop() - entity.dim.height;
      if (entity.vel.y > 0)  // travelling downwards
         entity.vel.y = 0;
  } 
  else
  if (!!(flags & (1 << 1))) {  // collision with lower surface
      entity.pos.y = target.getBottom();
      if (entity.vel.y < 0)  // travelling upwards
         entity.vel.y = 0;
  }

  if (!!(flags & (1 << 2))) {  // collision with left surface
      entity.pos.x = target.getLeft() - entity.dim.width;
      if (entity.vel.x > 0)  // travelling rightwards
         entity.vel.x = 0;
  } 
  else
  if (!!(flags & (1 << 3))) {  // collision with right surface
      entity.pos.x = target.getRight();
      if (entity.vel.x < 0)  // travelling leftwards
         entity.vel.x = 0;
  }
},

请注意,与您原始的代码不同,上述代码还允许发生碰撞 - 即解决速度和位置沿两个轴的问题。

谢谢你提供详细的答案!我认为我需要再深入研究一下,因为简单地实现(复制/粘贴)你上面发布的代码会导致玩家只要在箱子的上/下/左/右侧就会与之碰撞。即使玩家远离箱子,他也会立即传送到它旁边。碰撞后,玩家会黏在箱子上,无法移开。如果玩家继续向碰撞面移动,他将被传送到相反轴上的另一个面。这对你有意义吗? - Tim Eriksen
@TimEriksen 是的,我必须道歉 - 我刚意识到我在逻辑上犯了一个非常愚蠢的错误。我会尝试修复这个问题并回复你。 - meowgoesthedog
@TimEriksen,我已经更新了可能的修复方案,请您尝试并让我知道结果,谢谢。 - meowgoesthedog
终于!它能工作了!你对交叉点深度的思考让我找到了正确的方向 :) 然后我进行了更多的搜索,在XNA游戏工作室入门套件中找到了我需要的示例。我将更新我的答案,附上可工作的代码和我找到的资源参考。 - Tim Eriksen
1
是的,实际上我们几乎同时点击了“添加评论”按钮 :) 我会查看您的代码并比较解决方案,非常感谢您的帮助! - Tim Eriksen
显示剩余3条评论

4

我的工作代码

在@meowgoesthedog的帮助和指导下,我终于找到了想要的东西并走上了正确的轨道。问题(正如@meowgoesthedog所指出的)在于我的代码只适用于正方形。解决方法是检查碰撞物体的相交部分,并基于最短相交来解决。注意:如果需要使用小型和快速移动的物体进行精确物理计算,则这可能不是一个合适的解决方案。 查找相交深度的代码基于此:https://github.com/kg/PlatformerStarterKit/blob/0e2fafb8dbc845279fe4116c37b6f2cdd3e636d6/RectangleExtensions.cs,该项目与此相关:https://msdn.microsoft.com/en-us/library/dd254916(v=xnagamestudio.31).aspx

以下是我的工作代码:

我的物理循环并没有改变太多,只是一些函数名称更加清晰。

updatePhysics: function(step) {
  // Loop through entities and update positions based on velocities
  for (var entityID in Vroom.entityList) {
    var entity = Vroom.entityList[entityID];
    if (entity.physicsEnabled) {
      switch (entity.entityType) {
        case VroomEntity.KINEMATIC:
          entity.pos.x += entity.vel.x * step;
          entity.pos.y += entity.vel.y * step;
          break;

        case VroomEntity.DYNAMIC:
          // Dynamic stuff
          break;
      }
    }
  }
  // Loop through entities and detect collisions. Resolve collisions as they are detected.
  for (var entityID in Vroom.entityList) {
    var entity = Vroom.entityList[entityID];
    if (entity.physicsEnabled && entity.entityType !== VroomEntity.STATIC) {
      for (var targetID in Vroom.entityList) {
        if (targetID !== entityID) {
          var target = Vroom.entityList[targetID];
          if (target.physicsEnabled) {
            // Check if current entity and target is colliding
            if (Vroom.collideEntity(entity, target)) {
              switch (entity.collisionType) {
                case VroomEntity.DISPLACE:
                  Vroom.resolveDisplace(entity, target);
                  break;
              }
            }
          }
        }
      }
    }
  }
},

碰撞检测仍然保持不变。
collideEntity: function(entity, target) {
  if (entity.getBottom() < target.getTop() || entity.getTop() > target.getBottom() ||  entity.getRight() < target.getLeft() ||  entity.getLeft() > target.getRight()) {
    return false;
  }

  return true;
},

这是基本解决问题的代码。代码中的注释应该能够很好地解释它的功能。

getIntersectionDepth: function(entity, target) {
  // Calculate current and minimum-non-intersecting distances between centers.
  var distanceX = entity.getMidX() - target.getMidX();
  var distanceY = entity.getMidY() - target.getMidY();
  var minDistanceX = entity.halfDim.width + target.halfDim.width;
  var minDistanceY = entity.halfDim.height + target.halfDim.height;

  // If we are not intersecting at all, return 0.
  if (Math.abs(distanceX) >= minDistanceX || Math.abs(distanceY) >= minDistanceY) {
    return {
      x: 0,
      y: 0,
    };
  }

  // Calculate and return intersection depths.
  var depthX = distanceX > 0 ? minDistanceX - distanceX : -minDistanceX - distanceX;
  var depthY = distanceY > 0 ? minDistanceY - distanceY : -minDistanceY - distanceY;

  return {
    x: depthX,
    y: depthY,
  };
},

这是更新后的解决函数。现在在确定碰撞轴时考虑交集深度,然后在确定解决方向时使用碰撞轴的交集深度符号。

resolveDisplace: function(entity, target) {
  var intersection = Vroom.getIntersectionDepth(entity, target);
  if (intersection.x !== 0 && intersection.y !== 0) {
    if (Math.abs(intersection.x) < Math.abs(intersection.y)) {
      // Collision on the X axis
      if (Math.sign(intersection.x) < 0) {
        // Collision on entity right
        entity.pos.x = target.getLeft() - entity.dim.width;
      } else {
        // Collision on entity left
        entity.pos.x = target.getRight();
      }

      entity.vel.x = 0;
    } else if (Math.abs(intersection.x) > Math.abs(intersection.y)) {
      // Collision on the Y axis
      if (Math.sign(intersection.y) < 0) {
        // Collision on entity bottom
        entity.pos.y = target.getTop() - entity.dim.height;
      } else {
        // Collision on entity top
        entity.pos.y = target.getBottom();
      }

      entity.vel.y = 0;
    }
  }
},

非常感谢大家的帮助!


在你上一段代码中,如果沿X和Y方向的穿透深度相同,那么代码似乎是无效的。你需要选择一个坐标轴或者进行某种对角线响应。如果你恰好在一个角落进入并斜向移动,似乎可以通过物体。 - Tara

1
问题可能在于你正在基于同一位置来修正 X Y 的碰撞:

  1. 玩家处于某个位置。让我们检查碰撞。
  2. 玩家的右下角重叠了对象的左上角。
  3. X位置被纠正:玩家向左移动。
  4. 玩家的右下角重叠了对象的左上角。
  5. Y位置被纠正:玩家向上移动。
  6. 最终结果:玩家向上和向左移动。

你可能需要在检查之间再次“获取”玩家的位置。


注意:这只是一个猜测。可能完全错误,但希望能给你一些灵感。 - Cerbrus
我明白你的意思,但我不认为这适用于我的情况,因为每次检测到碰撞时,我只会修正单个轴。我相信问题与X Y中点的归一化有关(但我可能是错误的)。 - Tim Eriksen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接