Опубликован: 07.11.2006 | Уровень: специалист | Доступ: платный
Лекция 9:

Математика и физика Flash

  1. Теперь разберемся с "ленивыми" астероидами. Вот функция astMove.
    function astMove ()  {
        //  add velocity to position
        this._x +=  this.velX;
        this._y +=  this.velY;
        // perform screen wrapping
        if   (this._x>RIGHT)  {
          this._x  =   LEFT;
        }   else  if   (this._x<LEFT)   {
          this._x  =  RIGHT;
        }   else  if   (this._y>BOTTOM)  {
          this._y =  TOP;
        }   else  if   (this._y<TOP)   {
          this._y  =  BOTTOM;
        }
        //  check hitTest against ship_mc
        if   (this.hitTest(ship_mc._x, ship_mc._y, true))  {
          //   if ship is not already dead, destroy it
          if   (!shipDead)  {
            destroyShip();
            shipDead =  true;
          }
        }
      }

    Первая часть функции не требует объяснения. Здесь просто складываются скорость по направлению и позиция и затем осуществляется перенос. После этого следует проверка на столкновение астероида с кораблем.

    //  check hitTest against ship_mc
      if   (this.hitTest(ship_mc._x, ship_mc._y, true))  {
        //   if ship is not  already dead, destroy  it
        if   (!shipDead)   {
          destroyShip();
          shipDead =  true;
        }
      }

    Этот фрагмент реализует hitTest для фильма с использованием shapeFlag=true. Как вы помните, этот метод имеет один недостаток - он будет проверять только точку _x, _y корабля, которая является центральной точкой. Поэтому нос или хвост корабля будут проходить сквозь астероид, и столкновения при этом происходить не будет. Мы могли бы использовать shapeFlag=false и осуществлять проверку по граничному прямоугольнику. Однако вследствие этого корабль время от времени уничтожался бы без столкновения с астероидом. Из двух вариантов второй раздражал бы игроков гораздо больше, чем первый. Мы не можем использовать обнаружение коллизий по расстоянию, так как фильмы имеют неправильную форму, отличную от идеального круга или квадрата. Здесь возникла бы та же самая проблема. Вы можете попытаться найти более разумное решение этой проблемы, однако, как показывает практика, это сильно усложняет программу. Поэтому, в случае с нашим примером, позволим себе не доводить его до идеального состояния.

    Если происходит столкновение, выполняется функция destroyShip, которая, помимо всего прочего, удаляет корабль с рабочего места. Я выяснил, однако, при разработке этой программы, что даже после того, как корабль удален, координаты, отправленные в hitTest, в течение нескольких кадров будут продолжать вызывать столкновение. Поэтому я установил переменную shipDead на значение true при первом попадании и запустил эту переменную в качестве предпосылки запуска функции destroyShip. Это обеспечило выполнение данной функции только один раз для каждой коллизии.

  2. Раз уж зашла речь о функции destroyShip ():
    function destroyShip()  {
        //  remove ship
        removeMovieClip(ship_mc);
        //  update how many lives are left
        livesLeft--;
        //  if none, end game.
        if   (livesLeft<1)   {
          gotoAndStop(2);
        }   else   {
          //  tell it to come back in two seconds
          restoreID = setInterval(restoreShip, 2000);
        }
      }

    Эта функция удаляет фильм с кораблем с рабочего места и уменьшает переменную livesLeft.

    Если livesLeft равна нулю, переходим в кадр 2 фильма и остаемся в нем. Я добавил в кадр 2 текст "Game Over" и кнопку перезапуска, которая возвращает фильм обратно в кадр 1.

    В противном случае у нас остался еще как минимум один корабль. Мы используем setInterval для того, чтобы программа сделала паузу длиной в две секунды (это 2000 миллисекунд в переводе на время Flash), и затем выполняем функцию restoreShip. Имейте в виду, что здесь мы сохраняем ID интервала в переменной restoreID. Мы хотим, чтобы restoreShip выполнялась только один раз после столкновения, поэтому нам необходимо удалять интервал после его действия.

  3. Вернемся к restoreShip.
    function restoreShip() {
        // reincarnate ship
        shiplnit();
        shipDead = false;
        //  make sure this function doesn't run again
        clearInterval(restoreID);
      }
  4. Здесь все достаточно ясно. Запускаем shipInit для возврата корабля на рабочее место. Затем сбрасываем shipDead на значение "ложь", чтобы можно было уничтожить корабль заново, если он столкнется с другим астероидом. Наконец, мы сбрасываем интервал, чтобы не восстанавливать повторно корабль каждые две секунды (очень надоедает, поверьте).

    Можно запустить игру. Астероиды теперь будут полнофункциональными объектами, так же, как и ваш корабль. Вы можете попытаться избегать их, или, если вы садист, врезайтесь в астероиды и уничтожайте корабли! Ну, конечно, в целях тестирования:

  5. Я также добавил динамическое текстовое поле на рабочее место и присвоил ему переменную livesLeft, чтобы можно было иметь визуальное представление о моих "подвигах Гастелло".

    Теперь астероиды могут безжалостно врезаться в ваши корабли. Настало время мести.

  6. В начале мы определили, что при нажатии клавиши SPACE будет выполняться функция shoot (). Ознакомимся с ней.
    function shoot()  {
        //   if we have any ammo left...
        if   (numShots<5)  {
          numShots++;
          //  create one shot and make it move
          shot_mc  =   shots_mc.attachMovie("shot", 
             "s"+shotIndex, shotIndex++);
          shot_mc.onEnterFrame = shotMove;
          //  convert ship's rotation to radians
          rad =  ship_mc._rotation*Math.PI/180;
          // position shot at ship's nose
          shot_mc._x = ship_mc._x+10*Math.cos(rad);
          shot_mc._y = ship_mc._y+10 *Math.s in(rad);
          // determine shot's velocity, adding it to ship's velicity
          shot_mc.velX = 5 *Math.cos(rad)+ship_mc.velX;
          shot_mc.velY = 5*Math.sin(rad)+ship_mc.velY;
        }
      }

    Еще один кусок кода. Давайте опять разобьем его на части.

    function shoot()  {
        //   if we have any ammo left...
        if   (numShots<5)  {
          numShots++;
          //   create one shot and make it move
          shot_mc = shots_mc.attachMovie("shot", 
            "s"+shotIndex, shotIndex++);
          shot_mc.onEnterFrame  =  shotMove;
  7. Мы будем ограничивать число выстрелов пятью на каждый залп. При желании вы можете изменить это значение. Мы используем переменную numShots для протоколирования числа активных выстрелов, увеличивая ее значение с каждым новым выстрелом. По достижении значения 5, вся функция будет пропускаться. Если значение меньше пяти, будет добавляться фильм со снарядом в shots_mc (имейте в виду, что все выстрелы будут располагаться здесь для предотвращения конфликтов с астероидами по глубине). Мы присваиваем ссылку на этот фильм временной переменной shot_mc, а также присваиваем ее управляющий элемент onEnterFrame.

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


    Мой корабль имеет 20 пикселей в ширину, а также центральную точку регистрации. Это означает, что от носа до центра 10 пикселей. При повороте корабля это расстояние также рано 10 пикселям, но, как видно из правого рисунка, мы имеем очень знакомый нам треугольник. Имея угол поворота и расстояние в 10 пикселей, мы можем выяснить значения x и y, как показано на рисунке.

    x = cos(angle) * distance
      y = sin(angle) * distance

    Если прибавим их к значениям позиций _x и _y, то получим точную позицию для выстрела. Вот соответствующий ActionScript.

    //  convert ship's rotation to radians
      rad =  ship_mc._rotation*Math.PI/180;
      //  position shot at ship's nose
      shot_mc._x = ship_mc._x + (10 * Math.cos(rad));
      shot_mc._y = ship_mc._y + (10 * Math.sin (rad));

    Теперь мы сделаем почти в точности то же самое для определения скорости выстрела по направлению. Я пришел к тому, что 5 - оптимальная скорость выстрела. Снаряд вылетит из корабля под таким же углом, какой имеет в данный момент корабль. Поэтому его скорости по направлению X и Y могут быть разложены следующим образом.

    x velocity = cos(angle) *velocity
      y velocity = sin(angle) *velocity

    Другим небольшим внесенным мной дополнением было сложение текущей скорости по направлению корабля со скоростью снаряда, которую мы только что нашли. Если вы едете в автомобиле со скоростью 50 км/ч и бросаете из окна мяч со скоростью 10 км/ч в направлении движения автомобиля, то скорость мяча относительно земли будет равна 60 км/ч (здесь мы затрагиваем теорию относительности Эйнштейна, т.е. ее версию, представленную во Flash).

    //  determine shot's velocity, adding it to ship's velicity
      shot_mc.velX  =  5 * Math.cos(rad) + ship_mc.velX;
      shot_mc.velY  =  5 * Math.sin(rad) + ship_mc.velY;
  8. У нас на экране есть снаряд, и у него есть своя скорость по направлению. Нам нужно просто завершить функцию shotMove, и все будет готово! Однако эта функция довольно хитроумная. Она будет выполнять все действия по реализации движения каждого снаряда, проверяя на столкновение с астероидами, разламывая астероиды или удаляя их, а также протоколируя количество заработанных очков. Ух! Описание этой функции я буду излагать последовательно, не сваливая на вас сразу целую страницу кода.

    Для начала разберем хотя бы это короткое движение.

    function shotMove() {
        // add velocity to position
        this._x += this.velX;
        this._y += this.velY;
        // if shot is out of range, remove it
          if (this._x>RIGHT | | this._x<LEFT | | 
            this._y>BOTTOM | | this._y<TOP) {
          removeMovieClip(this);
          // update shot count
          if (numShots>0) {
          numShots--;
          }
        }
      }

    Здесь мы будем реализовывать сложную задачу, которая будет заключаться в использовании скорости по направлению для обновления позиции. Это уже нам знакомо. Затем мы выясняем, "вылетел" ли снаряд за пределы рабочего места. Если это так, удаляем его. Помните, что мы используем переменную numShots для протоколирования числа активных снарядов. Поэтому каждый раз при удалении снаряда мы уменьшаем переменную numShots. Я также выяснил, что иногда numShots может принимать отрицательное значение, что закончится тем, что можно будет выпускать больше пяти снарядов, так как переменная была уменьшена. Следовательно, прежде, чем уменьшать значение переменной, необходимо убедиться в том, что ее значение не равно нулю.

    Сейчас вы можете запустить фильм. Снаряды уже выпускаются. Однако вскоре вы заметите, что снаряды кончились, и уничтожать астероиды уже не так легко.

  9. Будем обрабатывать циклом массив, содержащий ссылки на все наши фильмы с астероидами, а также проверять коллизии для каждого из них.
    function shotMove()  {
        //  add veloicty to position
        this._x += this.velX;
        this._y += this.velY;
        //  if shot is out of range, remove it
        if (this._x>RIGHT | | this._x<LEFT | | 
           this._y>BOTTOM | | this._y<TOP)  {
          removeMovieClip(this);
          // update shot count if (numShots>0) {
            numShots--;
          }
        }
        // loop through asteroid array, checking for hit
        for (i=0; i<ast_array.length; i++) {
          if (ast_array[i].hitTest(this._x, this._y, true)) {
            // remove shot and update count removeMovieClip(this);
            if (numShots>0) {
              numShots--;
            }
            // remove asteroid from stage
            removeMovieClip(ast_array[i]);
            // and remove it from the array
            ast_array.splice(i, 1);
            // if this was the last one, create some new ones
            if (ast_array.length<1) {
              maxAst++;
              astInitID = setInterval(astInit, 2000);
            }
          }
        }
      }

    Мы используем ast_array.length в качестве управляющего элемента цикла for, так как он будет содержать число астероидов в массиве. Мы проверяем параметры снаряда _x и _y с использованием shapeFlag=true.

    При попадании мы удаляем снаряд аналогично тому, как описано выше. После этого удаляем фильм с астероидом с рабочего стола и используем ast_array.spice(i, 1) для удаления этого элемента из массива. Имейте в виду, что это также уменьшит на единицу ast_array.length, что позволит нам всегда обрабатывать циклом точное число имеющихся астероидов.

    Наконец, если ast_array достигает нуля - все астероиды успешно уничтожены - мы увеличиваем значение maxAst и создаем новую группу астероидов с помощью astInit. Однако мы добавим небольшой двухсекундный интервал перед запуском этой функции с помощью setInterval. Нам также потребуется удалять этот интервал astInitID, поэтому переходим к astInit и добавляем в него строку clearInterval.

    function astInit() {
        // create a number of asteroids
        astIndex = 0;
        while (astIndex<maxAst) {
          createAst();
        }
        clearInterval(astInitID);
      }

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

  10. Вместо исчезновения после единственного попадания снаряда, давайте сделаем так, чтобы астероиды раскалывались на части, как в игре Atari Asteroids. Сначала нам нужно, чтобы на месте попадания снаряда появлялось два астероида. Мы реализуем это с помощью функции createAst. Затем нам нужно будет сделать их меньшими, нежели исходный астероид. Это делается посредством изменения размеров фильмов.
    фильмов.
      function shotMove() {
        // add veloicty to position
        this._x += this.velX;
        this._y += this.velY;
        // if shot is out of range, remove it
        if (this._x>RIGHT | | this._x<LEFT | | this._y>BOTTOM | |  this._y<TOP) {
          removeMovieClip(this);
          // update shot count
          if (numShots>0) {
            numShots--;
          }
        }
        // loop through asteroid array, checking for hit
        for (i=0; i<ast_array.length; i++) {
          if (ast_array[i].hitTest(this._x, this._y, true)) {
            // remove shot and update count
            removeMovieClip(this);
            if (numShots>0) {
              numShots--;
            }
            // if asteroid is big enough, break it in two
            if (ast_array[i]._xscale>25) {
              // make this asteroid 1/2 size
              ast_array[i]._xscale /= 2;
              ast_array[i]._yscale /= 2;
              // create a new asteroid, make it same size, same position
              ast_mc = createAst();
              ast_mc._xscale = ast_array[i]._xscale;
              ast_mc._yscale = ast_array[i]._yscale;
              ast_mc._x = ast_array[i]._x;
              ast_mc._y = ast_array[i]._y;
            } else {
              // if asteroid is small, remove it from the stage
              removeMovieClip(ast_array[i]);
              // and remove it from the array
              ast_array.splice(i, 1);
              // if this was the last one, create some new ones
              if (ast_array.length<1) {
                maxAst++;
                astlnitID = setlnterval(astlnit, 2000);
              }
            }
          }
        }
      }
    Пример 8.2.

    Первой частью этого кода является

    if (ast_array[i]._xscale>25) {
        // make this asteroid 1/2 size
        ast_array[i]._xscale /= 2;
        ast_array[i]._yscale /= 2;

    Сначала проверяем _xscale астероида, в который попал снаряд. Если это значение больше 25, делим его на два. Его начальной величиной будет 100. После первого попадания значением станет 50. Попадание второго снаряда уменьшит размер до значения 25. После этого выражение if примет значение "ложь", и выполнится выражение else, код которого удалит астероид.

    Мы сделали имеющийся фильм меньше, и нам нужно добавить еще один, сделать его такого же размера и поместить его на той же позиции, что и первый. Это будет реализовывать вторая половина блока if.

    // create a new asteroid, make it same size, same position
      ast_mc = createAst();
      ast_mc._xscale = ast_array[i]._xscale;
      ast_mc._yscale = ast_array[i]._yscale;
      ast_mc._x = ast_array[i]._x;
      ast_mc._y = ast_array[i]._y;

    Помните, что функция createAst возвращает ссылку на созданный астероид. Здесь-то мы ее и используем. Мы присваиваем ее временной переменной ast_mc и настраиваем ее параметры _xscale, _yscale, _x и _y таким образом, чтобы они совпадали с параметрами астероида, в который только что попал снаряд. Также запомните, что createAst присваивает каждому новому астероиду случайные скорость и поворот, поэтому эти два маленьких обломка будут разлетаться различным образом в разных направлениях.

    Другим действием, которое выполняется createAst, является добавление нового астероида в массив ast_array, чтобы он в будущем проверялся автоматически при последующих проверках элементов массива на попадание. Сейчас все функциональности собираются воедино.

    Последнее, но не менее важное: нам нужно обеспечить подсчет очков. Я разместил еще одно динамическое текстовое поле вверху рабочего места для отображения количества очков. После этого добавляем последний штрих в нашу функцию shotMove. Теперь будет выполняться операция переключения на основе значения _xscale астероида, в который только что попал снаряд, и присваиваться разное количество очков, в зависимости от размера астероида - 100, 50 или 25. Это количество прибавляется к текущей сумме очков в поле.

    function shotMove() {
        // add velocity to position
        this._x += this.velX;
        this._y += this.velY;
        // if shot is out of range, remove it
        if (this._x>RIGHT | | this._x<LEFT | | this._y>BOTTOM | | this._y<TOP) {
          removeMovieClip(this);
          // update shot count
          if (numShots>0) {
            numShots--;
          }
        }
        // loop through asteroid array, checking
        for hit for (i=0; i<ast_array.length; i++) {
          if (ast_array[i].hitTest(this._x, this._y, true)) {
            // determine score based on asteroid's size (_xscale)
            switch (ast_array[i]._xscale) {
            case 100 :
              score += 10;
              break;
            case 50 :
              score += 50;
                break;
              case 25 :
                score += 100;
                break;
              }
              // remove shot and update count
              removeMovieClip(this);
              if (numShots>0) {
                numShots--;
            }
            // if asteroid is big enough, break it in two
            if (ast_array[i] ._xscale>25) {
              // make this asteroid 1/2 size
              ast_array[i] ._xscale /= 2;
              ast_array[i] ._yscale /= 2;
              // create a new asteroid, make it same size, same position
              ast_mc = createAst();
              ast_mc._xscale = ast_array[i] ._xscale;
              ast_mc._yscale = ast_array[i] ._yscale;
              ast_mc._x = ast_array[i] ._x;
              ast_mc._y = ast_array[i] ._y; }
            else {
              // if asteroid is amall, remove it from the stage
              removeMovieClip (ast_array[i]);
              // and remove it from the array
              ast_array.splice(i, 1);
              // if this was the last one, create some new ones
              if (ast_array.length<1) {
                maxAst++;
                astlnitID = setInterval(astlnit, 2000);
              }
            }
          }
        }
      }
    Пример 8.3.

    Итак, мы получили полнофункциональную версию игры Asteroids а-ля Flash, которая, однако, имеет огромное поле деятельности для усовершенствования и изменения. Вы можете сделать так, чтобы корабль взрывался при столкновении с астероидом. Это можно реализовать с помощью построения промежуточного объекта, начинающегося с третьего кадра корабля. Нужно просто выполнить gotoAndPlay(3) при попадании астероида перед его удалением. Вы можете захотеть добавить звуки с использованием материала, изученного в других местах этой книги. Вы также можете сделать так, чтобы, как и в оригинальной игре, появлялся вражеский корабль, что сделает бой еще более интересным и сложным. Для этого может быть использована большая часть того, что мы делали в нашей предыдущей "стрелялке".

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

    Теперь давайте изучим некоторые физические свойства Flash.

Игорь Хан
Игорь Хан

у меня аналогичная ситуация. Однако, если взять пример из приложения (ball_motion_04_click for trial.fla) то след остается. при этом заметил, что в моем проекте в поле "One item in library" виден кружок, в то время как в приложенном примере такого кружка нет.

Вопрос знатокам, что не так?

Александр Коргапольцев
Александр Коргапольцев

объект созданый мной упорно не желает оставлять след(единственное что добился, так это то что шарик резво гоняется за курсором) функция duplicateMovieClip остаётся не активной, т.е. следа от объекта не остаётся, но если я тоже самый код вбиваю в учебный файл всё работает, не могу понять где я ошибаюсь и почему в документе созданном заново, не работает код начиная от функции duplicateMovieClip? 

Тамара Ионова
Тамара Ионова
Россия, Нижний Новгород, НГПУ, 2009
Магомед Алисултанов
Магомед Алисултанов
Россия, Волгоград, лицей 2