Разработка 2D-игр на HTML5: Манипулирование временем, часть 2

Использование преобразователей времени для реализации нелинейных эффектов

В предлагаемом цикле статей знаток HTML5 Дэвид Гири шаг за шагом демонстрирует процесс создания 2D-видеоигры на HTML5. Эта статья учит искривлять время по своей воле для создания нелинейного движения и изменения цвета.

Дэвид Джири, президент, Clarity Training, Inc.

David GearyАвтор, лектор и консультант Дэвид Джири является президентом компании Clarity Training, Inc., Он обучает разработчиков создавать Web-приложения с использованием JSF и Google Web Toolkit (GWT). Он участвовал в экспертных группах JSTL 1.0 и JSF 1.0/2.0, был соавтором сертификационного экзамена Web Developer Certification Exam компании Sun, а также принимал участие в проектах с открытым кодом, в том числе Apache Struts и Apache Shale. Книга Дэвида Graphic Java Swing стала одной из самых продаваемых книг о Java, а Core JSF (в соавторстве с Кэем Хорстманом) - одна из самых продаваемых книг о JSF. Дэвид регулярно выступает на конференциях и встречах пользовательских групп. Он является регулярным участником конференций NFJS с 2003 года, проводил курсы в университете Java и дважды удостаивался звания JavaOne rock star.



16.05.2013

В предыдущей статье Манипулирование временем, часть 1 (developerWorks, февраль 2013 г.) обсуждалась реализация манипулятора прыжка бегуна в игре Snail Bait. Это была реализация линейного движения: бегун поднимался и опускался с постоянной скоростью. Однако в реальном мире сила тяжести приводит к тому, что при подъеме движение прыгуна замедляется, а при спуске ускоряется.

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

Рисунок 1. Естественная последовательность прыжка
Последовательные этапы прыжка персонажа анимации

Из этой статье читатель узнает:

Преобразователь

Определение преобразователя из Dictionary.com:

сущ.
Устройство, которое получает сигнал в виде энергии одного вида и преобразует его в сигнал другого вида. Микрофон ― это преобразователь, который преобразует акустическую энергию в электрические импульсы.

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

Таймеры анимации и преобразователи времени

Я взял термин преобразователь из мира электротехники и мира кино и применил его ко времени. Преобразователи времени ― это функции, которые преобразуют одно значение времени в другое, как показано на рисунке 2.

Рисунок 2. Преобразователь
Преобразователь

Как свидетельствует Листинг 1, для реализации таймера анимации, способного искривлять время, достаточно простой комбинации из преобразователей времени и секундомеров. О реализации секундомера см. в разделе Синхронизированная анимация: секундомеры предыдущей статьи.

Листинг 1. Таймер анимации
// AnimationTimer
//
// Анимация выполняется в течение интервала времени, заданного в миллисекундах.
//
// Можно снабдить преобразователь дополнительной функцией, которая изменяет процент 
// завершения операции анимации. Это изменение позволяет включать нелинейное движение, 
// например, ускорение, замедление, эластичность и т.д.

AnimationTimer = function (duration, transducer)  {
   this.transducer = transducer;

   if (duration !== undefined) this.duration = duration;
   else                        this.duration = 1000;

   this.stopwatch = new Stopwatch();
};

AnimationTimer.prototype = {
       start: function () { this.stopwatch.start();           },
        stop: function () { this.stopwatch.stop();            },
       pause: function () { this.stopwatch.pause();           },
     unpause: function () { this.stopwatch.unpause();         },
    isPaused: function () { return this.stopwatch.isPaused(); },
   isRunning: function () { return this.stopwatch.running;    },
       reset: function () { this.stopwatch.reset();           },

   isExpired: function () {
      return this.stopwatch.getElapsedTime() > this.duration;
   },

   getElapsedTime: function () {
      var elapsedTime = this.stopwatch.getElapsedTime(),
          percentComplete = elapsedTime / this.duration;

      if (percentComplete >= 1) {
         percentComplete = 1.0;
      }

      if (this.transducer !== undefined && percentComplete > 0) {
         elapsedTime = elapsedTime *
                       (this.transducer(percentComplete) / percentComplete);
      }

      return elapsedTime;
   },

};

Таймеры анимации ― это, по существу, секундомеры с двумя дополнительными функциями. Во-первых, они отсчитывают время действия в миллисекундах и содержат метод isExpired(), который сообщает, истекло ли время действия таймера.

Во-вторых, таймеры анимации снабжены дополнительным преобразователем времени. Метод getElapsedTime() таймера пропускает время, указываемое секундомером, через преобразователь и возвращает результат.


Использование преобразователей времени для анимации движения

Элемент HTML5 canvas обеспечивает мощный низкоуровневый API 2D-графики, но ему недостает высокоуровневых абстракций, присутствующих в других графических средах, таких как Flash. Например, Flash позволяет задать начальный и конечный кадры анимации и создает промежуточные кадры в соответствии с предоставленным преобразователем времени — который в терминах Flash называется твинингом. В этой статье мы реализуем промежуточное движение в Canvas.

Рисунок 3 иллюстрирует два вида классических преобразования движения: замедление и ускорение. Чтобы проиллюстрировать эти эффекты, приложение, показанное на рисунке 3, строит вертикальную шкалу времени, которая соответствует реальному времени.

Рисунок 3. Ускорение (слева) и замедление (справа)
Ускорение (слева) и замедление (справа)

Ускорение, которое на рисунке 3 иллюстрируется скриншотами в левом столбце, сверху вниз, начинается медленно — далеко позади на шкале времени — и набирает скорость к концу. Замедление, которое иллюстрируется в правом столбце, представляет собой обратный эффект: начинается с высокой скорости с замедлением к концу.

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

Рисунок 4. Преобразователи для ускорения (f(x) = x^2) и замедления ((1-x)^2)
Графики ускорения и замедления

Эффект ускорения, показанный на левом графике рисунка 4, последовательно укорачивает время; например, когда реальное время (по горизонтальной оси) указывает на половину всей анимации, ускоряющий преобразователь выдает только четверть.

Эффект замедления, показанный в правой части рисунка 4,последовательно растягивает время; например, когда реальное время (по вертикальной оси) указывает на половину всей анимации, замедляющий преобразователь выдает три четверти.

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

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

Листинг 2. Исполнение преобразователей
AnimationTimer.makeEaseOutTransducer = function (strength) {
   return function (percentComplete) {
      strength = strength ? strength : 1.0;

      return 1 - Math.pow(1 - percentComplete, strength*2);
   };
};

AnimationTimer.makeEaseInTransducer = function (strength) {
   strength = strength ? strength : 1.0;

   return function (percentComplete) {
      return Math.pow(percentComplete, strength*2);
   };
};

AnimationTimer.makeEaseInOutTransducer = function () {
   return function (percentComplete) {
      return percentComplete - Math.sin(percentComplete*2*Math.PI) / (2*Math.PI);
   };
};

AnimationTimer.makeElasticTransducer = function (passes) {
   passes = passes || 3;

   return function (percentComplete) {
       return ((1-Math.cos(percentComplete * Math.PI * passes)) *
               (1 - percentComplete)) + percentComplete;
   };
};

AnimationTimer.makeBounceTransducer = function (bounces) {
   var fn = AnimationTimer.makeElasticTransducer(bounces);

   bounces = bounces || 2;

   return function (percentComplete) {
      percentComplete = fn(percentComplete);
      return percentComplete <= 1 ? percentComplete : 2-percentComplete;
   }; 
};

AnimationTimer.makeLinearTransducer = function () {
   return function (percentComplete) {
      return percentComplete;
   };
};

Этот способ деформации времени можно использовать для реализации естественного движения во время прыжка.


Естественное движение при прыжке

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

Чтобы проиллюстрировать возможности преобразователей времени, в листинге 3 показаны необходимые изменения в коде Snail Bait для реализации естественных, нелинейных прыжков вместо неестественных, полученных с применением линейного движения.

Листинг 3. Создание таймеров бегуна
equipRunnerForJumping: function () {
      ...

      // При подъеме бегун теряет скорость под действием силы тяжести (замедление)

      this.runner.ascendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseOutTransducer(1.0));

      // При спуске бегун набирает скорость под действием силы тяжести (ускорение)

      this.runner.descendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseInTransducer(1.0));
      ...

      };
   },
};

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


Тонкая настройка преобразователей времени

Так как используется возведение в степень, кривые, показанные на рисунке 4, называются графиками степенной функции. Они широко распространены во многих дисциплинах от экономики до анимации. Рисунок 4 демонстрирует кривые второй степени; изменение степени, как показано на рисунке 5, приводит к кривым других степеней.

На рисунке 5 показаны три кривые для создания эффекта ускорения. Это, слева направо, кривые второй, третьей и четвертой степеней. Увеличение показателя степени приводит к усилению эффекта ускорения.

Рисунок 5. Преобразователи для ускорения (f(x) = x^2) и замедления ((1-x)^2)
Степенные кривые ускорения

Методы AnimationTimer, которые образуют преобразователи ускорения и замедления в листинге 2, принимают аргумент, который соответствует половине показателя степени. Значение по умолчанию равно 1, что дает степень 2.

Изменяя значение, передаваемое методам AnimationTimer.makeEaseInTransducer() и AnimationTimer.makeEaseOutTransducer(), можно управлять степенью эффекта. Например, в листинге 4 эффекты ускорения и замедления прыжка бегуна усилены с 1.0 до 1.15. Это небольшое изменение немного усиливает оба эффекта, так что в верхней точке прыжка бегун зависает в воздухе немного больше.

Листинг 4. Создание таймеров бегуна
equipRunnerForJumping: function () {
      ...
      this.runner.ascendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseOutTransducer(1.15));

      this.runner.descendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseInTransducer(1.15));
      ...

      };
   },
};

Теперь, когда ясно, как реализовать нелинейное движение, рассмотрим способы реализации нелинейных эффектов для других производных времени.


Пульсирующие платформы: нелинейное изменение цвета

Производные времени

Если известно, как быстро движется объект, то можно рассчитать его положение, зная координаты начальной точки и время движения (при условии постоянной скорости). Поэтому движение ― это производное времени. Управляя временем, вы автоматически изменяете все его производные, такие как движение или изменение цвета.

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

Несмотря на намерение реализовать нелинейные прыжки, мы не стали создавать преобразователи движения; вместо этого мы ввели преобразователи времени, потому они позволяют создавать нелинейные эффекты с любым производным времени, таким как изменение цвета, как показано на рисунке 6.

Рисунок 6. Пульсирующая платформа
Скриншот пульсирующей платформы Snail Bait

Рисунок 6 демонстрирует платформу, которая пульсирует, постоянно изменяя свою прозрачность. Линейное изменение цвета привело бы к эффекту мигания. Нам же нужен эффект пульсации, когда платформа сначала быстро становится яркой, а затем медленно тускнеет. Для этого эффекта опять используются преобразователи ускорения и замедления, как показано в листинге 5.

Листинг 5. Конструктор манипулятора пульсации
PulseBehavior = function (duration, opacityThreshold) {
   this.duration = duration || 1000;
   this.brightTimer = new AnimationTimer(this.time,
                                         AnimationTimer.makeEaseOutTransducer());

   this.dimTimer = new AnimationTimer(this.time, 
                                         AnimationTimer.makeEaseInTransducer());
   this.opacityThreshold = opacityThreshold;
},

Листинг 5 демонстрирует конструктор манипулятора пульсации. Продолжительность равна половине времени, необходимого для отображения одного импульса, а порог непрозрачности ― это наиболее тусклое изображение в продолжение одного импульса.

Листинг 6 отражает реализацию метода манипулятора пульсации execute().

Листинг 6. Метод execute() манипулятора пульсации
PulseBehavior.prototype = { 
   ...

   execute: function(sprite, time, fps) {
      var elapsedTime;

      // Если ничего не происходит, запустить затемнение и вернуться

      if (!this.isDimming() && !this.isBrightening()) {
         this.startDimming(sprite);
         return;
      }

      if(this.isDimming()) {               // Затемнение
         if(!this.dimTimer.isExpired()) {     // Затемнение не выполнено
            this.dim(sprite);
         }
         else {                            // Затемнение выполнено
            this.finishDimming(sprite);
         }
      }
      else if(this.isBrightening()) {      // Осветление
         if(!this.brightTimer.isExpired()) {  // Осветление не выполнено
            this.brighten(sprite);
         }
         else {                            // Осветление выполнено
            this.finishBrightening(sprite);
         }
      }
   },
};

Метод манипулятора пульсации execute() реализует пульсацию — которая заключается в чередовании периодов усиления и ослабления яркости. Методы затемнения, вызываемые в листинге 6, показаны в листинге 7.

Листинг 7. Затемнение
PulseBehavior.prototype = { 
   ...

   startDimming: function (sprite) {
      this.dimTimer.start();
   },

   isDimming: function () {
      return this.dimTimer.isRunning();
   },

   dim: function (sprite) {
      elapsedTime = this.dimTimer.getElapsedTime();  
      sprite.opacity = 1 - ((1 - this.opacityThreshold) *
                            (parseFloat(elapsedTime) / this.duration));
   },

   finishDimming: function (sprite) {
      var self = this;
      this.dimTimer.stop();
      setTimeout( function (e) {
         self.brightTimer.start();
      }, 100);
   },
};

Интересной частью листинга 7 является метод dim(), который затемняет платформу путем уменьшения ее непрозрачности. Это уменьшение непрозрачности рассчитывается по времени, прошедшему с момента начала изменения яркости. (Точнее, это время, прошедшее по таймеру затемнения).

По окончании регулировки яркости finishDimming() метод останавливает таймер затемнения и после короткой паузы запускает таймер осветления.

Методы манипулятора пульсации для управления яркостью приведены в листинге 8.

Листинг 8. Осветление
PulseBehavior.prototype = { 
   ...

   isBrightening: function () {
      return this.brightTimer.isRunning();
   },

   brighten: function (sprite) {
      elapsedTime = this.brightTimer.getElapsedTime();  
      sprite.opacity += (1 - this.opacityThreshold) *
                         parseFloat(elapsedTime) / this.duration;
   },

   finishBrightening: function (sprite) {
      var self = this;
      this.brightTimer.stop();
      setTimeout( function (e) {
         self.dimTimer.start();
      }, 100);
   },
};

Большинство спрайтов ведет себя нелинейно.

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

Методы повышения яркости практически идентичны их собратьям, направленным на затемнение, за исключением того, что для каждого метода используется свой таймер. Кроме того, в листинге 7 метод brighten() повышает непрозрачность спрайта платформы, а метод dim() уменьшает ее.


Приостановка манипуляторов

В разделе Приостановка игры третьей статьи этого цикла мы обсуждали, как организовать паузу Snail Bait. Теперь, когда в игру добавлено поведение спрайтов, нужно уметь приостанавливать и их, как показано в листинге 9.

Листинг 9. Приостановка и возобновление работы всех манипуляторов спрайтов Snail Bait
SnailBait.prototype = {
   ...

   togglePausedStateOfAllBehaviors: function () {
      var behavior;

      for (var i=0; i < this.sprites.length; ++i) { 
         sprite = this.sprites[i];

         for (var j=0; j < sprite.behaviors.length; ++j) { 
            behavior = sprite.behaviors[j];

            if (this.paused) {
               if (behavior.pause) {
                  behavior.pause(sprite);
               }
            }
            else {
               if (behavior.unpause) {
                  behavior.unpause(sprite);
               }
            }
         }
      }
   },

   togglePaused: function () {
      var now = +new Date();

      this.paused = !this.paused;

      this.togglePausedStateOfAllBehaviors();

      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
};

Как видно из листинга 9, приостановить манипуляторы игры легко. Но для этого нужны методы pause() и unpause(), тогда как прежде от объекта манипулятора требовалась только реализация метода execute().

Манипуляторы реализуют свои методы pause() и unpause() по мере необходимости. Листинг 10 иллюстрирует реализацию этих методов манипулятором пульсации.

Листинг 10. Приостановка манипулятора пульсации
PulseBehavior.prototype = { 
   ...

   pause: function() {
      if (!this.dimTimer.isPaused()) {
         this.dimTimer.pause();
      }

      if (!this.brightTimer.isPaused()) {
         this.brightTimer.pause();
      }

      this.paused = true;
   },

   unpause: function() {
      if (this.dimTimer.isPaused()) {
         this.dimTimer.unpause();
      }

      if (this.brightTimer.isPaused()) {
         this.brightTimer.unpause();
      }

      this.paused = false;
   },

Чтобы приостановить или возобновить работу манипулятора, достаточно приостановить или вновь запустить его таймеры.


В следующей статье

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


Загрузка

ОписаниеИмяРазмер
Пример кодаwa-html5-game7-code.zip1,2 MБ

Ресурсы

Научиться

Получить продукты и технологии

  • Replica Island: загрузите открытый исходный код этого популярного платформера для Android.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Технология Java
ArticleID=929915
ArticleTitle=Разработка 2D-игр на HTML5: Манипулирование временем, часть 2
publish-date=05162013