Есть ли способ повысить эффективность проверки столкновений системы из n объектов?

9

Я делаю игру, которая состоит из множества экранных объектов, одним из которых является игрок. Мне нужно знать, какие объекты сталкиваются на каждой итерации.

Я сделал что-то вроде этого:

for (o in objects)
{
   o.stuff();
   for (other in objects)
      if (collision(o, other))
          doStuff();

   bla.draw();
}

Это имеет O (n ^ 2), что мне говорят, это плохо. Как мне сделать это более эффективно, возможно ли это? Я делаю это в Javascript, и n обычно будет меньше 30, это будет проблемой, если это останется прежним?

jcora
источник
3
Вы пытались запустить код, чтобы увидеть, как он работает?
Thedaian
Нет, нет, просто я предполагаю, что это плохо из-за O (n ^ 2).
Jcora
1
Всего 30 предметов? Я бы порекомендовал пространственное разделение, но было бы бесполезно только с 30 объектами. Есть некоторые незначительные оптимизации, на которые указывали другие, но все они - незначительные оптимизации в масштабе, о котором вы говорите.
Джон Макдональд

Ответы:

16

Если максимум 30 объектов, вам вообще не нужно много оптимизировать, кроме того, чтобы не проверять одни и те же две пары друг против друга более одного раза за кадр. Какой пример кода ниже будет охватывать. Но если вам интересны различные оптимизации, которые будет использовать физический движок, продолжайте читать оставшуюся часть этого поста.

Вам потребуется реализация пространственного разбиения , такая как Octree (для 3D-игр) или Quadtree (для 2D-игр). Они делят мир на подразделы, а затем каждый подраздел разделяется далее в той же усадьбе, пока они не будут разделены до минимального размера. Это позволяет очень быстро проверить, какие другие объекты находятся в том же регионе мира, что и другой, что ограничивает количество столкновений, с которыми вы должны проверять.

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

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

Большинство физических движков деактивируют активное физическое моделирование на физических телах, как только они придут в себя. Когда физическое тело деактивировано, ему нужно только проверять наличие столкновений с его AABB в каждом кадре, и если что-то сталкивается с AABB, то оно затем активируется и выполняет более детальную проверку столкновений. Это уменьшает время моделирования.

Кроме того, многие физические движки используют «острова симуляции», где группы физических тел, которые находятся близко друг к другу, группируются вместе. Если все на острове моделирования находится в состоянии покоя, то сам остров моделирования отключается. Преимущество острова моделирования состоит в том, что все тела внутри него могут перестать проверять наличие столкновений после того, как остров неактивен, и единственная проверка каждого кадра состоит в том, чтобы увидеть, попало ли что-либо в AABB острова. Только когда что-то входит в AABB острова, каждое тело на острове должно проверять наличие столкновений. Остров симуляции также активируется, если какое-либо тело внутри него снова начинает двигаться самостоятельно. Если тело перемещается достаточно далеко от центра группы, оно удаляется с острова.

В конце концов у вас остается что-то вроде этого (в псевдокоде):

// Go through each leaf node in the octree. This could be more efficient
// by keeping a list of leaf nodes with objects in it.
for ( node in octreeLeafNodes )
{
    // We only need to check for collision if more than one object
    // or island is in the bounds of this octree node.
    if ( node.numAABBsInBounds > 1)
    {
        for ( int i = 0; i < AABBNodes.size(); ++i )
        {
           // Using i+1 here allows us to skip duplicate checks between AABBS
           // e.g (If there are 5 bodies, and i = 0, we only check i against
           //      indexes 1,2,3,4. Once i = 1, we only check i against indexes
           //      2,3,4)
           for ( int j = i + 1; j < AABBNodes.size(); ++j )
           {
               if ( AABBOverlaps( AABBNodes[i], AABBNodes[j] ) )
               {
                   // If the AABB we checked against was a simulation island
                   // then we now check against the nodes in the simulation island

                   // Once you find overlaps between two actual object AABBs
                   // you can now check sub-nodes with each object, if you went
                   // that far in optimizing physics meshes.
               {
           }
        }
    }
}

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

Кроме того, убедитесь, что вы не изменяете контейнер AABBNodes во время его циклического прохождения, поскольку это может означать пропущенные проверки коллизий. Это может звучать как здравый смысл, но вы будете удивлены, насколько легко реагировать на столкновения, вызывая изменения, которых вы не ожидаете. Например, если столкновение привело к тому, что один из сталкивающихся объектов изменил положение настолько, чтобы удалить их из AABB узла Octree, который вы проверяли, он может изменить этот контейнер. Чтобы решить эту проблему, я рекомендую вести список всех событий столкновения, которые происходят во время проверок, а затем, после того как все проверки завершены, просмотрите список и отправьте все события столкновения.

Ник Фостер
источник
4
Очень последовательный ответ с приятной и полезной технической точностью, чтобы открыть читателю разум к существующим методам. +1
Valkea
Что если мне нужно удалить сталкивающийся объект? Могу ли я изменить контейнер? Я имею в виду удаление его из контейнера, поскольку мне больше не нужен объект, потому что он «уничтожен». Мне нужен еще один цикл для прохождения событий столкновения, если я не удаляю его во время обнаружения столкновения.
Новый
Удаление сталкивающегося объекта - это хорошо, но я бы порекомендовал подождать, пока это не будет сделано после того, как проход столкновения будет выполнен в течение всего моделирования. Обычно вы просто помечаете объекты, которые необходимо удалить, или генерируете список объектов, которые нужно удалить, а затем, после того как симуляция столкновений завершена, вы применяете эти изменения.
Ник Фостер
4

Ваш пример тестирует каждую пару объектов несколько раз.

Давайте рассмотрим очень простой пример с массивом, содержащим 0,1,2,3

С вашим кодом вы получите это:

  • В цикле 0 вы тестируете против 1, 2 и 3
  • В цикле 1 вы тестируете по 0, 2 и 3 ===> (0-1 уже протестировано)
  • В цикле 2 вы тестируете по 0, 1 и 3 ===> (0-2 / 1-2 уже протестировано)
  • В цикле 3 вы тестируете по 0, 1 и 2 ===> (0-3 / 1-3 / 2-3 уже протестировано)

Теперь давайте посмотрим на следующий код:

for(i=0;i<=objects.length;i++)
{
    objects[i].stuff();

    for(j=i+1;j<=objects.length;j++)
    {
        if (collision(objects[i], objects[j]))
        doStuff();
    }

    bla.draw();
}

Если мы снова используем массив, содержащий 0,1,2,3, у нас будет следующее поведение:

  • В цикле 0 вы тестируете против 1, 2, 3
  • В цикле 1 вы тестируете против 2, 3
  • В цикле 2 вы тестируете против 3
  • В цикле 3 вы проверяете ничего

Со вторым алгоритмом мы получили 6 тестов столкновений, в то время как предыдущий запросил 12 тестов столкновений.

Valkea
источник
Этот алгоритм делает N(N-1)/2сравнения, которые по-прежнему O (N ^ 2) производительности.
Кай
1
Ну, с 30 объектами по запросу это означает 465 тестов столкновений с 870 ... это, вероятно, похоже с вашей точки зрения, но не с моей. Кроме того, решение, предлагаемое в другом ответе, является точно таким же алгоритмом :)
Valkea
1
@Valkea: Ну, отчасти это так. :)
Ник Фостер
@NicFoster: да, вы правы;) Я говорил строго о проверке столкновений между выбранными объектами, а не о разделительной части алгоритма, которая, очевидно, является очень ценным дополнением, которое я даже не думал добавлять в своем примере, когда Я писал это.
Valkea
Это называется амортизацией? В любом случае, спасибо!
Jcora
3

Разработайте свой алгоритм в соответствии с вашими потребностями, но сохраняйте детали реализации в инкапсуляции. Даже в Javascript применяются основные концепции ООП.

Ибо N =~ 30, O(N*N)это не проблема, и ваш линейный поиск, скорее всего, будет таким же быстрым, как и любая другая альтернатива. Но вы не хотите жестко кодировать предположения в своем коде. В псевдокоде у вас будет интерфейс

interface itemContainer { 
    add(BoundingBox);
    remove(BoundingBox);
    BoundingBox[] getIntersections();
}

Это описывает, что может сделать ваш список элементов. Затем вы можете написать класс ArrayContainer, который реализует этот интерфейс. В Javascript код будет выглядеть так:

function ArrayContainer() { ... } // this uses an array to store my objects
ArrayContainer.prototype.add = function(box) { ... };
ArrayContainer.prototype.remove = function(box) { ... };
ArrayContainer.prototype.getIntersections = function() { ... };

function QuadTreeContainer { ... } // this uses a quadtree to store my objects
... and implement in the add/remove/getIntersections for QuadTreeContainer too

И вот пример кода, который создает 300 ограничивающих рамок и получает все пересечения. Если вы реализовали ArrayContainer и QuadTreeContainer правильно, единственное , что вам нужно будет изменить в коде изменения var allMyObjects = new ArrayContainer()в var allMyObjects = QuadTreeContainer().

var r = Math.random;
var allMyObjects = new ArrayContainer();
for(var i=0; i<300; i++)
    allMyObjects.add(new BoundingBox(r(), r()));
var intersections = allMyObjects.getIntersections();

Я пошел дальше и подхватил реализацию стандартного ArrayContainer здесь:

http://jsfiddle.net/SKkN5/1/

Джимми
источник
Примечание. Этот ответ был мотивирован жалобой Бэйна на то, что его кодовая база становилась слишком большой, грязной и сложной для управления. Хотя это мало что добавляет к обсуждению использования Array vs Tree, я надеюсь, что это уместный ответ о том, как конкретно он мог бы улучшить организацию своего кода.
Джимми
2

Вы должны также рассмотреть типы объектов, которые могут разумно сталкиваться.

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

Для эффективной реализации этого вы, вероятно, хотите хранить отдельные списки объектов, по одному для каждого типа объекта.

Адам
источник