Как-то раз я решил написать игру. Для себя и в своё удовольствие. Передо мной стоял выбор – использовать всё готовенькое ака Box2D или написать для неё физику самому. Второй вариант показался мне более интересным, и я принялся выискивать в просторах сети информацию, которая помогла бы мне написать всё необходимое. Выискал. Как результат получился очень гибкий(как для игры) и простой физический движок. Основой движка стал метод численного интегрирования Верле.




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


Объединив 2 уравнения, мы получим


Уравнение Верле мало чем отличается от записи выше.


Как видно, в методе Верле скорость точки выражается через разницу между её текущей и старой координатой. Отлично, нам меньше мороки. Точность вычислений от этого падает, но мы и не атомные реакторы рассчитываем. Плюс это делает расчёт столкновений до смешного простым. Рассмотрим движение точки на примере:


Мы видим, что текущая и старая координаты точки выбраны таким образом, что она неотвратимо движется вправо навстречу с синим квадратом. Заглядывая немного вперёд, скажу, что при обнаружении столкновения с квадратом для обработки этого столкновения достаточно вывести текущую координату точки за пределы квадрата. Всё. Так как скорость тут выражается через разницу координат, изменение текущей или старой координаты автоматически приводит и к изменению скорости движения точки.


Если облечь всё это в код на С++, то получится что-то такое

struct Point
{
  Vec2 Position;
  Vec2 OldPosition;
  Vec2 Acceleration;
};

class Physics 
{
  int PointCount;
  Point* Points[ MAX_VERTICES ];
  float Timestep;
public:
  void  UpdateVerlet();
};

void Physics::UpdateVerlet() 
{
  for( int I = 0; I < PointCount; I++ ) 
 {
    Point& P = *Points[ I ];

    Vec2 Temp = P.Position;
    P.Position += P.Position - P.OldPosition + P.Acceleration*Timestep*Timestep;
    P.OldPosition = Temp;
  }
}

Этого достаточно для описания движения материальных точек. И это хорошо, если у вас в игре планируются только точки. Если же хочется чего-то более интересного, надо делать твёрдые тела. В природе твёрдое тело это много точек-атомов, соединённых между собой мистическими силами природы. Увы, для симуляции физики в игре подобный метод подходит плохо, ни один компьютер не сможет выдержать расчётов миллиардов и миллиардов точек и связей между ними. Поэтому мы обойдёмся моделированием вершин физических тел. Для квадрата таким образом будет достаточно 4 точек и связей между ними. Осталось только описать эти связи. Очевидно, что расстояние между вершинами должно оставаться постоянным. Если расстояние между вершинами будет меняться, тело будет деформироваться, а мы этого не хотим (те, кто этого хотят, пока молчат). Как же сохранить расстояние между вершинами постоянным? В реальности для решения этой задачи достаточно было бы засунуть между вершинами арматурину соответствующей длинны. Не мудрствуя лукаво, поступим так же и в нашей симуляции. Создадим класс, который будет описывать эту воображаемую арматурину, удерживающую вершины на необходимом расстоянии. Алгоритм, выполняющий эту работу, будет вызываться сразу же после обработки движения точек-вершин. В сути своей он очень простой. Мы расчитываем вектор между двумя вершинами, соединёнными нашей воображаемой арматуриной и сравниваем длину этого вектора с нашей эталонной длинной. Если длины отличаются, нам остаётся только сдвинуть\раздвинуть вершины на необходимое расстояние, что бы длины снова совпадали и вуаля – дело в шляпе.
Вот так это всё выглядит в коде:

struct Edge
{
  Vertex* V1;
  Vertex* V2;
  float OriginalLength; 
};

void Physics::UpdateEdges() 
{
  for( int I = 0; I < EdgeCount; I++ ) 
 {
    Edge& E = *Edges[ I ];
    //Расчёт вектора между вершинами
    Vec2 V1V2 = E.V2->Position - E.V1->Position; 
    float V1V2Length = V1V2.Length(); 
    //Расчёт разницы в длине
    float Diff       = V1V2Length - E.OriginalLength; 
    V1V2.Normalize();
    //Корректировка расстояния
    E.V1->Position += V1V2*Diff*0.5f; 
    E.V2->Position -= V1V2*Diff*0.5f;
  }
}

Вот так вот. Если создать несколько вершин и соединить их между собой такими вот сторонами, результирующее тело будет демонстрировать свойства твёрдого тела, в том числе вращение, вызываемое столкновением с другими телами и полом. Как это всё работает? Ведь мы всего-то добавили новое правило для сохранения расстояния между точками, а тут на – сразу твёрдое тело. Секрет в том самом методе интегрирования Верле. Как мы помним, этот метод не оперирует скоростями, вместо этого в нём используется разница между координатам текущего и старого положения точки. Как следствие, от изменения координаты изменится и скорость. А для сохранения постоянной длинны между вершинами мы как раз и меняем их координаты. Итоговое изменение скорости вершин даёт эффект, приближённо напоминающий поведение твёрдых тел. Точнее почти твёрдых. В том виде, в котором код находится сейчас, тела будут деформироваться при ударах о пол и друг об друга. Почему? Всё очень просто. Если вершина присоединена более чем к одной стороне, а так обычно и будет происходить, корректировка длины одной из сторон автоматически приведёт к изменению длины другой стороны.


Единственным методом лечения этой болезни является множественны�� вызов функции, корректирующей длины сторон. Чем больше раз её вызвать, тем точнее будет аппроксимация. Сколько раз это делать зависит только от вашей фантазии. На этом первая часть статьи заканчивается. В следующей части будут рассмотрены такие моменты как обнаружение и обработка столкновений между телами а также некоторые финты ушами, которые можно проделать при помощи этого метода.