Про восьмой флеш и физику. Часть 2

July 2, 2007

Flash 8 & Физика. Столкновение точки со стеной



Сегодня мы продолжаем начатый в прошлый раз рассказ о методах интеграции физики во flash. В прошлый раз мы рассмотрели основы векторной математики. Мы узнали, что такое вектор, что такое длина вектора, и нормаль к нему, и что такое нормализованный вектор. Поняли, как векторы преобразуются в углы, и какой смысл несет скалярное произведение двух векторов. Сегодня мы завершаем рассмотрение этой темы, нам осталось разобраться с понятием движения по вектору. Также мы рассмотрим механизм определения точки, где два вектора пересекаются.

Мы узнали, что вектор задает собой направление движения чего-то. Если вектор состоит из двух компонент (x,y), то это движение на плоскости. Из школьной формулы “расстояние = время * скорость”, ясно, что координаты объекта зависят от времени. Во flash есть два стандартных механизма позволяющих изменять координаты объекта во времени. Прежде всего, если flash основан на понятии временной шкалы на которой находится множество кадров сменяющих друг за другом, то простейший способ это ввести код который срабатывает на событие начала каждого нового кадра. Важно знать сколько времени занимает один кадр, по умолчанию, частота равна 12 кадрам в секунду. Скорость смены кадров определяется настройками документа flash в меню modify -> document.

Для примера создайте любой символ в библиотеке нового документа (я использовал круг, но какой либо разницы это не имеет). Дайте этому символу имя smb_circle, затем поместите два объект этого символа на слой Layer1 и дайте им имена obj_circle_1 и obj_circle_2. Введите следующий actionscript код в первом кадре документа.
  1. var v_coords = {start: {x: 10, y: 20}, ort: {x: 0.31, y: 0.94} };
  2. _root.obj_circle_1._x = v_coords.start.x;
  3. _root.obj_circle_1._y = v_coords.start.y;
  4.  
  5. var count_frames = 0;
  6. // счетчик количества пройденных кадров
  7. _root.onEnterFrame  = function (){
  8. count_frames ++;
  9. _root.obj_circle_1._x += v_coords.ort.x * 10;
  10. _root.obj_circle_1._y += v_coords.ort.y * 10;	
  11. // или так 
  12. _root.obj_circle_2._x = 200+ v_coords.start.x + v_coords.ort.x * 10 * count_frames;
  13. _root.obj_circle_2._y = v_coords.start.y + v_coords.ort.y * 10 * count_frames;
  14. }
Как видите при запуске ролика, падение двух шариков абсолютно идентично, просто второй падает на 200px правее. Я создал вектор v_coords задающий начальные координаты шарика. А затем назначил функцию обработчик события onEnterFrame (при наступлении каждого очередного кадра, т.е. каждую 12 долю секунды) который изменяет значения координат объекта obj_circle на величину произведения соответствующей компоненты нормализованного вектора и числа десяти. Почему я использовал произведение 10 и именно нормализованного вектора. Да просто это смотрится неплохо, шарик медленно ползет сверху вниз, не более того. На самом деле, нужно понимать, что в то время как на экране мы оперируем пикселями, объекты окружающего нас мира задаются в других единицах измерения и все что вам нужно, так это задать правила соотношения этих величин. Это достаточно нетривиальная задача, иногда в целях геймплея приходится нарушать эти пропорции. Часть игр, если их делать действительно похожими на реальную жизнь, будут невообразимо скучны или, наоборот, требовать от игрока просто нечеловеческой реакции или усидчивости. В общем играйтесь коэффициентами, но оставьте базовые законы математики и физики в покое.

А теперь отложим в сторону философию и рассмотрим, чисто технические сложности подхода основанного на onEnterFrame. Flash построен на концепции кадров сменяющихся друг за дружкой. Если вы указываете в свойства документа некоторую частоту FPS, то это значит что flash просто будет пытаться поддерживать именно такую скорость выполнения. Но это не означает, что он действительно может выполнить вашу просьбу. На это влияют такие факторы как загрузка процессора иными задачами, браузером, антивирусом. Возможно, что объем вычислений будет разниться в разных кадрах, а, следовательно, заявленные 12 кадров в секунду могут упасть до меньших значений. Давайте попробуем другой подход основанный на таймере. Мы можем попросить flash извещать нас каждые, скажем, 100 миллисекунд, и выполнять действия по расписанию.

Для этого нам следует воспользоваться функцией setInterval. В простейшем случае в качестве параметров этой функции следует указать имя функции, которая будет вызываться по расписанию, затем значение интервала в миллисекундах и, наконец, можно передать произвольное количество параметров которые будут в свою очередь переданы функции вызываемой из таймера. Для демонстрации некоторых неприятных моментов связанных с реализацией setInterval давайте модифицируйте пример выше.
  1. var v_coords = {start: {x: 10, y: 20}, ort: {x: 0.31, y: 0.94} };
  2. _root.obj_circle_1._x = v_coords.start.x;
  3. _root.obj_circle_1._y = v_coords.start.y;
  4.  
  5. _root.obj_circle_2._x = 200+ v_coords.start.x;
  6. _root.obj_circle_2._y = v_coords.start.y;
  7.  
  8. var initialTime_1 = getTimer();
  9. var initialTime_2 = getTimer();
  10. _root.onEnterFrame  = function (){
  11.   var nowTime = getTimer();
  12.   _root.obj_circle_1._x += v_coords.ort.x * 1;
  13.   _root.obj_circle_1._y += v_coords.ort.y * 1;	
  14.   trace ("-- onEnterFrame "+ (nowTime - initialTime_1));	
  15.   initialTime_1 = getTimer();	
  16. }
  17. // функция выполняющая изменение координат мяча,
  18. // параметры _fio, _age, _sex не используются и служат только
  19. // для демонстрации
  20. function fooUpdateBall (_fio, _age, _sex){
  21.   var nowTime = getTimer();
  22.   _root.obj_circle_2._x += v_coords.ort.x * 1;
  23.   _root.obj_circle_2._y += v_coords.ort.y * 1;	
  24.   trace ("-- setInteral "+ (nowTime - initialTime_2));	
  25.   initialTime_2 = getTimer();	
  26.   updateAfterEvent ();
  27. }
  28. // запускаем функцию срабатывающую каждые 50 мс
  29. setInterval (fooUpdateBall, 50, "Bill", 12, "male");
Для оценки времени я использовал функцию getTimer которая возвращает величину количества времени прошедшего от момента запуска ролика. Для удобства я изменил значение частоты кадров до 20 кадров в секунду, или, что тоже самое, 50 миллисекунд между отдельными кадрами (именно это число указано вторым параметром функции setInterval). Предположительно два шарика должны двигаться синхронно. Давайте проверим и запустим пример. А вот и нет. Шарики движутся хоть и близко, но не с одинаковой скоростью. Более того, обратите внимание на значения, выводимые функций trace – разность времени скачет от 45 миллисекунд до 55, и иногда в вызовах setInterval разницы подскакивают до 100 миллисекунд. Теперь изменим параметр – время задержки с 50 до 60 миллисекунд и соберем статистику (время задержки подскочило до 100 и не опускается ниже), теперь уменьшим время до 10 и снова соберем статистику (здесь числа примерно равны 15).

Проще говоря, если вы просите flash вызвать некоторую функцию через определенный интервал то, будьте готовы к погрешности примерно равной величине времени для одного кадра. С другой стороны уменьшать время кадра до величин, скажем, 60 кадров в секунду также нельзя, чтобы не привести к катастрофической потере производительности. Нельзя сказать, что недостаток погрешностей характерен только для flash, схожими проблемами страдают любые приложения, ведь в основе лежит квантование времени средствами операционной системы и многозадачность. Единственный выход смириться и пытаться вести учет времени самим, так если вашу функцию вызвали по расписанию не стоит верить, что это произошло точно через 50 миллисекунд, просто узнайте разницу во времени между прошедшим и текущим вызовом, и пользуйтесь школьной формулой S=V*T. Примерно, как в примере ниже, хотя там также возникают погрешности, но это лучшее что можно предложить:
  1. var initialTime_2= getTimer();
  2. function fooUpdateBall (_fio, _age, _sex){
  3.   var nowTime = getTimer();
  4.   var time: Number = int(( nowTime - initialTime_2) / 60) ;
  5.   _root.obj_circle_2._x = 200+ v_coords.start.x + v_coords.ort.x * time;
  6.   _root.obj_circle_2._y = v_coords.start.y + v_coords.ort.y * time;	
  7.   updateAfterEvent ();
  8. }
  9. // обратите внимание на число 60 здесь и на то, что внутри функции fooUpdateBall  разницу во времени я делю на него же
  10. setInterval (fooUpdateBall, 60, "Bill", 12, "male");
Да еще, напишите письмо adobe, чтобы они наконец-то озаботились поддержкой в flash player многопоточных вычислений, синхронизации, и заодно многоядерных машин. А ведь еще есть проблема, связанная с лимитом времени выполнения одного кадра в 15 секунд, и если ваш код сложнее, то крутитесь, как хотите.

И последняя рекомендация, в приведенных выше примерах расчет новых значений _x, _y был тривиален, возможно, что в иной ситуации расчет будет требовать условных конструкций, циклов, проще говоря, нужны будут промежуточные вычисления для определения новых координат объекта клипа. Никогда, никогда не используйте для сохранения этих промежуточных значений переменные _x, _y. Как только вы присваиваете новые значения этим переменным, то flash тут же пытается переместить объект в промежуточную точку, только затем, чтобы через долю секунды еще переместить его уже в конечную точку.

Теперь разберемся с пересечением объектов. Итак, у нас есть вектор движения материальной точки и есть вектор, задающий прямую, необходимо узнать пересекутся ли они и если да, то в какой точке. Если два вектора параллельны (т.е. равны их направления движения), то они не пересекаются. Внимание, я не говорю, “направлены в одну сторону”.
  1. // функция выполняющая проверку что два вектора параллельны
  2. function parallel(vec1, vec2){
  3.  return (((vec1.ort.x == vec2.ort.x) and (vec1.ort.y == vec2.ort.y))
  4.  or ((vec1.ort.x == -vec2.ort.x) and (vec1.ort.y == -vec2.ort.y)));
  5. }
  6. var vec1 = {start: {x: 10, y: 10}, ort: {x: 0.31, y: 0.94} };
  7. var vec2 = {start: {x: 200, y: 40}, ort: {x: -0.31, y: -0.94} };
  8.  
  9. // функция рисующая два вектора для иллюстрации
  10. function drawVector(v, c){
  11.  _root.lineStyle(5, c , 100, true, "none", "round", "miter", 1);
  12.  _root.moveTo (v.start.x, v.start.y);
  13.  _root.lineTo (v.start.x + v.ort.x* 500, v.start.y + v.ort.y*500);
  14. }
  15. // рисуем вектора разными цветами
  16. drawVector (vec1, 0xff00ff);
  17. drawVector (vec2, 0xffff00);
  18. // и проверяем их на пересечение
  19. if (parallel (vec1, vec2))
  20.  trace ("parallel");
  21. else
  22.  trace ("intersection");
После проверки того, что пересечение между двумя векторами v1 и v2 возможно, остается определить только координаты этой точки. Алгоритм вкратце таков, мы находим новый вектор vec3, который соединяет начало v1 и начало v2. Затем делим между собой скалярное произведение нормали vec3 и vec2 на скалярное произведение нормали vec1 и vec2. Получившееся число задает на сколько далеко в направлении вектора vec1 находится точка пересечения. Если формула вам непонятна, то вы можете пойти альтернативным путем. Например, вы знаете, что по двум точкам вектора (начальной и конечной) возможно задать уравнение, определяющее множество точек лежащих на прямой (x2 - x)(y2-y1) – (y2-y)(x2-x1) = 0. С другой стороны точка пересечения принадлежит обоим векторам, а значит надо просто решить систему из двух уравнений.
  1. x(y1-y2)+y(x2-x1)+(x1*y2 – x2*y1) = 0
  2. x(b1-b2)+y(a2-a1)+(a1*b2 – a2*b1) = 0
  3. Или, что тоже самое, если спрятать формулы под новыми обозначениями:
  4. x*A1+y*B1+C1=0 и x*A2+y*B2+C2=0
  5. Решать уравнения с двумя неизвестными нас всех учили еще в школе. Так что дерзайте. 
  6. // создаем два вектора 
  7. var vec2 = {start: {x: 0, y: 0}, delta: {x: 10, y: 5} };
  8. var vec3 = {start: {x: 5, y: 0}, delta: {x: 0, y: 5} };
  9. // вычисляем их длины
  10. vec2.len = Math.sqrt(vec2.delta.x*vec2.delta.x + vec2.delta.y*vec2.delta.y);
  11. vec3.len = Math.sqrt(vec3.delta.x*vec3.delta.x + vec3.delta.y*vec3.delta.y);
  12. // вычисляем нормализованные вектора
  13. vec2.ort = {x: vec2.delta.x / vec2.len, y: vec2.delta.y / vec2.len};
  14. vec3.ort = {x: vec3.delta.x / vec3.len, y: vec3.delta.y / vec3.len};
  15. // рисуем вектора
  16. drawVector(vec1,0xff00ff);
  17. drawVector(vec2,0xffff00);
  18. // фунция находящая пересечение двух векторов
  19. function intersection(v1, v2) {
  20.   var v3 = {start: {x: v1.x, y: v1.y} , 
  21.   delta: {x: v2.start.x-v1.start.x, y:v2.start.y-v1.start.y}};
  22.   // проверяем два вектора на параллельность
  23.   if (parallel(v1, v2)) {
  24.     // и если они действительно параллельны, то возвращаем особое значение
  25.     return {status: false}
  26.   }else var t = scalarNV(v3, v2) / scalarNV(v1, v2);
  27.    return {status: true, x: v1.start.x+v1.delta.x*t, y: v1.start.y+v1.delta.y*t};
  28. }
  29. // произведение нормали первого вектора и второго
  30. function scalarNV(v1, v2) {
  31.    return v1.delta.x*v2.delta.y-v1.delta.y*v2.delta.x;
  32. }
  33.  
  34. var vi = intersection (vec3,vec2 );
  35. // в поле статус возвращенного объекта хранится признак того было ли пересечение
  36. if (vi.status)
  37. // и если да - пересечение было, то выводим его координаты
  38.   trace ('intersection was at ' + vi.x + ", " + vi.y);
  39.  else
  40.   trace ('vectors are parallels');
Теперь рассмотрим, как определить новый вектор движения точки после удара об стену. Рассмотрите рисунок 1 (на нем обозначены v1 – вектор движения точки до удара об стену, v2 – вектор стены, v2n – нормаль стены, и v3 – вектор отраженной точки, также смотрите на проекции векторов до и после удара), и обратите внимание на проекции векторов движения точки до и после удара на нормаль стены и вектор самой стены.



План действия вкратце таков: найти точку пересечения вектора стены и вектора движения точки, возможно, что они вообще не пересекаются тогда нам ничего делать не нужно. Если же пересечение, все-таки, было, то нужно найти нормаль стены, как это сделать смотри в прошлой части статьи, затем нужно спроецировать вектор движения точки v1 на вектор стены v2 и на его нормаль v2n. Затем получившуюся проекцию на нормаль мы перевернем (перевернем обе компоненты, как x, так и y, рисунок показывает ситуацию падения на горизонтальную плоскость, мы же выводим универсальную формулу для произвольно ориентированной стены). И последний шаг, нужно на основании полученных проекций (в том числе и перевернутой) сформировать новый вектор который будет равен сумме этих проекций, и исходить из точки найденной с помощью алгоритма поиска пересечения.
  1. // код функций scalarNV, intersection берем из предыдущего примера
  2. function len (v){
  3.  return Math.sqrt(v.delta.x*v.delta.x + v.delta.y*v.delta.y);
  4. }
  5. // обновленная функция рисующая два вектора для иллюстрации
  6. function drawVector(v, c){
  7.  _root.lineStyle(5, c , 100, true, "none", "round", "miter", 1);
  8.  _root.moveTo (v.start.x, v.start.y);
  9.  _root.lineTo (v.start.x + v.delta.x, v.start.y + v.delta.y);
  10. }
  11. // функция находящая скалярное произведение двух векторов
  12. function dot (v1 , v2){
  13.  return v1.delta.x*v2.delta.x + v1.delta.y * v2.delta.y;}
  14. // функция выполняющая расчет отражения точки от стены
  15. function makeBounce (){
  16. // очищаем сцену от ранее нарисованных объектов
  17. _root.clear();	
  18. // создаем два вектора 
  19. var v1 = {start: {x: 100, y: 100}, delta: {x: 100, y: 300} };
  20. // v1 - задает направление падения токи
  21. var v2 = {start: {x: 10, y: 300}, delta: {x: 400, y: 200} };
  22. // случайным образом изменяем направление движения падающей точки
  23. v1.delta.x += (0.5 - Math.random())*500;
  24. v1.delta.y += (0.5 - Math.random())*500;
  25. // v2 - задает стену
  26. // выполним подготовительные операции для векторов - вычисляем их длины
  27. v1.len = len(v1)
  28. v2.len = len(v2)
  29. // вычисляем нормализованные вектора
  30. v1.ort = {x: v1.delta.x / v1.len,y: v1.delta.y / v1.len};
  31. v2.ort = {x: v2.delta.x / v2.len,y: v2.delta.y / v2.len};
  32. // рисуем вектора
  33. drawVector(v1,0xff00ff);
  34. drawVector(v2,0xffff00);
  35. var vi = intersection (v1,v2 );
  36. if (vi.status){
  37.   // и если пересечение было, то выводим его координаты
  38.   trace ('intersection was at ' + vi.x + ", " + vi.y);
  39.   // находим левую нормаль к вектору стены
  40.   v2.left = {delta: {x: v2.delta.y, y: - v2.delta.x } };
  41.   // находим длину нормали вектора стены, вообще то она должна быть равна длине исходного вектора стены v2
  42.   v2.left.len = len (v2.left);  
  43.   // находим нормализованный вектор левой нормали стены
  44.   v2.left.ort = {x: v2.left.delta.x / v2.left.len,y: v2.left.delta.y / v2.left.len};
  45.   // находим скалярные произведения, они нужны для расчета проекций векторов
  46.   var dot_v1_v2 = dot (v1, v2);
  47.   var dot_v1_v2left = dot (v1, v2.left);  
  48.   // теперь сами проекции
  49.   var projectV2 = {delta: {x: dot_v1_v2*v2.ort.x/(v2.len), y: dot_v1_v2*v2.ort.y/v2.len} };
  50.   var projectV2left = {delta: {x: dot_v1_v2left*v2.left.ort.x/v2.left.len, y: dot_v1_v2left*v2.left.ort.y/v2.left.len} };  
  51.   // переворачиваем проекцию на нормаль стены
  52.   projectV2left.delta.x *= -1;
  53.   projectV2left.delta.y *= -1;  
  54.   // и, наконец, складываем две проекции вектора получая исходящий вектор движения отраженной точки
  55.   var v3 = {delta: {x: projectV2.delta.x + projectV2left.delta.x, y: projectV2.delta.y + projectV2left.delta.y} };
  56.   // теперь зная направление движения вектора необходимо задать его начальную точку
  57.   // это будет точка пересечения
  58.   v3.start = vi;
  59.   // проверяем, чтобы длина входного вектора был равна длине исходящего вектора, 
  60.   // если у вас эти цифры не совпали, то где то находится ошибка
  61.   trace (len(v3)+ " , "+ len(v1));
  62.   drawVector(v3,0x00ffff);  
  63. }
  64.  else
  65.   trace ('vectors are parallels');
  66. }
  67. // запускаем таймер многократного повторения имитации столкновения
  68. setInterval (makeBounce , 2000);
На сегодня все. В следующий раз мы разберем поведение более сложных объектов, чем точка и линия.