Разрешение столкновений AABB скользящие стороны

Итак, в настоящее время я изобретаю колесо (и многому учусь), пробуя свои силы в создании простого физического движка для своего игрового движка. Я искал в Интернете, пытаясь (и безуспешно) исправить мою текущую проблему. На эту тему есть много ресурсов, но ни один из них, похоже, не применим к моему делу.

КРАТКАЯ ПРОБЛЕМА: разрешение столкновений не работает должным образом на некоторых углах, когда два прямоугольника сталкиваются. То, как это не удается, зависит от размеров прямоугольников. Я ищу разрешение «кратчайшего перекрытия» для столкновения или другое довольно простое решение (я открыт для предложений!). (Прокрутите вниз, чтобы найти лучшее объяснение и иллюстрации).

ВНИМАНИЕ! Следующий код, вероятно, не очень эффективен ...

Прежде всего, вот моя физическая петля. Он просто перебирает все игровые объекты и проверяет, не сталкиваются ли они с какими-либо другими игровыми объектами. Это неэффективно (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. Это работает нормально, пока и сущность, и цель являются точными квадратами. Если предположить, что объект (фигура игрока на гифке) представляет собой прямоугольник, то столкновение будет "проскальзывать" при столкновении самых длинных сторон (ось 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 РАБОЧИЙ КОД: - См. мой ответ ниже для примера рабочего кода!


person Tim Eriksen    schedule 12.09.2017    source источник
comment
В качестве общего совета см. этот вопрос на Gamedev.   -  person Etheryte    schedule 12.09.2017
comment
@Nit Спасибо, я совсем недавно занялся разработкой игр, поэтому обязательно добавлю книгу по физике игр в свой список для чтения :)   -  person Tim Eriksen    schedule 12.09.2017
comment
Очень хорошая альтернатива моему. У него меньше веток, поэтому он намного аккуратнее (и, возможно, быстрее - определенно было бы так, если бы он реализован на скомпилированном языке)   -  person meowgoesthedog    schedule 16.09.2017
comment
@meowgoesthedog Спасибо, хотя я не могу поверить в это :) Ваш подход с использованием фактических битовых флагов тоже был интересен, я фактически не видел, чтобы это использовалось раньше. Я подробнее рассмотрю возможные приросты производительности при использовании этого по сравнению с массивами или объектами.   -  person Tim Eriksen    schedule 16.09.2017
comment
Пожалуйста, подумайте о добавлении ответа на свой вопрос, который показывает ваш рабочий код, вместо публикации ответа в вашем вопросе.   -  person Ethan Field    schedule 19.09.2017
comment
@EthanField Хороший звонок, я сделаю это!   -  person Tim Eriksen    schedule 19.09.2017


Ответы (3)


Вы сделали важную логическую ошибку:

if (absoluteNormalizedX > absoluteNormalizedY) {

Это работает, только если оба объекта квадратные.

Рассмотрим почти экстремальный случай для вашего примера проскальзывания X: если они почти касаются угла:

введите здесь описание изображения

Хотя диаграмма немного преувеличена, вы можете видеть, что absoluteNormalizedX < absoluteNormalizedY в этом случае - ваша реализация перейдет к разрешению вертикального столкновения вместо ожидаемого горизонтального.


Другая ошибка заключается в том, что вы всегда устанавливаете соответствующий компонент скорости равным нулю, независимо от того, на какой стороне происходит столкновение: вы должны обнулить компонент только в том случае, если он находится в направлении, противоположном нормали столкновения, иначе вы не сможете отодвинуться. с поверхности.


Хороший способ преодолеть это - также записывать столкнувшиеся лица при обнаружении столкновений:

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

Обратите внимание, что в отличие от вашего исходного кода, приведенный выше также позволяет углам сталкиваться, то есть для определения скоростей и положений по обеим осям.

person meowgoesthedog    schedule 13.09.2017
comment
Спасибо за обстоятельный ответ! Я думаю, мне придется немного подробнее изучить это, поскольку простая реализация (копирование / вставка) кода, который вы опубликовали выше, заставляет игрока всегда сталкиваться с коробкой, пока он находится вверху / внизу / справа / слева от него. Даже если игрок находится далеко от коробки, он телепортируется прямо рядом с ней. После столкновения игрок прилипает к коробке и не может отойти от нее. Если игроки будут продолжать двигаться к коробке на сталкивающейся грани, он будет телепортирован на другую грань на противоположной оси. Имеет ли это для вас смысл? - person Tim Eriksen; 15.09.2017
comment
@TimEriksen да, я должен извиниться - я только что понял очень глупую ошибку, которую сделал с логикой. Я постараюсь исправить это и вернусь к вам. - person meowgoesthedog; 15.09.2017
comment
@TimEriksen Я обновил информацию о возможном исправлении, не могли бы вы дать мне знать? Спасибо - person meowgoesthedog; 15.09.2017
comment
Это, конечно, лучше, но есть проблемы. Я сделал гифку о том, как он себя ведет после ваших последних изменений: media.giphy.com/media /3o7aCRGhbVHDi06kk8/giphy.gif. Пример диагонального столкновения применим только к углам (куда игрок телепортируется после удара влево или вправо. Большое спасибо за вашу помощь! Просто примечание: я не собираюсь делать что-то особенно эффективное здесь , просто то, что помогает мне понять, как работают физические движки :) - person Tim Eriksen; 16.09.2017
comment
@TimEriksen Ого, хорошо, это ничего не решило. Мне придется написать свои собственные тесты для этого, я думаю, лол - person meowgoesthedog; 16.09.2017
comment
Наконец-то! Это работает! Ваше мышление о глубине пересечения привело меня к правильному пути :) Затем я провел еще несколько поисков и нашел примеры того, что мне нужно, в XNA Game Studio Started Kit. Я дополню свой ответ рабочим кодом и ссылками на найденные ресурсы. - person Tim Eriksen; 16.09.2017
comment
@TimEriksen Думаю, я исправил это - он отлично работает в тесте C #, который я сделал. РЕДАКТИРОВАТЬ: вау время - person meowgoesthedog; 16.09.2017
comment
Да, практически одновременно нажмите кнопку «Добавить комментарий» :) Я посмотрю на ваш код и предложу решения, большое вам спасибо за вашу помощь! - person Tim Eriksen; 16.09.2017

МОЙ РАБОЧИЙ КОД

Так что с некоторой помощью и руководством замечательного @meowgoesthedog я наконец встал на правильный путь и нашел то, что искал. Проблема (как указал @meowgoesthedog) заключалась в том, что мой код действительно работал только с квадратами. Решение состояло в том, чтобы проверить пересечение сталкивающихся тел и решить на основе кратчайшего пересечения. Примечание: это, вероятно, не будет подходящим решением, если вам нужна точная физика с небольшими и быстро движущимися объектами. Код для определения глубины пересечения основан на следующем: https://github.com/kg/PlatformerStarter8cd3d3e6e6e6e4e6e6e6e6e6e6e6e6e6e6e6e8e6e4e6e9 этот проект: 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;
    }
  }
},

Спасибо за вашу помощь!

person Tim Eriksen    schedule 19.09.2017
comment
В вашем последнем фрагменте мне кажется, что ваш код ничего не делает, если глубины проникновения по X и Y одинаковы. Вам нужно выбрать ось или сделать какой-то диагональный отклик. Кажется, что вы могли бы пройти сквозь объект, если войдете точно в угол и продолжите движение по диагонали. - person Tara; 17.05.2021

Проблема может заключаться в том, что вы исправляете оба X и Y столкновения на основе одной и той же позиции:

  1. Игрок находится в определенной позиции. Проверим столкновение.
  2. Нижний правый угол игрока перекрывает верхний левый угол объекта.
  3. X исправлено положение: игрок перемещен влево.
  4. Нижний правый угол игрока перекрывает верхний левый угол объекта.
  5. Y исправлено положение: игрок перемещен вверх.
  6. Конечный результат: игрок перемещается вверх и влево.

Вероятно, вам нужно снова «получить» позицию игрока между проверками.

person Cerbrus    schedule 12.09.2017
comment
Примечание: это предположение. Возможно, это совершенно неверно, но, надеюсь, это вдохновит вас. - person Cerbrus; 12.09.2017
comment
Я понимаю, что вы имеете в виду, но не думаю, что это применимо к моему случаю, поскольку я корректирую только одну ось каждый раз, когда обнаруживается столкновение. Я считаю, что проблема как-то связана с нормализацией средних точек X Y (но я могу ошибаться и, вероятно, ошибаюсь). - person Tim Eriksen; 12.09.2017