Тема 10. Столкновения


  1. Два круга
  2. Два прямоугольника
  3. Точка и клип
  4. Сложная фигура и клип

1. Два круга

Проще всего определить факт столкновения между двумя кругами:

Пусть центры кругов находятся в точках (x1,y1) и (x2,y2), а их радиусы равны r1 и r2. Круги пересекаются, если расстояние между их центрами

меньше, чем сумма радиусов r1+r2.

к началу К началу страницы

2. Два прямоугольника

Метод hitTest

В среде Flash есть стандартное средство для проверки пересечения прямоугольников, ограничивающих два клипа (символа типа Movie Clip). Это метод hitTest, который можно вызывать для любых объектов-клипов. Вот пример использования этой функции
if ( clip1.hitTest(clip2) )
  trace ("Клипы пересекаются!");
Здесь clip1 и clip2 — имена двух клипов, которые задаются на панели Properties. Метод hitTest возвращает true («да») в том случае, когда прямоугольники, ограничивающие клип, пересекаются. То же самое можно было записать иначе, поменяв местами имена клипов:
if ( clip2.hitTest(clip1) )
  trace ("Клипы пересекаются!");
Мы изучим эту функцию на простом примере парковки автомашины (см. ниже). Желтая машина управляется клавишами-стрелками, задача — припарковаться между машин в левой или в правой части стоянки.

  Откройте файл PRACTICE\10\parking.fla, выделите желтую машину и добавьте к ней код двух обработчиков, обеспечивающих движение при нажатии клавиш-стрелок:
 onClipEvent (load) {
   v = 5;
   function move(d) {
     r = _rotation * Math.PI / 180;
     _x += d * Math.sin(r);
     _y -= d * Math.cos(r);
   }
 }
 onClipEvent (enterFrame) {
   if (Key.isDown(Key.UP)) move(v);
   else
   if (Key.isDown(Key.DOWN)) move(-v);
   if (Key.isDown(Key.LEFT)) _rotation -= 5;
   else
   if (Key.isDown(Key.RIGHT)) _rotation += 5;
 }
При столкновении с уже стоящей машиной надо выдать звуковой сигнал. Кроме того, желтая машина не должна «наезжать» на другие, поэтому после столкновения необходимо вернуть ее обратно в то положение, где она была до аварии. Для того, чтобы хранить предыдущие координаты и угол поворота машины, введем переменные _xOld, _yOld и _rotOld. Их начальные значение устанавливаются при загрузке клипа (событие load).
  Добавьте в обработчик события load строчки
 _xOld = _x;
 _yOld = _y;
 _rotOld = _rotation;
При входе в новый кадр будем проверять, не задела ли наша машина другие с помощью функции check. В этой функции в случае столкновения машина возвращается на предыдущее место и выдается звуковой сигнал. Затем запоминаем новое положение машины.
  В самое начало обработчика enterFrame добавьте код:
 check();
 _xOld = _x;
 _yOld = _y;
 _rotOld = _rotation;
Осталось написать функцию check. С помощью панели Properties дайте остальным машинам-клипам имена car1, car2, car3 и car4. Такая система имен позволит нам организовать цикл проверки по всем машинам.
  Добавьте в конец обработчика load код функции check:
 function check() {
   for (i = 1; i <= 4; i++)
     if (this.hitTest(_root["car" + i])) {
       _x = _xOld;
       _y = _yOld;
       _rotation = _rotOld;
       break;
     }
 }
Здесь в цикле проверяется пересечение клипа this (желтой машины, к которой присоединен этот код) с клипами car1, car2, car3 и car4. Если обнаружено пересечение, машина возвращается на предыдущее место и происходит выход из цикла по оператору break (столкновение с остальными машинами проверять не нужно).

Добавим проигрывание звука при столкновении.

  Дайте звуку crash.wav из библиотеки имя crash и добавьте в нужное место функции check код
 snd = new Sound();
 snd.attachSound("crash");
 snd.start(0, 1);
Проверьте работу клипа.

Посмотрев на исходное расположение машин, можно ожидать, что определение пересечения по границам прямоугольников будет довольно точным, поскольку все машины достаточно плотно заполняют отведенное им место. Однако при проверке клипа вы обнаружите, что столкновение происходит даже тогда, когда машины довольно далеко друг от друга.

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

  Создайте новый символ типа Movie Clip с именем Прямоугольник, нарисуйте красный прямоугольник без заливки так, чтобы его левый верхний угол находился в точке регистрации (где стоит крестик при редактировании символа). Выделите контур двойным щелчком и установите на панели Properties толщину линии 1 пиксель.
Для контуров можно также задать режим масштабирования (список Scale), то есть определить, как будет изменяться толщина линии при изменении размеров клипа. В списке Scale можно выбрать один из четырех вариантов:

Загрузка символа из библиотеки

Теперь можно перетащить на сцену клип-прямоугольник и перемещать его вслед за машиной. Но мы применим другой прием — «вытащим» клип из библиотеки во время работы ролика, используя код на ActionScript.

Сначала надо подготовить клип — дать ему имя.

  Дайте символу Прямоугольник из библиотеки кодовое имя rectangle для использования в программе (команда Linkage в контекстном меню). Добавьте в обработчик события load для машины строчку
 _root.attachMovie("rectangle", "rect",
                   _root.getNextHighestDepth() );
Здесь мы вызвали метод attachMovie (присоединить клип) для главного монтажного стола _root. У этого метода три параметра: Для того, чтобы расположить клип поверх всех существующих (на этом монтажном столе), мы использовали метод getNextHighestDepth («получить следующую самую высокую глубину»). Она вернет номер первого свободного уровня.

Определение границ клипа

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

Границы прямоугольника для любого клипа можно определить с помощью метода getBounds. Например, границы клипа qq на главном монтажном столе определяются так:

bounds = qq.getBounds(_root);
В переменную bounds будет помещен объект, у которого есть 4 свойства: xMin, xMax, yMin и yMax, определяющие предельные значения области по осям X и Y. Разница xMax-xMin дает ширину прямоугольника, а yMax-yMin — его высоту.
  Добавьте этот код в конец обработчика события enterFrame для машины:
 bounds = this.getBounds(_root);
 with (_root.rect) {
   _x = bounds.xMin;
   _y = bounds.yMin;
   _width = bounds.xMax - bounds.xMin;
   _height = bounds.yMax - bounds.yMin;
 }
Сохраните файл и проверьте его работу.
Запись with(_root.rect){...} говорит о том, что все команды внутри фигурных скобок относятся к свойствам объекта _root.rect. Вместо этого можно было записать:
_root.rect._x = bounds.xMin;
_root.rect._y = bounds.yMin;
_root.rect._width = bounds.xMax - bounds.xMin;
_root.rect._height = bounds.yMax - bounds.yMin;
Использование with(...) сокращает запись, но может усложнить понимание кода.
к началу К началу страницы

3. Точка и клип

Вторая форма hitTest

В этом примере красная точка (изображающая судно или яхту) при нажатии клавиш-стрелок может перемещаться по карте Валаамского архипелага (Ладожское озеро). Однако точка не может зайти на сушу.

Здесь используется еще один вариант вызова стандартной функции hitTest:

qq.hitTest ( x, y, true )
Эта функция определяет, попадает ли точка с координатами x и y (это глобальные координаты, для главного монтажного стола) в область клипа с именем qq. Третий параметр, равный true, означает, что надо учитывать форму фигуры внутри клипа, то есть попадание точки в пустые области «не считается».

Если третий параметр будет равен false, функция определяет, попала ли точка в прямоугольник, ограничивающий клип.

  Откройте файл PRACTICE\10\valaam.fla. Преобразуйте карту Валаамского архипелага в клип Карта (клавиша F8, тип Movie Clip). Дайте экземпляру клипа имя map.
  Создайте новый слой Точка и перетащите на него клип Точка из библиотеки. С помощью панели Properties установите высоту и ширину объекта 10 пикселей. Добавьте к клипу код, позволяющий двигать точку по экрану клавишами-стрелками (как на предыдущем уроке):
 onClipEvent (load) {
   v = 2;
   function move ( d ) {
     r = _rotation * Math.PI / 180;
     _x += d * Math.sin(r);
     _y -= d * Math.cos(r);
   }
 }
 onClipEvent (enterFrame) {
   if (Key.isDown(Key.UP)) _y -= v;
   else
   if (Key.isDown(Key.DOWN)) _y += v;
   if (Key.isDown(Key.LEFT)) _x -= v;
   else
   if (Key.isDown(Key.RIGHT)) _x += v;
 }
Проверьте ролик.
В простейшем случае, когда объект достаточно мал, можно проверить только попадание его центра в активную зону клипа.
  Добавьте в обработчик события load строчки
 _xOld = _x;
 _yOld = _y;
и функцию для процерки столкновения с картой:
 function check() {
   if ( _root.map.hitTest(this._x, this._y, true) ) {
     _x = _xOld;
     _y = _yOld;
     }
 }
В самое начало обработчика enterFrame вставьте вызов функции
 check();
Посмотрите, как двигается точка.

Карта (рисунок map.gif в библиотеке) — это файл в формате GIF, где область воды — прозрачная. Это легко проверить, если сменить фон клипа с синего на какой-нибудь другой. Поэтому можно было ожидать, что функция hitTest не будут фиксировать попадание в эти прозрачные области. Но этого не случилось — точка не может войти в прямоугольник, ограничивающий карту.

Трассировка растровых рисунков

Для того, чтобы точно учесть форму островов, необходимо преобразовать растровый (точечный) рисунок в векторный, то есть выполнить трассировку. При этом программа выделяет области одинакового (или близкого) цвета и превращает их в векторные заливки.
  Перейдите двойным щелчком а режим редактирования клипа Карта. Выберите команду меню Modify—Bitmap—Trace Bitmap... и преобразуйте рисунок в векторную форму при следующих параметрах:

Перейдите к редактированию сцены и просмотрите результат.

Параметры означают следующее:
  Теперь рисунок map.gif можно удалить из библиотеки, чтобы уменьшить размер файла. Вместо него в клипе Карта используется векторная фигура, полученная в результате трассировки.

Точки вдоль границы

Сейчас проверка выполняется только для одной точки — центра круга. Поэтому точка может немного «залезать» на сушу. Чтобы исправить ситуацию, мы будем проверять 8 точек, равномерно распределенных по границе круга:

На рисунке в скобках для каждой точки указаны ее координаты относительно центра (смещения dx и dy). Учитывается, что на экране ось Y направлена вниз.

Для того, чтобы можно использовать цикл для перебора точек, их смещения предварительно записаны в два массива. Радиус круга r определяется как половина ширины клипа.

  Добавьте в обработчик load два массива координат точек для проверки:
 r = _width / 2;
 dx = [0, 0.707*r, r, 0.707*r, 0, -0.707*r, -r, -0.707*r];
 dy = [r, 0.707*r, 0, -0.707*r, -r, -0.707*r, 0, 0.707*r];
и измените функцию check таким образом:
 function check() {
   for (i=0; i<8; i++)
     if ( _root.map.hitTest(_x+dx[i], _y+dy[i], true) ) {
       _x = _xOld;
       _y = _yOld;
       break;
     }
 }
Сохраните фильм и проверьте его работу.
Теперь точка «стопорится» (возвращается в предыдущее положение), если хотя бы одна точка оказалась в активной области карты. Оператор break (выход из цикла) служит для того, чтобы не проверять остальные точки, если одно пересечение уже найдено.
к началу К началу страницы

4. Сложная фигура и клип

Два подхода

Теперь рассмотрим самый сложный случай — столкновение двух фигур неправильной формы. Нужно сразу сказать, что универсального решения этой задачи не существует, нет такой функции, которая могла бы определять, пересекаются ли два клипа с учетом формы фигур. Если два основных подхода:

В следующем примере мы будем использовать второй способ. Машинка управляется клавишами-стрелками, задача — доехать до финиша. При касании камней слышится звук удара.

  Откройте файл PRACTICE\10\stones.fla и проверьте его работу.
Машину управляется клавишами-стрелками (соответствующий код уже добавлен к клипу), однако никакой реакции на столкновение с камнями и выход на финиш мы не видим.

По границам камней стоят значки — красные точки. Это символы типа Точка из библиотеки, которые определяют возможные точки столкновения машины с камнем.

  Двойным щелчком войдите в режим редактирования четвертого камня, вокруг которого нет точек. Перетащите из библиотеки на сцену клип Точка и расставьте точки по границе камня (для копирования клипа перетаскивайте его при нажатой клавише Alt).
Теперь нужно при очередном перемещении машины как-то узнать координаты этих точек. Для этого при создании клипа-камня мы построим массив, состоящий из ссылок на клипы-точки, которые являются «детьми» (дочерними объектами) для камня.

Цикл по всем дочерних объектов

Для перебора всех «детей» используется специальный вид цикла:
for ( x in объект ) { ... }
Здесь вместо слова объект нужно указать абсолютный или относительный адрес объекта. Например, код
for ( x in _root )
  trace( _root[x]._name );
выводит в окно Output имена всех объектов главного монтажного стола (они определяются на панели Properties). Вместо x можно использовать любое другое имя переменной.

Кроме клипов, к объекту могут присоединяться и другие элементы (например, функции). Определить класс объекта можно с помощью оператора typeof.

  Создайте новый слой Программа и добавьте в первый ключевой кадр код
 for ( x in _root )
   trace( "Объект " + _root[x] +
          ", класс: " + typeof _root[x] +
          ", имя: " + _root[x]._name );
Запустите фильм.
Скорее всего, вы увидите на экране что-то такое:
Объект WIN 9,0,45,0, класс: string, имя: undefined
Объект _level0.finish, класс: movieclip, имя: finish
Объект _level0.car, класс: movieclip, имя: car
Объект _level0.stone4, класс: movieclip, имя: stone4
Объект _level0.stone3, класс: movieclip, имя: stone3
Объект _level0.stone2, класс: movieclip, имя: stone2
Объект _level0.stone1, класс: movieclip, имя: stone1
Первая строчка определяет версию Flash-проигрывателя (в виде символьной строки, string). В данном случае используется версия 9.0.45.0 для Windows. Остальные строчки описывают клипы, находящиеся на главном монтажном столе (на уровне 0, отсюда _level0 — здесь это то же самое, что и _root).
  Номер версии Flash-проигрывателя можно получить и иначе, с помощью кода
 trace(_level0.$version);
  Выведите в окно Output информацию о внутренних объектах для камня _root.stone1.
Так как мы не давали имена клипам-точкам, программа сделала это самостоятельно, они называются instance1, instance2 и т.д.

Теперь можно написать код, с помощью которого строится массив объектов-точек для каждого камня.

  Выделите камень в левом нижнем углу (его имя — stone4) и добавьте к нему код обработчика события load:
 onClipEvent (load) {
   points = new Array();
   for (x in this)
     if ( typeof this[x] == "movieclip" &&
       this[x]._name.indexOf('instance') >= 0 )
       points.push(this[x]);
 }
При загрузке клипа создается новый внутренний массив points, в который записываются адреса всех клипов-точек — объектов типа movieclip, в именах которых есть слово instance. Метод push означает «добавить новый элемент в конец массива».

Теперь нужно написать функцию проверки столкновения: она будет возвращать true при столкновении (если хотя бы одна из точек попадает в область клипа car) и false, если столкновения нет. Для этого используется «точечная» форма метода hitTest, как и в предыдущем разделе.

  Добавьте к обработчику load для клипа-камня код функции hits:
 function hits ( obj ) {
   var p = new Object();
   for ( i=0; i<points.length; i++) {
     p.x = points[i]._x;
     p.y = points[i]._y;
     this. localToGlobal ( p );
     if ( obj.hitTest(p.x, p.y, true) ) {
       this.points[i].play();
       return true;
     }
   }
   return false;
 }

Локальные и глобальные координаты

В цикле перебираются все точки, адреса которых были ранее записаны в массив points. Координаты такой точки, _x и _y, заданы относительно родителя, то есть, клипа-камня. Это локальные координаты.

В то же время для функции hitTest надо задать абсолютные (глобальные) координаты на монтажном столе. Для перевода локальных координат в глобальные используется метод клипа localToGlobal. Он принимает объект p, имеющий свойства x и y. Глобальные координаты возвращаются в том же объекте.

  Существует также и метод, выполняющий обратное преобразование, он называется globalToLocal.

Если очередная точка попала в область клипа obj, начинается проигрывание клипа Точка. Если посмотреть «внутрь» этого клипа, видно, что при проигрывании (начиная с кадра 2) параметр Alpha цвета точки (непрозрачность) постепенно меняется от 0 до 100% и снова до нуля (работает анимация формы). Одновременно проигрывается звук удара.

Теперь нужно добавить реакцию на столкновение в код клипа car (желтая машина). Как и в предыдущем примере, мы будем запоминать предыдущее положение машины и, если произошло столкновение, возвращать машину в это место.

  Выделите машину и добавьте в обработчик события load код
 _xOld = _x;
 _yld = _y;
 _rotOld = _rotation;
и функцию
 function check(){
   if ( _root.stone4.hits(this) ) {
     _x = _xOld;
     _y = _yOld;
     _rotation = _rolOld;
   }
 }
В самое начало обработчика enterFrame вставьте вызов этой функции и запоминание новых координат
 check();
 _xOld = _x;
 _yOld = _y;
 _rotOld = _rotation;
Проверьте клип (машина не должна заезжать на выбранный камень).

Прототипы

Остается добавить абсолютно такой же код ко всем оставшимся камням. Но можно сделать лучше — добавить функции к прототипу класса MovieClip.

Прототип — это описание общих свойств всех экземпляров (объектов) какого-то класса. Все камни относятся к классу MovieClip, и если мы добавим функции к прототипу, их смогут использовать все объекты этого класса.

  Выделите кадр 1 слоя Программа, удалите весь старый код и добавьте новый:
 MovieClip.prototype.onLoad = function() {
   this.points = new Array();
   for (x in this) {
     if ( typeof this[x] == "movieclip" &&
          this[x]._name.indexOf("instance") >= 0 )
       this.points.push(this[x]);
   }
 }
 MovieClip.prototype.check = function(obj) {
   var p = new Object();
   for (i = 0; i < this.points.length; i++) {
     p.x = this.points[i]._x;
     p.y = this.points[i]._y;
     this.localToGlobal(p);
     if ( obj.hitTest(p.x, p.y, true))
       this.points[i].play();
       return true;
   }
   return false;
 }
Здесь мы использовали еще один способ назначения обработчика события. В первой строчке метод onLoad для всех объектов класса MovieClip определяется как новая функция, тело которой идет далее в фигурных скобках. Для того, чтобы этот обработчик вызывался, необходимо, чтобы с клипами-камнями был связан хотя бы какой-то код, хотя бы пустая строка-комментарий:
//

Кроме того, надо изменить функцию check для клипа-машины: теперь она должна проверять все 4 камня. Учитывая, что их имена stone1, stone2, stone3 и stone4, эту проверку можно сделать в цикле.

  Измените функцию check для клипа-машины так:
 function check() {
  for (i=1; i<=4; i++)
    if (_root["stone"+i].check(this) ) {
      _x = _xOld;
      _y = _yOld;
      _rotation = _rotOld;
      return;
    }
 }
Выделив поочередно все камни, добавьте в их блок кода строчку комментария:
 //
Проверьте ролик.

Обработка события для звука

Остается последний штрих: когда машина достигла финиша (клипа с именем finish), проигрывается музыка и, когда музыка заканчивается, машина возвращается на стартовую позицию.

Понятно, что начальное положение машины надо запомнить в обработчике load. Для этого введем переменные _x0, _y0 и _rot0.

  Добавьте в начало обработчика load код
 _x0 = _x; _y0 = _y; _rot0 = _rotation;
Проигрывание звука можно добавить в функцию check (или написать другую функцию).
  Присвойте звуку cheers.wav из библиотеки кодовое имя cheers (пункт Linkage... в контекстном меню). Добавьте в конец функции check код для проигрывания звука при пересечении клипов car и finish:
 if ( this.hitTest(_root.finish) ) {
   snd = new Sound();
   snd.attachSound("cheers");
   snd.start(0,1);
   }
 
Когда проигрывание звука заканчивается, нужно вернуть машину в исходное положение. Для этого объекту-звуку надо задать обработчик события soundComplete (завершение проигрывания звука).
  Добавьте после строчки snd.start(0,1); код
 snd.onSoundComplete = function() {
   _x = _x0; _y = _y0;
   _rotation = _rot0;
 }
При проверке обнаруживается, что при достижении финиша звук совсем не такой, какой мы слышим при проигрывании из библиотеки.

Дело в том, что функция check вызывается при каждой смене кадра, то есть 50 раз в секунду (для данного клипа). Причем каждый раз запускается новая копия звука, которая смешивается с предыдущими.

Чтобы исправить ситуацию, надо как-то запомнить, что звук уже проигрывается, и не запускать новую копию. Для этого используем новую логическую переменную soundOn. Она будет равна true, когда звук играет.

  Добавьте строчку
 soundOn = true;
после команды snd.start(0,1);. В начале обработчика load и в обработчике onSoundComplete вставьте строчки
 soundOn = false;
Просмотрите результат и сохраните файл.
к началу К началу страницы


Оглавление
 Математика и физика Назад В начало Вперед Внешние файлы


© 2007  К. Поляков


Сайт создан в системе uCoz