Phys2D: физика и java

April 10, 2008

Эта статья является логическим продолжением ранее опубликованной серии посвященной 2d-физике для flash-разработчиков. Сегодня я продолжу рассказ о методах “плоской” физической симуляции, вот только язык программирования будет java. Сравнивать эти две платформы (java и flash) практически не возможно: отличаются и технические средства языка и цели, которые ставят перед собой идеологи java и flash. Основная сфера применения java – это “большие” корпоративные приложения, базы данных, сервера приложений, работающие с множеством клиентов. Flash первоначально позиционировался на рынке как средство для создания “блестящих” баннеров. Как только в массовом сознании сложилась мысль, что в internet можно размещать не только html-страницы, но интерактивные приложения, то adobe решила перепозиционировать flash, “обозвала” его платформой для разработки RIA-приложений, выпустила flex, air. Очевидно, что и flash и java имеют достаточное количество общих сфер применения, например, игры. Конечно, на рынке гораздо большее количество игр (я говорю про небольшие игрушки) написанных на flash, чем на java (с большими играми ситуация в точности противоположная). В любом случае, для того чтобы некоторая технология завоевала внимание разработчиков необходимо достаточное количество прикладных библиотек и учебных пособий. По мере своих скромных сил я исправляю эту ситуацию и рассказываю об интересных, но не “раскрученных” библиотеках и подходах в программировании. Сегодня время помочь всем, кто хочет сделать на java небольшую игру (или серьезное приложение) активно использующую “настоящую” физику.

Библиотека phys2d (домашний сайт www.cokeandcode.com) идеально подходит для желающих создать java-приложение выполняющее симуляции “плоских” объектов. Код библиотеки ориентирован на java 1.4 (выпущена года эдак 4-е назад), а значит, что для его работы клиентам, наверняка, не нужно будет обновлять версию среды исполнения java. Библиотека регулярно обновляется, и последняя версия датирована концом 2007 г (в svn репозитории на google code есть и более свежая версия). После того как вы загрузили библиотеку и подключили ее к вашей IDE, самое время обратить внимание на идущие в комплекте примеры. Два с лишним десятка примеров покажут вам как работать со всеми возможностями phys2d. Каждый класс примера является наследником от класса AbstractDemo, внутри которого создается каркас приложения и содержится код отрисовки моделируемого мирка. Что бы не занимать место и не копировать чужой код, я буду все свои примеры создавать как расширения класса AbstractDemo (однако перед этим я советую просмотреть его исходники).

Итак, “сердце” симуляции с помощью phys2d это класс World. Когда мы его создаем, то нужно указать как параметры конструктора направление действующей силы гравитации (она задается с помощью вектора Vector2f), затем количество итераций выполняемых для расчета каждого шага симуляции (чем больше это число, тем точнее симуляция, но и выше затраты ресурсов), и последний параметр – стратегия первичного обнаружения столкновений. QuadSpaceStrategy предполагает, что пространство будет рекурсивно разбиваться на четыре подобласти до тех пор, пока в каждом из сегментов не останется не более 20 тел, или будет выполнено не более 5-и разбиений (см. параметры конструктора). Кроме QuadSpaceStrategy стратегии есть еще BruteCollisionStrategy, которая предполагает выполнение прямого перебора всех пар и проверку их на коллизию (но считайте, что про это “достижение” phys2d я ничего не говорил). Важно: отрисовка физической модели целиком на вашей совести: никаких средств для визуализации в состав phys2d не входит. С другой стороны в примерах показано как выполнить отрисовку объектов “мирка” в виде контуров (в классе AbstractDemo обратите внимание на метод draw). Давайте создадим “мир”:
  1. protected World world = new World(new Vector2f(0.0f, 10.0f), 10, new QuadSpaceStrategy(20,5));
После создания world-а необходимо наполнить его содержимым. Здесь вы будете пользоваться двумя методами: “add (Body b)” и “add(Joint j)”. Первый из них служит для добавления тел, второй – для добавления “сочленений”. Разберем эти два типа данных подробнее: класс Body является базовым для всех физических тел. Только одно перечисление методов класса будет способно растянуться на несколько страниц. Однако, основные из них я упомяну:
Метод Описание
addForce Метод служит для добавления силы действующей на объект (суммируя новую и старую силу).
setForce Устанавливает значение силы примененной к объекту, замещая старую.
getMass Получает значение массы объекта и устанавливает его (вместо массы можно использовать специальное значение Body.INFINITE_MASS, которое означает, что тело обладает бесконечной массой). Здесь и далее: если есть метод getX, то должен быть и метод setX для установки значения параметра тела.
getI Получить величину инерции тела.
getRotation Получить значение величины вращения тела.
getTorque Получить значение вращательного момента.
getFriction Получить значение коэффициента трения (характеристика материала, из которого изготовлен объект).
getConnected Получить список объектов, с которым наш объект соединен.
Надо сказать, что для многих из параметров (координаты, линейная и угловая скорость) есть два значения – текущее и то, которое было на предыдущем шаге расчета симуляции. Например, для координат объекта это getPosition и getLastPosition. Также для многих параметров есть не только методы вида getX и setX (получающие и устанавливающие новое значение некоторого параметра), но и методы вида adjustX – их назначение добавить к параметру некоторую поправку, например, adjustPosition (delta) – добавляют к координатам объекта значение delta. Для определения того, находится ли тело в состоянии покоя, используйте метод isResting. В общем случае наши тела находятся не в вакууме и на них действуют сила трения с воздухом, конкретное значение этого коэффициента мы можем задать с помощью методов setDamping и setRotDamping (второй из них задает силу трения, которая действует на объект в ходе его вращения).

Очень приятно, что к каждому из “тел” можно присоединить пользовательский объект с помощью метода setUserData (например, к модели “машинка” мы можем присоединить объект, хранящий сведения о количестве оставшихся жизней или топлива). Также у объекта есть специальные флажки, управляющие тем, будет ли ему позволено перемещаться и вращаться под действием других тел (по умолчанию они установлены в true), но можно и запретить такое поведение с помощью методов (setRotatable и setMoveable). Также можно исключить данный объект из списка тех тел, на которые действует гравитация (мы задавали ее для всей сцены при создании объекта World). Для этого используйте метод setGravityEffected (также воздействию гравитации не подвержены те тела, масса которых бесконечна). Phys2d позволяет настроить списки объектов, которые не могут сталкиваться (они будут проходить сквозь друг друга как приведения). Для этого используйте два метода addExcludedBody и removeExcludedBody. Одним словом параметров очень много, но вот только я нигде не упомянул о форме этого тела. Body – это что? Body – это обертка для еще одного объекта: Shape (форма тела). Т.е. когда мы создаем Body, мы тут же должны создать еще один объект производный от класса AbstractShape (или поддерживающий интерфейс Shape), и эту ссылку на “фигуру” нужно передать в конструктор “тела”, например, так:
  1. Body body2 = new Body("Body2", ФИГУРА, 100.0f);
В этом примере первый параметр для Body – строка с именем объекта, затем идет форма объекта и, наконец, его масса (если имя объекта для вас не существенно, то вы его можете не указывать: есть и такой вариант конструктора). Разновидностью Body является StaticBody. Это статическое тело, т.е. тело не способное ни к перемещению под действием сил, ни к вращению (описанные выше флажки rotatable и moveable равны false). Т.к. тело статическое, то его масса полагается равной бесконечности и конструктору не передается.
  1. StaticBody wall_1 = new StaticBody("Box1", new Box(100,100));
Есть следующие разновидности форм тел:

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

Circle – Для создания круга нужно передать как единственный параметр конструктору значение радиуса.

Polygon – не совсем обычный полигон. Для его создания необходимо передать конструктору список вершин. Необычность полигона в том, что (в отличие от реализации полигона в FOAM) он может не быть выпуклым. Надо сказать, что есть и другая разновидность полигона требующая, чтобы он был выпуклым (ConvexPolygon).

Line – Для создания линии мы передаем конструктору координаты точки.

И тут самое время задуматься, а позвольте спросить, где указывается центр этих фигур? Или, например, Line, чтобы ее задать, нам нужны две точки, но ведь конструктор Line принимает только одну координату, где вторая? Центр координат фигур это не часть Shape, а часть Body. После создания Body мы выполним его перемещение в некоторую точку, например, так:
  1. Body body2 = new Body("Body2", new Box(100.0f, 100.0f), 100.0f);
  2. body2.setPosition(200.0f, 200.0f);
Теперь я приведу пример с двумя наклонными плоскостями, на которые падают несколько кубиков. Коэффициенты трения для поверхностей будут различаться, так чтобы скорость соскальзывания тел была бы различной. Также я изменил силу гравитации и сопротивление воздуха. Обратите внимание на то, что объекты “стены” созданы в виде StaticBody и поэтому “висят” в воздухе, а не падают вниз как кубики.
  1.  создаем первую стену
  2. StaticBody wall_1 = new StaticBody(new Box(200,5));
  3.  
  4.  позиционируем ее
  5. wall_1.setPosition(100, 300);
  6.  
  7.  вращаем фигуру
  8. wall_1.setRotation((float)(Math.PI / 4));
  9.  
  10.  устанавливаем силу трения
  11. wall_1.setFriction (1f);
  12.  
  13.  и добавляем ее в "мир"
  14. world.add (wall_1);
  15.  
  16.  аналогично и для второй стены
  17. StaticBody wall_2  =new StaticBody(new Box(200,5));
  18. wall_2.setPosition(300, 300);
  19. wall_2.setRotation(-(float)(Math.PI / 4));
  20.  
  21.  упавшие на эту стену кубики будут плавно “соскальзывать вниз”
  22. wall_2.setFriction(.0f);
  23. world.add (wall_2);
  24.  
  25. for (int i = 0; i < 5; i++){
  26.     создаем динамическое "тело" в виде падающего кубика
  27.    Body body = new Body(new Box (15, 15), 5);
  28.    body.setPosition((float)(100 + 300 * Math.random()),50);
  29.    world.add (body);
  30. }
  31.  
  32.  увеличим силу трения воздуха
  33. world.setDamping(1f);
  34.  
  35.  и изменим силу гравитации, действующую на все динамические объекты
  36. world.setGravity(0,1);
Все: приведенный выше пример полностью работоспособен, и результат будет выглядеть так, как показано на рис.1.



Вы можете спросить, а где скрывается код запускающий симуляцию? Ведь когда я рассказывал про FOAM, то использовал методы simulate и stop для запуска и остановки симуляции? Не забывайте, что наш класс примера был наследован от AbstractDemo, внутри которого и реализован главный цикл приложения вызывающий метод step (сделать шаг симуляции). Также нам по наследству досталась обработка нескольких клавиш: “R” – выполняет сброс симуляции, а клавиша “C” добавляет к рисуемым контурам фигур еще и отображение точек контакта и нормалей к ним.

Помимо объектов есть еще и сочленения, соединяющие между собой фигуры. Все сочленения должны поддерживать интерфейс Joint. По сравнению с FOAM и его “куцыми” ограничениями (Spring и Bungee) Phys2d просто великолепен. Самым простым видом сочленения является “жесткое” (FixedJoint). При создании FixedJoint нужно просто указать два тела, которые мы хотим соединить. В примере ниже обратите внимание на то, что только один из кругов является статическим, второй – нет и должен падать под действием силы тяжести. Однако он останется неподвижным т.к. жестко “приколочен” к первому кругу.
  1.  создаем два круга
  2. Body ball1 = new StaticBody(new Circle(20));
  3. ball1.setPosition(350, 100);
  4. world.add (ball1);
  5.  
  6. Body ball2 = new Body(new Circle(20), 1);
  7. ball2.setPosition(450, 100);
  8. world.add (ball2);
  9.  
  10.  и создаем соединение между телами
  11. Joint bj_1 = new FixedJoint(ball2, ball1);
  12. world.add(bj_1);
Следующий вид сочленения - BasicJoint – это ограничение похоже на шарнир. Например, далее я создам “кривенькие” качельки, состоящие из двух досок соединенных с помощью BasicJoint.
  1.  создаем потолок
  2. StaticBody ceil=new StaticBody(new Box (400, 5));
  3. ceil.setPosition(250, 50);
  4.  
  5.  создаем скамейку
  6. Body bench =new Body(new Box (200, 5), 10);
  7. bench.setPosition(250, 300);
  8.  
  9.  добавляем эти два объекта на сцену
  10. world.add (bench);
  11. world.add (ceil);
  12.  
  13.  и создаем соединение между телами
  14. BasicJoint bj_1 = new BasicJoint(bench, ceil, new Vector2f(200,200));
  15. world.add(bj_1);
При создании BasicJoint указываются два тела, которые необходимо соединить (соединение исходит из центра фигуры), затем указываем точку, через которую идет соединение. На рис. 2 я кружком отметил “якорь”, через который проходит соединение между телами.



Пригодится и такой вид соединения как DistanceJoint, он служит для задания жесткой шарнирной связи между двумя телами. Например, следующий код, создает два круга, один из которых является статическим. Второй же расположен на шарнире сбоку от первого и как только симуляция будет запущена, то второй круг начнет вращаться как на “качелях” вокруг первого круга, не удаляясь и не приближаясь (храня дистанцию между кругами равной 100).
  1.  создаем два круга
  2. Body ball1 = new StaticBody(new Circle(20));
  3. ball1.setPosition(350, 100);
  4. world.add (ball1);
  5.  
  6. Body ball2 = new Body(new Circle(20), 1);
  7. ball2.setPosition(450, 100);
  8. world.add (ball2);
  9.  
  10.  и создаем соединение между телами
  11. DistanceJoint bj_1 = new DistanceJoint(ball1, ball2, new Vector2f(0,10), new Vector2f(0,10), 100);
  12. world.add(bj_1);
Еще один вид сочленения – это SpringJoint (обычная “резинка”, которая соединяет два тела между собой). При создании SpringJoint, нужно указать не только те два тела, которые мы хотим соединить, но и точки к которым “крепится” резинка (координаты должны быть заданы в глобальной системе координат и в примере я упростил, соединяя между собой центры фигур). В следующем примере я создаю потолок в виде статического прямоугольника, к которому с помощью резинки присоединил один кружок, а к нему в свою очередь еще 9-ь кружков соединенных попарно.
  1.  создаем потолок
  2. Body ceil=new StaticBody(new Box (400, 5));
  3. ceil.setPosition(250, 50);
  4. world.add (ceil);
  5.  
  6. Body [] ball = new Body[10];
  7.  
  8.  создаем массив из десятки кружков расположенных в форме эллипса
  9. for (int i= 0; i < ball.length; i++){
  10.  ball[i] = new Body(new Circle(10),1);
  11.  ball[i].setPosition(
  12.     (float)(200 + 50 * Math.cos(Math.PI * i/ 5)), 
  13.     (float)(200 + 100 * Math.sin(Math.PI * i/ 5))
  14.  );
  15.  world.add(ball[i]);
  16. }
  17.  теперь создаем попарные связи между кружками
  18. for (int i= 0; i < ball.length - 1; i++){
  19.   world.add (new SpringJoint(ball[i], ball[1+i], ball[i].getPosition(), ball[1+i].getPosition())); 
  20. }
  21.  
  22.  последняя связь между одним из кругов и “потолком”
  23. world.add(new SpringJoint(ball[ball.length-1], ceil, 
  24.           ball[ball.length-1].getPosition(), ceil.getPosition()));
Для того чтобы ограничить перемещение двух тел друг относительно друга мы можем использовать и такое соединение как AngleJoint. При его создании мы указываем два соединяемых тела, затем их точки и, наконец, два числа означающие пределы углов (например, от 0 до PI) в которых эти тела могут перемещаться. Похож на AngleJoint и такой вид соединения как SlideJoint, только вместо ограничения по углам между телами, здесь используется ограничение по расстоянию между ними (например, тела, точнее их точки привязки, должны быть в отрезке от 10 до 100). И есть еще несколько других ограничений. Помните, что часть ограничений визуально проявятся только тогда, когда они используются не по отдельности, а совместно. Например, какой смысл в AngleJoint, если между телами нет другой, более “осязаемой” связи (добавьте, для примера, к связи AngleJoint еще и ограничение по расстоянию между телами DistanceJoint).

Гораздо интереснее посмотреть, а какие средства для анализа происходящей на сцене ситуации есть в составе phys2d? Можно ли спросить, какие объекты столкнулись между собой, столкнулась ли, например, модель машинки со столбом или нет? Такие средства есть, хотя и не очень удобные. Возвратимся к рассмотрению устройства класса AbstractDemo: в главном цикле симуляции после расчете физики и отрисовки фигур вызывается метод update, внутри которого и следует выполнить обращение к “мирку” с вопросом: “дай-ка мне список всех контактов для вот этого тела”. В примере ниже я создал “пол” в виде статического прямоугольника и бросил на него ряд мячиков. Затем внутри метода update я получаю массив коллизий шариков с “полом” и вывожу их на экран.
  1. protected void update() {
  2.   super.update();
  3.   CollisionEvent[] colls = world.getContacts(floor);
  4.   if (colls.length > 0){
  5.     System.out.println("was collision: ");
  6.     for (CollisionEvent col : colls) {
  7.        System.out.println("--"+ col.getBodyA() + " * "+ col.getBodyB());
  8.     } 
  9.   } 
  10. }
На этом все. Помните, что помимо широко распространенных “трехмерных” физических движков есть и их младшие братья, работающие с 2d-объектами. И при должных усилиях вы можете создавать очень интересные “плоские” игры (бильярд, арканоид, пинг-понг).

Примеры роликов показывающих возможности phys2d