HTML5 2D 游戏开发: 操纵时间,第 2 部分

使用时间变换器来实现非线性效果

本系列文章中,HTML5 专家 David Geary 将告诉您如何一步一个脚印地实现 HTML5 的 2D 视频游戏。在本期文章中,您将学习如何按自己的意愿操纵时间,以创建非线性运动和色彩变化。

David Geary, 作者和演讲家, Clarity Training, Inc.

David GearyDavid Geary 是 Core HTML5 Canvas 的作者,也是 HTML5 Denver User's Group 的合著者和其他 8 本 Java 图书的作者,其中包括畅销书 Swing 和 JavaServer Faces。David 经常在各大会议发表演讲,包括 JavaOne、Devoxx、Strange Loop、NDC 和 OSCON,他曾三次当选为 JavaOne 之星。他还为 developerWorks 撰写了 JSF 2 fuGWT fu 系列文章。您可以通过 @davidgeary 在 Twitter 上关注他。



2013 年 5 月 06 日

HTML 5 专题

HTML5 代表了 Web 业务和云业务在实现方式上的里程碑式改变。本 HTML 5 专题将顺应潮流为您介绍一些和 HTML5 新特性相关的内容,及其炫酷的效果。

操纵时间,第 1 部分” (developerWorks,2013 年 2 月)是本期文章的先决条件,讨论了在 Snail Bait 中实现跑步者的跳跃行为。该实现导致的线性运动,即跑步者上升和下降的速率是恒定的。然而,在物理世界中,重力使任何以自己的力量跳跃的人在上升阶段都会减速,在下降阶段会加速。

本文表面上的目标是修改跑步者的跳跃行为,让她在跳跃时运动得更自然,如 图 1 所示。然而,本文的根本动机是向您展示如何扭曲时间,为时间的任何衍生物(如运动或颜色变化)创建非线性效果。

图 1. 一个自然跳跃序列
一个在跳跃序列中各个阶段的动画人物

在本文中,学习如何:

变换器

Dictionary.com 对 变换器的定义:

名词
一种设备,它以一种形式接收能量信号,并将该信号转换为另一种形式的信号: 麦克风就是一种变换器,它将声能转换成电脉冲。

  • 实现时间 变换器
  • 使用动画计时器 —一个包含变换器的秒表 —扭曲时间
  • 使用时间变换器创建非线性运动
  • 使用时间变换器创建非线性颜色变化
  • 实现逼真的跳跃、脉动和弹跳
  • 暂停行为

动画计时器和时间变换器

我采用了来自电气工程和动态影像领域的术语 变换器,并把它应用到时间。时间变换器是将时间从一个值转换为另一个值的函数,如 图 2 所示。

图 2. 变换器
该图例是两个分别显示不同经过时间的秒表,在两个秒表之间是以下 JavaScript 函数:function (percentComplete) { return Math.pow(percentComplete, 6); }

如 清单 1 所证明,结合变换器和秒表来实现可以扭曲时间的动画计时器,可以是一件简单的事情。请参阅在前一期文章中 “定时动画:秒表” 一节对实现秒表的讨论。

清单 1. 动画计时器
 // AnimationTimer.................................................................. 
 // 
 // An animation runs for a duration, in milliseconds. 
 // 
 // You can supply an optional transducer function that modifies the percent 
 // completed for the animation. That modification lets you incorporate 
 // nonlinear motion, such as ease-in, ease-out, elastic, etc. 
   
 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 元素提供了一个功能强大的低级别 2D 图形 API,但它缺乏在其他图形框架中可以找到的更高层次的抽象,如 Flash。例如,Flash 让您可以定义动画的起始帧和结束帧,并且 Flash 会根据您所提供的时间变换器创建两者之间(in between)的帧 —Flash 称之为 补间(tweening)函数。在这篇文章中,我使用 Canvas 实现运动补间。

图 3 说明了两个经典的运动补间:淡入和淡出。为了说明这些效果,图 3 所示的应用程序绘制了一条垂直时间轴,代表真实的时间。

图 3. 淡入(左)和淡出(右)
Snail Bait 跑步者的两个截屏系列,说明淡入和淡出

图 3 左侧截图所示的从上到下的淡入启动得较慢 —远远落后于时间轴 —并在结束时获得速度。而右侧所示的淡出则是相反的效果,开始时速度很快,结束时放缓。

从数学的角度来看,图 4 中的两个图所描绘的方程实现了淡入和淡出效果。横轴表示所完成的动画的真实时间百分比,而纵轴表示从一个相应的变换器所返回的完成百分比。图中的直线表示真实时间,而曲线显示效果如何扭曲时间。

图 4. 淡入 (f(x) = x^2) 和淡出 (1 - (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 () { 
      ... 

      // On the ascent, the runner looses speed due to gravity (ease out) 

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

      // On the descent, the runner gains speed due to gravity (ease in) 

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

      }; 
   }, 
 };

我现在没有创建秒表来计算跳跃的上升和下降时间,在 清单 3 中,我使用动画定时器。上升计时器配有一个淡出变换器,这会导致上升在开始时是高速度的,在上升过程中慢慢地失去速度。凭借一个淡入传感器,下降在开始时是慢慢地,并随着它的进度而获得速度。


微调时间变换器

因为它们使用一个数字作为幂来计算值,图 4 中所示的曲线被称为 幂曲线。幂曲线在从经济学到动画补间的许多学科中都很普遍。图 4 显示幂为 2 的曲线;修改这个数字,如 图 5 所示,产生不同的幂曲线。

图 5 显示用于淡入效果的三条幂曲线。从左至右,它们代表的幂分别为 2、3 和 4。增加指数可以增大淡入效果。

图 5. 淡入 (f(x) = x^2) 和淡出 (1 - (1-x)^2) 变换器
淡入幂曲线

清单 2 中用于创建淡入和淡出变换器的 AnimationTimer方法带有一个参数,代表其幂曲线的指数的一半。默认值是 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 nothing's happening, start dimming and return 

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

      if(this.isDimming()) {               // Dimming 
         if(!this.dimTimer.isExpired()) {     // Not done dimming 
            this.dim(sprite); 
         } 
         else {                            // Done dimming 
            this.finishDimming(sprite); 
         } 
      } 
      else if(this.isBrightening()) {      // Brightening 
         if(!this.brightTimer.isExpired()) {  // Not done brightening 
            this.brighten(sprite); 
         } 
         else {                            // Done brightening 
            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); 
   }, 
 };

大部分子画面行为是非线性的

在物理世界中,大部分时间的衍生物(如运动或颜色变化)都是非线性的。从弹跳的球到冲向终点的短跑运动员,几乎我们周围的一切都是非线性的。因为非线性在现实世界中无处不在,重要的是要知道如何在游戏中实现它。

调亮方法几乎与调暗的方法完全相同,区别在于各自所使用的定时器不同。此外,brighten()方法增加平台子画面的不透明度,而不是像 清单 7 中的 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.2MB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=877604
ArticleTitle=HTML5 2D 游戏开发: 操纵时间,第 2 部分
publish-date=05062013