{{notification.text}}

MirGames

В этой статье я постараюсь рассказать о реализации простейшей физики без использования физ. движков (т.е. напишем свой),
здесь будет присутствовать код на Delphi. Большинство описанных здесь методов не являются физически верными, и “разработаны”
(выдуманы), мной за время участия в конкурсе по программированию игр на Delphi.

Для начала договоримся о том, что мир будет представлен картой высот(heightmap), а автомобиль BBox’ом (точнее 8-ю векторами, но об этом позже) и 4-я колесами.

Как видите на рисунке, синие точки (вершины BBox’а) расположены не по параллелепипеду, а по углам модели автомобиля. Т.е. не образуют правильного BBox’а, но нам это и не надо. Это может показаться странным, но проверятся будут именно точки, а не грани, так гораздо проще и быстрее, Конечно, будут “провалы” в ландшафт, но для начала неплохо бы и так сделать. Красным обозначены точки приложения силы двигателя и трения колес. Если хотите, можете взять любое другое кол-во вершин.

Гравитация всегда действует на центр масс, расположенный в нашем случае в (0,0,0 – локальные координаты) Все эти вектора (позиции вершин Box’а, колёс) надо хранить как в локальных, так и в глобальных координатах, что понадобится для трения. Для хранения поворота будет матрица 3x3, а угловую скорость запишем в 3D вектор, линейная позиция и скорость также вектора. Ещё понадобится масса, угол поворота рулевых колёс, обороты или сила двигателя (здесь всё очень упрощено), массивы координат вершин BBox’а и позиций колёс. Для понятности объявим структуру автомобиля.


PPhObject = ^TPhObject;
TPhObject = record
  Mass:Single;        // Масса
   gD:Boolean;         // true – двигатель включен
  E_Turns:Single;     // обороты двигателя

...

   rD:Boolean;          // true – никто не рулит, т.е. делаем поворот в нейтральное положение
   rT:Boolean;          // true – тормоз активен
  WheelsAngle:Single;  // угол поворота рулевых колёс
  Wheels:array[0..3] of TBWheel;   // структура колеса, их 4 будет
  BBox:array[0..7] of TVector;     // Б-Box
   abs_BBox,p_abs_BBox:array[0..7] of TVector;  // об этом потом, это для трения
   V,Va:TVector;    // скорость линейная, угловая
  M:TRMatrix;       // матрица вращения 3x3
  Pos:TVector;      // позиция объекта
   Next:PPhObject;  // не придумал ни чего лучше списка, но это не важно
 end;

и структуру колеса

TBWheel = record
  R:Single;        // Радиус колеса
  Rz:Single;       // Расстояние до ландшафта
  P:TVector;        // позиция отн. центра игрока (в локальных координатах)
  abs_P:TVector;    // абс. позиция
  p_abs_P:TVector;  // пред. абс. позиция
  Angle:Single;    // угол поворота колеса вокруг своей оси
  VAngle:Single;   // скорость вращения колеса
 end;

Локальные позиции, конечно же, задаются при инициализации, а абсолютные вычисляются в каждом такте из локальных,
умножением на матрицу M, и прибавлением вектора Pos

Ещё нам понадобятся две функции:

function TrHeight(X,Y:Single):Single;

Возвращает высоту ландшафта в произвольной точке

function TrNormal(X,Y:Single):TVector;

и нормаль.


Как же будет происходить обработка столкновений? Очень просто, в каждом такте физики производится проверка расстояния от каждой вершины BBox’а до ландшафта (тут и нужна функция TrHeight()), и если расстояние меньше радиуса (для вершин у меня 0.1), применяется сила направленная по нормали к земле (TrNormal()) и пропорциональная массе авто и глубине погружения. Для колёс всё делается также, отличие только в том, что от глубины погружения в ландшафт сила зависит более мягко. Напишем несколько функций вычисляющих коэффициент, на который умножается сила. Единственный параметр функции это глубина, а результат коэффициент.

function Solid(F:Single):Single;
begin
 result:=F*F+35;
end;
 
function Spring(F:Single):Single;
begin
 result:=F+25;
end;
 
function Water(F:Single):Single;
begin
 result:=F*0.15;
end;

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

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

procedure ApplyForce(Obj:PPhObject; P,F:TVector; TF:Single);
var
 Angles:TVector;
begin
 // находим углы поворотов вокруг x,y,z за время TF
 Angles:=VCrossProduct(P,F); 
 // изменяем скорость
 Obj^.Va:=VSum(Obj^.Va,VMult(Angles,0.35*TF/Obj^.Mass)); // тут числа также подобраны
 Obj^.V:=VSum(Obj^.V,VMult(F,TF/Obj^.Mass));
 // а это чтоб не прыгать бесконечно, т.е. возвращаемый импульс меньше начального
  Obj^.V.y:=Obj^.V.y*0.998;
  Obj^.Va:=VMult(Obj^.Va,0.995);
end; 

Насчет основного цикла, он будет с фиксированным delta time = 0.005 сек, (так будет намного меньше проблем, чем с произвольным dTime) т.е. цикл должен повторятся 200 раз в секунду, для этого сделаем вот так.

Ph_dC:=Ph_dC+TF;   // Ph_dC – глобальная float переменная
 ICount:=0;   
 if Ph_dC>0.005 then begin   // если прошло более 0.005 секунд
  ICount:=trunc(Ph_dC*200);  // Icount – количество циклов
  PhTF:=0.005;
   Ph_dC:=Ph_dC-PhTF*Icount; // вычитаем из Ph_dC “просчитанное” время
 end;
 
 for j:=1 to ICount do
 begin
  
 end;

Сколько бы раз в секунду не вызывался этот кусок кода, то что внутри цикла for все равно будет выполнено 200 раз/сек. Т.е. и при низком и при высоком FPS результат будет одинаков, что нам и надо. Можете, конечно, сделать и другие значения dTime. Это повлияет на точность расчётов, и нагрузку на CPU

Теперь о abs_BBox,p_abs_BBox:array[0..7] of TVector; всё это нужно для нахождения вектора перемещения вершины в глобальных координатах, этот вектор и есть сила трения. Как только происходит коллизия, применяем и его (трение).

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

Ну а для колёс расчет трения чуть сложнее, зная матрицу поворота автомобиля (и угол поворота колеса, для рулевых), находим вектор, ось вращения, делаем DotProduct оси с вектором перемещения, и домножаем на это вектор оси, получаем силу трения, сопротивляющуюся боковому движению. А у рулевых колёс это трение разворачивает весь авто. При движении назад знак меняется, и разворот происходит в нужную сторону.Ещё надо сказать об ограничении трения на колёсах (т.к. оно у нас нечестно считается), иначе авто перевернётся от абсолютного трения, при повороте на большой скорости.

Вращение колёс вокруг своей оси делается также DotProduct’ом, но умножаем не ось а перпендикуляр к ней направленный по направлению вперёд.

При нахождении перпендикуляра для простоты я отбрасываю вертикальную составляющую вектора, это не сильно повлияет на реалистичность т.к. ландшафт представленный heightmap’ом достаточно плоский, а в 2D найти перпендикуляр сами знаете как.

Применение силы двигателя, тоже не сложно, делаем ApplyForce() к нижним точкам колёс, не забывая проверять, что они касаются земли. Почти то же самое и с тормозом.

Столкновения с другими объектами я сделал очень просто (но ничто не мешает вам всё усложнить), каждый автомобиль это 3 сферы, работает это так: каждый объект проверяет, не въехал ли он во что, и если да, то сам к себе применяет силу пропорциональную массе другого, никак не трогая остальных. Трение не учитывается.

Ну, вот и всё, осталось получить новую матрицу поворота из угловых скоростей, тут проще показать код:

procedure ResultMatrix(var P:TRMatrix;Angles:TVector;TF:Single);
begin
 P:=MRMultiply(P,MRGetRotationY(Angles.y*TF));
 P:=MRMultiply(P,MRGetRotationX(Angles.x*TF));
 P:=MRMultiply(P,MRGetRotationZ(Angles.z*TF));
 // чтоб не разъехалась
 MRNormalize(P); // ортогонализация и нормализация
end;

почему именно такой порядок перемножения матриц, он, в общем, то и не важен (можете проверить), т.к. углы поворотов очень малы.

И ещё кое-что:

v:=VDotProduct(cur^.V,cur^.V); // квадрат скорости
 if v<1 then begin
  cur^.V.x:=cur^.V.x*0.995;
  cur^.V.y:=cur^.V.y*0.95;
  cur^.V.z:=cur^.V.z*0.995;
  cur^.Va:=VMult(cur^.Va,0.98);
 end;

Ссылки по теме

http://www.gamedev.ru/articles/?id=70124

http://www.gamedev.ru/articles/?id=70108

Пример
Пример (103k)
Исходники (OpenGL)(127k)
31.10.05 05:05