FOAM: физика и actionscript 3. Часть 2

April 2, 2008

Сегодня я завершу рассказ об FOAM. FOAM - это один из лучших физических движков доступных для flash-разработчиков. В прошлый раз я рассказал об идее легшей в основу FOAM (четкое разделение ответственности за расчеты между несколькими частями движка), об иерархии интерактивных объектов (которые мы можем добавить на сцену). Сегодня осталось только рассмотреть методику добавления к объектам, воздействующих на них сил и ограничений. Также интересен вопрос о создании renderer-ов, (пользовательских объектов позволяющих управлять тем, как “голый” физический объект можно визуализировать).

В прошлой статье я рассказал об двух основных объектах-примитивах, из которых будет строиться сцена: RigidBody и Circle. Помимо них есть еще третий объект CubicBezierCurve (кривая Безье). Однако практического смысла в этом объекте нет, т.к. фактически кривая строится из нескольких линий сегментов, каждый из которых представляется в виде RigidBody (так что, правильнее было бы назвать ее не кривой, а “ломаной”). Если вы еще не сталкивались с кривыми Безье во flash, то я рекомендую посмотреть библиотечку bezier.ru/blog/. Автор FOAM пишет, что в ближайшее время он перепишет код объекта CubicBezierCurve, но (как это часто бывает для проектов на “голом” энтузиазме), скорее всего, это будет еще не скоро. Так что строить свои “физические мирки” вам придется только из кругов и полигонов. Не забывайте также, что полигоны должны быть выпуклыми, в противном случае, в ходе симуляции возникнут забавные эффекты. Например, когда я создал круг, падающий на полигон в виде фантика конфеты, то столкновение произошло гораздо раньше, чем надо (см. рис. 1).



Однако больших трудностей в построении моделей ограничение на форму полигона не имеет т.к. почти любую форму можно составить нескольких “правильных” полигонов. Для построения сложной модели гораздо важнее наличие средств задать связь между объектами. Посмотрим, что есть в копилке FOAM? Средства связи в FOAM построены как расширения генераторов силы. Фактически, если вы связываете два объекта между собой, то это значит, что когда некоторая сила воздействует на первый объект, то она посредством связи передает часть себя и воздействует на второй объект. О генераторах силы (классы поддерживающие интерфейс IForceGenerator) я говорил в прошлой статье, однако напомню, что на физические объекты в FOAM можно оказать воздействие двумя способами: либо посредством силы (у каждого объекта есть метод addForce или addForceAtPoint) либо с помощью генератора силы (addForceGenerator). Отличие этих воздействий в их продолжительности: addForce разово применяет силу к объекту (например, взрыв), в то время как addForceGenerator присоединяет к объекту силу, действующую на протяжении некоторого времени. Использование ForceGenerator является предпочтительным т.к. позволяет гибко задать алгоритм изменения силы во времени. Вы можете создать и собственный генератор силы, если стандартные вас не устраивают. Для этого нужно создать класс, реализующий интерфейс IForceGenerator и определить в нем метод:
  1. public function generate( element:ISimulatable ) : void
Как видите, в качестве входного параметра генератору передается объект, к которому нужно применить силу. Вы рассчитываете величину силы и воздействуете на объект с помощью метода объекта addForce или addTorque.

В копилке FOAM есть четыре класса реализующих интерфейс IForceGenerator.

SimpleForceGenerator – самый простой генератор силы, оказывающий одинаковое по величине воздействие на объект. Величина силы задается как параметр конструктора.

Gravity – этот генератор также получает в качестве параметра конструктора значение применяемой к объекту силы, но ее воздействие на объект возрастает с увеличением массы объекта.

Friction – имитирует силу трения. При создании генератора задается величина коэффициента трения, затем сила рассчитывается по формуле “- скорость * массу * коэффициент”. Для тел (IBody) добавляется и вращательный момент.

KeyDrivenTorqueGenerator - этот генератор применяет к объекту вращательный момент в зависимости от того, какая кнопка на клавиатуре была нажата. По умолчанию, направление вращения задается как влево и вправо, но это можно изменить.

В примере вместе c библиотекой FOAM идет еще один генератор GravitationalForceGenerator. В качестве параметра конструктора для него, задается некоторый объект, играющий роль “центра притяжения”. Когда вы применяете этот генератор к произвольному объекту (один генератор может “обслуживать” любое количество объектов), то он начинает притягиваться к центру притяжения с учетом массы объектов и расстояния между ними: F = G * m1 * m2 / r^2.

Применить генератор силы можно не только к одному или нескольким объектам на сцене, но и к самому объекту FOAM. Тогда при добавлении объекта на сцену к нему автоматически будет присоединен и генератор. Что касается тех объектов, которые уже есть на сцене, то будет ли присоединен к ним генератор, зависит от значения второго параметра (по умолчанию true) метода addGlobalForceGenerator.
  1.  добавляем на сцену круг
  2. foam.addElement (new Circle (100, 100, 25, 20));
  3.  
  4.  генератор силы должен быть применен к новым объектам
  5. foam.addGlobalForceGenerator (new SimpleForceGenerator (new Vector (1, 1)), false);
  6.  
  7.  например, к вот этому кругу
  8. foam.addElement (new Circle (200, 100, 25, 20));
  9. var circ : Circle = new Circle (300, 100, 25, 20);
  10.  применим к объекту персональный генератор силы
  11. circ.addForceGenerator (new SimpleForceGenerator (new Vector(0,-1)));
  12. foam.addElement(circ);
Естественно, что генератор силы можно не только применить к объекту (сцене), но и отозвать с помощью метода removeForceGenerator и removeGlobalForceGenerator.

Теперь перейдем к ограничениям (связям между объектами). Они представлены классами Spring, Bungee и RigidBodySpring, RigidBodyBungee. “Резинка” (Spring) представляет собой однонаправленную (это важно) связь между двумя телами (их центрами). Например, в следующем примере я создам два круга и связь (Spring) между ними, затем первый круг начнет перемещение по сцене вниз. Как только это произойдет, то начнет движение и второй круг.
  1.  создаем два круга
  2. var circ1 : Circle = new Circle (100, 100, 25, 20);
  3. foam.addElement(circ1);
  4. var circ2 : Circle = new Circle (300, 100, 25, 20);
  5. foam.addElement(circ2);
  6.  
  7.  и связь между ними
  8. var spring1 : Spring  = new Spring  (circ2, circ1);
  9.  
  10.  теперь заставим первый круг перемещаться
  11. сirc1.addForce (new Vector (0, 20));
Как работает Spring? Как только я создал объект “резинки”, то ко второму кругу (в списке параметров конструктора он первый) добавился генератор силы (заметьте, что нет никакого кода, который бы добавлял созданную “резинку” на сцену foam). Расстояние между двумя объектами на момент создания “резинки” запоминается и используется при расчете силы действующей на второй объект. Как только первый объект начинает двигаться, резинка начинает растягиваться, что влечет за собой перемещение и второго круга. Важно, что воздействие резинки одностороннее и если перемещаться будет второй объект (circ2), то резинка не приведет в движение первый круг (circ1). “Резинка” характеризуется двумя коэффициентами “степень эластичности” и “коэффициент damp-инга” (суть его в том, что чем больше это значение, тем меньше оказываемое воздействие на второй круг). Эти коэффициенты можно указать как дополнительные параметры конструктора Spring:
  1. new Spring  (circ2, circ1, 0.02, 0.1);
“Резинка” является не визуальным объектом, что не очень приятно на стадии отладки приложения, когда хочется видеть не только объекты, но и факторы оказывающие на них воздействие. В Foam есть механизм позволяющий добавить на сцену “только отображаемый объект”. Суть такого объекта в том, что он исключается из любых физических расчетов выполняемых движком FOAM, но имеет визуальное представление. Следующая строка кода добавит на сцену изображение Spring в виде сплошной красной линии (изменить цвет линии не возможно):
  1. foam.addRenderable (new Renderable (spring1));
Помимо простой “резинки” соединяющей две точки (центры фигур), есть “улучшенная резинка” (RigidBodySpring). Когда мы создаем ее, то указываем не только два соединенных тела, но и точки к которым крепится “резинка”.
  1. var spring1 : RigidBodySpring  = new RigidBodySpring (circ1, new Vector(10, 10), circ2, new Vector( -10, 10));
Второй вид ограничения “амортизатор” является разновидностью “резинки” (а значит параметры, управляющие его работой те же: elasticity, dumping), но сила натяжения будет применена ко второму объекту, только если длина растянутой “резинки” превзойдет начальное расстояние между объектами.
  1. var bungee1 : Bungee  = new Bungee (circ2, circ1, 0.02, 0.1);
  2.  так же как и для резинки для амортизатора есть разновидность RigidBodyBungee
  3. var bungee1 : RigidBodyBungee  = new RigidBodyBungee(circ1, new Vector(10, 10), circ2, new Vector( -10, 10));
Рассказ о Foam я завершу тем, что покажу, как можно выполнить настройку renderer-а и как можно управлять отображением добавленных на сцену объектов. На это влияют два фактора. Прежде всего, для всей сцены может быть назначен специальный объект Renderer. К renderer-у предъявляется только два требования - он должен реализовывать интерфейс IFoamRenderer и он должен быть производным от любого “визуального” класса (например, MovieClip или Sprite). На вход renderer-у Foam подает сведения о координатах всех физических объектов, а он должен их отрисовать на сцене. В случае если я не создал собственный renderer, то по умолчанию Foam рисует объекты в виде их контуров, отдельно обозначая кругом центр координат. Если вы хотите создать собственный renderer (затем его можно присоединить к объекту foam с помощью метода setRenderer), то я настоятельно советую посмотреть исходные коды класса renderer-а по-умолчанию SimpleFoamRenderer. В примере вместе с Foam идет еще два примере renderer-ов: BitmapFoamRenderer и DisplayObjectFoamRenderer. В первом случае отрисовка выполняется на BitmapData (затем эту картинку можно обработать с помощью эффектов или сохранить на сервере в отдельный файл), во втором случае каждому физическому объекту присоединяется MovieClip, который будет показан в тех координатах, где расположен центр физического объекта. Внимание: никакой другой связи между моделью и визуализацией нет. Так что, если вы неверно согласуете центры координат, точки регистрации клипа, размеры или форму, то можете получить ряд забавных ситуаций. В случае, если вы нарисовали клип со сложной формой, то вам нужно создать полигон, повторяющий такой же контур фигуры. Это может быть затруднительно и здесь пригодится функция переключения renderer-ов на лету. Например, можно назначить на клавишу “Q” обработчик события, который будет по очереди переключать renderer-ы с контурного SimpleFoamRenderer на графический DisplayObjectFoamRenderer:
  1. stage.addEventListener (KeyboardEvent.KEY_DOWN, 
  2. function (keyEvt: KeyboardEvent):void{
  3.  if (keyEvt.keyCode == 81){ анализируем клавишу
  4.    if (lastMode) переключаем режим на противоположный
  5.       foam.setRenderer (new DisplayObjectFoamRenderer ());
  6.    else  
  7.       foam.setRenderer( new SimpleFoamRenderer() ); 
  8.    lastMode = ! lastMode;
  9.  } 
  10. } 
  11. );
Создать собственный renderer очень просто и часто необходимо, так для себя я сделал несколько расширений к Foam, позволяющие легче понять, что же происходит на сцене. Например, к каждому из рисуемых объектов можно добавить стрелку, указывающую направление движения объекта с числовыми показателями скорости, его массы.

Для демонстрации работы с renderer-ами создадим две картинки: баскетбольный мяч и корзина, сохранив их с именами ball.png и basket.png. Теперь попробуем создать два Foam объекта круг и прямоугольник, и заменим их графическое представление с помощью этих картинок. Для этого в качестве полей главного класса приложения я объявил графические ресурсы:
  1. [Embed(source = 'ball.png')]
  2. private var BALL_SPRITE_CLASS : Class;
  3.  
  4. [Embed(source = 'basket.png')]
  5. private var BASKET_SPRITE_CLASS : Class;
Внимание: синтаксис с квадратными скобками специфичен для flex-компилятора, так что если вы повторяете мои примеры в flash cs3, то

вам следует импортировать в библиотеку эти картинки и программно создать их экземпляры, чтобы затем подать их как параметр объекту renderable. Фактически я мог использовать в качестве графического наполнения не просто внешний файл, но и клип из flash-файла. Например, так:
  1. [Embed(source='foamarticle.swf', symbol='BALL_MOVIECLIP)]
  2. private var BALL_SPRITE_CLASS : Class;
  3.  
  4. [Embed(source='foamarticle.swf', symbol='BASKET_MOVIECLIP)]
  5. private var BASKET_SPRITE_CLASS : Class;
Теперь рассмотрим главную часть кода, собственно, создающую физическую модель и ее графическое представление:
  1.  это мяч (физическая модель)
  2. var ball : Circle = new Circle (100, 100, 25, 20);
  3.  
  4.  создаем объект графического представления
  5. var ballSprite: DisplayObject = new BALL_SPRITE_CLASS();
  6.  
  7.  масштабируем картинку так, чтобы ее размер совпал с удвоенным радиусом шара
  8. ballSprite.width = ballSprite.height = 50;
  9.  
  10. /* добавляем в движок объект-мяч, также указываем то что объект будет обсчитываться физическим движком, 
  11. то что он будет отображен на экране и последний параметр задает информацию об клипе, 
  12. который будет визуализировать объект */
  13. foam.addElement( ball, true, true, new DisplayObjectData( ballSprite ) );
  14.  
  15.  обводка начального положения мяча
  16. foam.addElement(new Circle (100, 100, 28, 20), false);
  17.  
  18.  заставляем мяч падать вниз на корзину
  19. ball.addForce (new Vector (0, 20));
  20.  
  21.  физическая модель корзины
  22. var basket : RigidBody = new RigidBody (100, 400, 10, ShapeUtil.createRectangle(200, 180));
  23.  
  24.  создаем объект графического представления
  25. var basketSprite: DisplayObject = new BASKET_SPRITE_CLASS();
  26.  
  27.  подгоняем картинку под размер корзины
  28. basketSprite.width = 200; 
  29. basketSprite.height = 180;
  30. foam.addElement( basket, true, true, new DisplayObjectData( basketSprite ) );
  31.  
  32.  обводка начального положения корзины
  33. foam.addElement(new RigidBody (100, 400, 10, ShapeUtil.createRectangle(200, 180)), false);
  34.  
  35.  теперь изменяем renderer на новый
  36. foam.setRenderer (new DisplayObjectFoamRenderer ());
Когда я вызывал метод addElement, то первым параметром указал физическую модель мяча, затем признак того, что к мячу следует применять физические расчеты, затем признак того, что мяч будет визуализироваться и, наконец, сведения о параметрах этой визуализации (ballSprite). Результат работы приложения показан на рис. 2.



В ходе “доделок” Foam (а они обязательно будут) вы часто будете сталкиваться с объектом Vector. Этот объект используется почти везде, где нужно задать координаты объекта, его скорость, силу, действующую на объект. Однако помимо свойств x,y в составе Vector-а есть ряд методов реализующих “классические” операции над векторами:

Операция сложения векторов реализуется либо методом plusEquals, либо plus. В любом случае нужно передать как параметр методу ссылку на “добавляемый” вектор. Однако в первом случае результат сложения будет присвоен объекту вектору от имени которого был вызван метод plusEquals.
  1. var v1 : Vector = new Vector (1, 2);
  2. var v2 : Vector = new Vector (1, 2);
  3. var v3 : Vector = v1.plus (v2);
  4. v2.plusEquals (v3);
По аналогии, есть операции разницы между векторами (minusEquals и minus). Для умножения вектора на скаляр используйте методы timesEquals и times. Разделить вектор на скаляр помогут dividedByEquals и dividedBy. Метод magnitude вернет длину вектора. А normalize выполнит нормализацию вектора (“создаст единичный вектор”). Для получения скалярного произведения двух векторов используйте dot. Метод getPerp создаст новый вектор как перпендикуляр к основному. Для получения угла между двумя векторами используйте getAngle. Для сравнения двух векторов используйте метод equals либо equalsWithDelta (во втором случае вы можете передать как второй параметр методу точность сравнения - delta). Вам часто будет не хватать средств аналитики (хотя в каком движке они есть в полной мере?). Под аналитикой я понимаю возможность спросить у движка: “если я добавлю в эту точку, например, вот такой полигон, то не столкнется ли он там с каким-либо другим объектом сейчас или через пару минут?”. Могу только посоветовать взглянуть на объект PhysicsEngine (он скрыт внутри Foam) и его метод getBodyUnderPoint, позволяющий получить объект находящийся в указанной точке. На основе его можно сделать другие методы “все объекты находящиеся внутри заданного круга”, “все объекты внутри некоторого полигона” и т.д.
  1. var body : IBody = foam.engine.getBodyUnderPoint(new Vector (100, 100));
И последнее: в состав Foam входит (хотя мне лично ни разу не пригодился) механизм позволяющий “тягать” элементы мышью. Для этого нужно вызвать метод foam.useMouseDragger( true );

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

Несколько видеороликов:

Пример неправильно обработки полигонов со сложной формой:
Пример Spring:
Пример Bungee: