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

March 27, 2008

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

Почти год назад я написал серию из трех статей посвященных основным физическим законам и их практическому применению при разработке flash-приложений. Тогда фокус внимания был посвящен flash 8, а код писался на actionscript2. Несколько раз я порывался написать статью, посвященную физике и flash, но использующую не “собственные поделки”, а flade (известный и, по сути, единственный качественный) физический движок для flash 8. В flade уже предусмотрены типовые операции: обнаружение коллизий объектов, расчет движения тел после столкновений, действие сил и ограничений. Но не сложилось: находились более интересные темы для статей. Затем прошло время, и actionscript 2 ушел в прошлое.

Теперь, если вы начинаете новый flash проект, то в расчет следует брать только actionscript3. Здесь же ситуация с движками (не только физика, но 3d-графика) гораздо лучше: навскидку можно назвать несколько проектов заслуживающих внимание: box2dflash.sourceforge.net/, www.cove.org/ape/, blog.generalrelativity.org/?cat=12 , fisixengine.com/, lab.polygonal.de/motor_physics/. В практике я сталкивался только с движками под номером 2, 3, 4. Первый из них (ape) был разработан Aleс Cove - это автор также и единственного толкового физического движка для flash 8 - flade. К сожалению, развитие flade было прекращено несколько лет назад, хотя была попытка переписать данный движок на haxe (язык программирования, для которого есть компилятор, формирующий исполняемый на flash player файл). Развитие же ape продолжается и по настоящий момент (также есть результаты переноса данного движка на c++ и java). Если почитать отзывы на сайтах, “погонять” демки (посматривая одним глазом загрузку процессора) и попробовать сделать несколько приложений, то впечатление будет самое положительное.

Fisix … отношение к данному движку двойственное: с одной стороны, если оценить функциональность движка по количеству готовых для использования функций, то fisix превосходит и ape и foam. С другой стороны fisix не доступен в виде исходных кодов – есть только скомпилированная версия библиотеки в виде swc-файла. Чем это плохо? Если вы хотите серьезно заниматься разработкой “физических” игр или приложений для flash, то будьте готовы, что выбор движка нельзя делать только по сложной и красивой “демке”. Традиционно демку делают так: на сцене размещается набор объектов (прямоугольники, круги, возможно, кривые), некоторые из этих частей образуют более сложные объекты (четыре круга плюс прямоугольник – вот вам машинка). Добавим к объектам ограничение в виде рычага или “резинки”, “намешаем” немного сил трения и гравитации – вот и готова демка. После ее запуска объекты на экране двигаются, сталкиваются, скатываются по наклонным поверхностям – казалось бы, вот она – симуляция. Но нет. В реальности для построения качественного геймплея вам потребуется в движке развитые средства извещения о происходящих событиях. Например, столкнулись два объекта (машинка со столбом) – тут же должна вызваться специальная функция, которая посчитает степень повреждений для обоих объектов и уменьшит у машинки количество жизней или вообще прекратит игру: мол, авария, машинка уничтожена. Нужна возможность гибко менять характеристики объектов и некоторые физические законы или константы: вот машинка сталкивается со стенами, отскакивает от них, а через минуту правила игры меняются и машинка становится “настоящим приведением”, проходя через все игровые объекты без повреждений и учета коллизий. Пригодится и функция управления временем: так чтобы можно было “откатить” время назад. Почти всегда нужны средства гибкого управления силами, действующими на моделируемые объекты: они могут быть постоянными, временными, действующими в рамках определенной области, изменяющимися во времени или действующими избирательно (в рамках определенной области, в течении некоторого интервала). Собственно, список таких требований можно продолжать и пополнять по мере создания качественной flash-игры и все эти возможности вам придется добавлять к “чужому” движку самостоятельно. Когда я интересуюсь у знакомых профессионально занятых в flash gamedev: “Ну, как оно, какой движок самый, самый ?”. Они в ответ смеются: “в качестве основы мы когда-то взяли движок X, но спустя время от него уже ничего не осталось, все переделано и улучшено”. Еще негативным моментом в fisix оказалось то, что развитие проекта то ли продолжается, то ли заморожено … не понятно. Одним словом, последняя выложенная версия почти двухгодичной давности и это не радует. Что касается FOAM, то с ним я столкнулся несколько месяцев назад, когда мне нужно было сделать небольшой проект. Времени было достаточно, и я решил поэкспериментировать с новым для себя физическим движком, покопался в его “внутренностях”, добавил несколько полезных функций и решил оформить свои “путевые заметки” как статью.

Для того чтобы получить самую свежую версию библиотеки вам нужно загрузить ее из svn-хранилища foam-as3.googlecode.com/svn/trunk. Исходные коды библиотеки неплохо документированы, плюс, вместе с библиотекой идет и пример ее использования. Весь дальнейший код я пишу в flashdevelop и компилирую с помощью flex sdk, хотя с равной долей успеха я мог бы писать код и в flash cs3 или flex builder. Не забудьте подключить к проекту путь к каталогу, где находятся исходники библиотеки, и давайте начнем с самого простого. Физический движок не может быть оторван от подсистемы визуализации хода симуляции. FOAM использует подход с тем, что необходимо на сцене разместить объект типа org.generalrelativity.foam.Foam (этот класс производен от Sprite). Далее я привожу пример заготовки, которая будет развиваться по ходу статьи. Конструктор главного класса приложения создает объект FOAM, размещает на сцене и устанавливает обработчик события “начало очередного кадра” в виде функции run.
  1. import org.generalrelativity.foam.*;
  2.  
  3. [SWF(width="800", height="662", backgroundColor="#334433")] 
  4. public class MainCode extends Sprite {
  5.  
  6.   private var foam:Foam = null;
  7.  
  8.   public function MainCode() {
  9.     добавляем обработчик события начало нового кадра
  10.    addEventListener(Event.ENTER_FRAME, run);
  11.    foam = addChild( new Foam() ) as Foam;
  12.   } end of -- constructor --
  13.  
  14.   private function run(evt:Event):void {
  15.   }
  16. } end of – class --
Если запустить данный пример, то мы увидим … да ничего мы не увидим: на сцену необходимо поместить непосредственные физические модели. Однако перед тем как я это сделаю немного слов об идее лежащей в основе симуляции FOAM. Автор Foam провел неплохое планирование структуры движка, составляющих его объектов, сил … На своем сайте он писал об идее смены “ядра расчетов” так, чтобы можно было подобрать качественную симуляцию (специфическую для вашего приложения) и при этом не слишком завысить системные требования (несмотря на значительное ускорение виртуальной машины flash в actionscript 3, большое количество объектов на экране способно “подвесить” flash player). Так вот, очевидно, что в физике и gamedev главная цель не создать абсолютно точную симуляцию, а создать “забавную игру”. И в рамках этого мы можем достаточно вольно поступать с физическими законами и пренебрегать рядом факторов оказывающих влияние на движение тел. Условно говоря, каждый шаг симуляции представляется как чередование двух процессов. Сначала мы изменяем положение тел, с учетом действующих на них сил, их скоростей, ограничений. А затем необходимо определить те пары объектов, которые пришли в столкновение, и рассчитать их “отскок”. Каждая из этих задач достаточно не тривиальна (попробуйте найти мои статьи по flash 8 и физике, там я рассказывал об основных проблемах и способах их решения). Например, задача обнаружения столкновений двух тел решается в два этапа: сначала широкое сканирование. В ходе этого процесса реальные фигуры заменяются на “окружающие” их прямоугольники (bounding box), для которых проверка пересечения примитивна. Очевидно, что не всякие объекты, столкнувшиеся на “широкой стадии”, действительно пришли в соприкосновение (зато мы отбросили множество пар заведомо не столкнувшихся объектов) и поэтому запускается второй этап: “точная проверка”. Здесь проверка идет с учетом реальных геометрических фигур. Шаг физической симуляции после обнаружения столкновения должен принять решение об изменении направления движения тел, их скоростей. Очевидно, что каждый из этапов может быть решен различными способами. Поэтому автор foam решил создать три фактора управляющих симуляцией: “то, что отвечает за широкое обнаружение столкновений”, “то, что отвечает за точное обнаружение столкновений” и, наконец, “то, что отвечает за перемещение тел”. Каждую из этих частей “то, что отвечает за что-то …” flash-программист можно заменить собственной реализацией. Хотя выбор заменяемых частей движка не богат (точнее, его вообще нет), но искать ошибки и вносить правки в движок очень легко: изменения в одной части не приводят к непонятным ошибкам в других подсистемах. В следующем примере я устанавливаю алгоритм расчета движения тела как Runge-Kutta (возможен и вариант Euler). Чем отличаются эти два подхода в плане погрешности вычислений и нагрузки на процессор можно прочитать на сайте автора foam blog.generalrelativity.org/?p=20 .

Вторая строка в примере устанавливает количество итераций (Foam в рамках одного шага симуляции на самом деле выполняет несколько шагов, результаты которых комбинируются для более точного расчета движения тела). Последняя строка устанавливает “то, что отвечает за обнаружение столкновений”. Объект AABRDetector проводит “широкое обнаружение” и затем вызывает “то, что обнаруживает точные столкновения” (в примере это не показано).
  1. foam.defaultSolver = RK4;
  2. foam.solverIterations = 2;
  3. foam.setCoarseCollisionDetector (new AABRDetector ());
Настроив движок попробуем добавлять на него объекты. Для этого в объекте Foam есть метод addElement. Вот его сигнатура:
  1. public function addElement( element:ISimulatable, collide:Boolean = true, 
  2.       render:Boolean = true, renderData:* = null, solver:IODESolver = null ) : void
В качестве первого параметра функции нужно подать ссылку на физический объект (что это такое чуть дальше), затем логический признак того будет ли данный физический объект участвовать в коллизиях с другими объектами (однако на такой объект продолжают действовать силы и могут менять их местоположение). Следующий параметр конструктора – render – управляет тем, будет ли объект отображаться. По-умолчанию все объекты рисуются в виде их контура с особой пометкой локального центра координат. С помощью параметров render и renderData, можно заменить схематическое изображение тела на произвольный клип с картинкой. И последний параметр - solver - задает правила расчета движения объекта. Теоретически, можно часть объектов заставить перемещаться по алгоритму RK4, часть с помощью Euler. Теперь, зная как добавить объект на сцену, рассмотрим то, какие объекты существуют.



На рис 1. я привел иерархию встроенных в FOAM примитивов. В основе всего лежит интерфейс ISimulatable. Любой объект, поддерживающий его, (вообще-то это абсолютно все физические объекты на сцене) будут иметь такие характеристики как:
Свойство Комментарий
Поля
x,y координаты объекты (точнее центр его локальной системы координат).
vx,vy скорость движения тела по оси OX и OY, соответственно
mass масса тела
force вектор (две компоненты x,y) задающие силу, действующую на данный объект вот в этот, именно этот, момент времени.
elasticity степень эластичности материала, из которого изготовлен объект. Этот параметр влияет на поведение тел при столкновение. Действительно, ведь если столкнутся два шарика из ваты, то их поведение будет отличным от столкновения двух шариков из каучука. Значение должно быть между 0 и 1.
friction коэффициент трения (значение от 0 до 1).
Методы
addForce В качестве параметра методу передается объект Vector задающий значение силы оказавшей разовое действие на объект (например выстрел из пушки). Из второго закона Ньютона F = a*m следует, что фактическая величина ускорения объекта будет обратно пропорциональна его массе (также, свойство ISimulatable).
addForceGenerator В отличие от addForce оказывавшей разовое воздействие на объект, генератор силы влияет на объект все время симуляции (например, двигатель у ракеты).
removeForceGenerator А этот метод служит для удаления привязанного к объекту генератора силы (у нашей ракеты выгорело все топливо).
От интерфейса ISimulatable порожден интерфейс IBody. Его поддерживают все объекты на сцене у которых есть тело. Фактически говоря, единственный объект, который реализует только интерфейс ISimulatable – это абстрактная точка (однако добавлять подобный объект на сцену, очевидно, не возможно). В состав IBody входят следующие поля и методы:
Свойство Комментарий
q Величина вращения объекта в радианах.
av Угловая скорость объекта.
vertices Список вершин объекта.
edges Массив граней объекта.
addTorque Этот метод добавляет к телу вращательный момент.
addForceAtPoint Первым параметром метода задается значение силы действующей на тело, второй же параметр – точка приложения силы.
Это не все свойства объекта – но наиболее полезные. Теперь перейдем к рассмотрению конкретных классов реализующих описанные выше интерфейсы. Во первых это RigidBody (в общем случае это какой-то полигон). Чтобы его создать нужно либо передать все координаты вершин объекта (в его локальной системе координат) при вызове конструктора RigidBody. Либо можно воспользоваться вспомогательным классом ShapeUtil. Его назначение генерация массива вершин для RigidBody либо в виде некоторого прямоугольника (метод createRectangle), либо произвольного полигона. В этом случае методу createSymmetricPolygon нужно передать как параметры количество вершин, значение радиуса круга, в который затем будет вписан сформированный полигон, и возможно указать значение угла смещения. В следующем примере создается несколько полигонов, однако обратите внимание на параметры конструктора. Для любого тела (IBody) нужно указать значение его массы и возможно, начальную скорость линейного движения, значения коэффициентов трения и эластичности, величину угловой скорости. Второй наиболее часто используемый объект – круг, он представляется классом Circle (наследник от RigidBody). Входные параметры тривиальны: центр круга, его радиус и набор общих свойств для всех IBody (масса, скорость …).
  1.  создаем два объекта круга
  2. var circ_1 : ISimulatable = new Circle (100, 100, 50, 100, 0, 1);
  3. var circ_2 : ISimulatable = new Circle (100, 300, 50, 100, 0, 0);
  4.  
  5.  добавляем их на сцену
  6. foam.addElement (circ_1);
  7.  
  8.  но при добавлении второго я говорю, что не нужно учитывать действие 
  9. foam.addElement (circ_2, false);
  10.  
  11.  задаем набор координат для произвольного полигона
  12. var points_local : Array = [
  13.   new Vector (-30 , -10), 
  14.   new Vector (-20,  - 25), 
  15.   new Vector (20, - 35), 
  16.   new Vector (60, - 35), 
  17.   new Vector (60, -5), 
  18.   new Vector (40, 5), 
  19.   new Vector (20, 25), 
  20.   new Vector (20, 35), 
  21.   new Vector (10, 35), 
  22.   new Vector (-50, 35), 
  23.   new Vector (-60, 5)
  24. ];
  25.  
  26.  теперь на основании списка вершин создаем это тело
  27. var ri : RigidBody = new RigidBody(200,100,10, points_local);
  28. foam.addElement (ri);
  29.  
  30.  создаем прямоугольник
  31. var ri2 : IBody = new RigidBody (200, 300, 10, ShapeUtil.createRectangle (200, 50));
  32. foam.addElement (ri2);
  33.  
  34.  создаем шестиугольник
  35. var ri3 : IBody = new RigidBody (200, 400, 10, ShapeUtil.createSymmetricPolygon (6, 50, 0.3));
  36. foam.addElement (ri3);
Теперь создав сцену и заполнив ее объектами, попробуем запустить симуляцию. В общем случае есть два подхода: “запусти, и оно само работает” и “контролируй каждый шаг симуляции”. В первом случае нужно вызвать у объекта foam метод simulate для начала симуляции (метод stop останавливает симуляцию). Второй подход предполагает, что мы не вызываем simulate, а на основании показаний таймера или внутри функции обработчика ситуации “начало очередного кадра” вызываем метод step (сделай один шаг симуляции). Результат работы показан на рис. 2.


Обратите внимание, что круг помеченный “звездочкой” заполз на другую фигуру, в то время как первый круг аккуратно огибает полигон по краю. Это происходит потому, что второй круг был исключен из списка “способных к коллизии” объектов.

Теперь попробуем оказать воздействие на любой из объектов, для этого нужно вызвать метод addForce с указанием значения силы разложенной по осям OX,OY.
  1. ri3.addForce (new Vector (100, 0));
Можно применить силу не к центру масс объекта, а к определенной его точке. Например, следующая строка кода заставит вращаться прямоугольник относительно одного из углов (надо отметить, что такое приложение силы эквивалентно добавлению силы к центру масс объекта и заданию вращательного момента относительно некоторой точки):
  1.  внимание, координаты задаются в локальной системе координат самого объекта
  2. ri2.addForceAtPoint (new Vector (-90, 20), new Vector (100,50));
В следующий раз я продолжу рассказ об FOAM и расскажу о том, как для объекта присоединить генератор силы, как заменить стандартное представление фигуры на произвольный MovieClip.