关于Corona SDK最强大的一件事,就是任何display object,都可以动画之。这足以证明Corona的图像模型的超级灵活性。
动画允许你创建一个视觉丰富,引人入胜的使用体验。动画是通过生成一个帧序列来完成的,在帧到帧之间平滑的演变。术语“补间”(简称 inbetween)就是说这种中间帧的生成过程的。它通常用来略称,一个对象在动画期间一个属性的改变,比如位置补间。
变换
transition允许你很容易的只用一行代码来创建动画,这通过允许你补间一个display object的一个或多个属性来实现。例如你可以淡出一个display object,通过补间其alpha属性(这个alpha属性从1.0到0)。
做到这一点最简单的方法是,用 transition.to 方法,第一个参数是一个display object,第二个参数是一个table包含控制参数。这个控制参数指定了动画的时间,动画开始的延时(可选),这个display object最终的属性值。一个属性的中间值,是由一个指定的变化函数作为控制参数产生的。默认是一个线性函数。
下面是一些关于如何让一个矩形动画化的例子。(see Transition2 in the sample code):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local square = display.newRect( 0, 0, 100, 100 )
square:setFillColor( 255,255,255 )
local w,h = display.contentWidth, display.contentHeight
local square = display.newRect( 0, 0, 100, 100 )
square:setFillColor( 255,255,255 )
local w,h = display.contentWidth, display.contentHeight
-- (1) 移动square到右下角;大小减半
-- 以square的中心为本地原点; 淡出
transition.to( square, { time=1500, alpha=0, x=(w-50), y=(h-50) } )
-- (2) 在2.5秒后, square 淡入回来
transition.to( square, { time=500, delay=2500, alpha=1.0 } )
在第一个补间中,请注意多个值会改变:位置和alpha值。这是因为在控制参数中我们指明,x、y和alpha属性的最终值。对控制参数中指定的每个属性,这个库会读取当前值,并且在指定的时间内(这里是1.5秒)逐步改变属性直到最终值。在最后一个补间里,我们使用delay控制参数,让之前淡出补间完成之后,再开始后一个补间动画。
注意 transition 库的操作是基于时间轴的规则。
transition.to( target, params )
随时间,返回target显示对象的动画属性的补间。最终属性值再参数table中指定了。为了自定义补间,你可以可选的使用下列非动画属性参数:
• params.time 指定 transition的时间周期(毫秒)。默认,这个值是500ms(0.5秒)
• params.transition 默认值是 easing.linear . 参看 Easing 来获取更多信息。
• params.delay 指定在补间动画开始前的延时 (没默认值)
• params.delta 是一个布尔值,指明是否非控制参数,被解释为最终值,或作为值的改变。默认是nil,意味着false。
• params.onStart i是一个函数或者table listener(在补间动画开始前被调用)。table Listener们必须有一个onStart方法。当被调用时,这个listener作为目标,而不是一个event。
• params.onComplete 是一个函数或者table listener,在补间动画完成后被调用,异步的。
transition.from( target, params )
和 transition.to 相似,不同在于属性值的开始值,在参数table中被指定,而最终值是在调用时相应的属性值。
这个 transition 库,消除了手动操作动画对象的必要性。比较下面两个例子代码,都是让一个白色矩形在一秒钟淡出:
The transition library eliminates the need for manually animating objects. Compare the following code that causes a white rectangle to fade out in 1 second:
Manual animation
Using transition
local rect = display.newRect(
0, 0, 100, 100 )
rect:setFillColor(255,255,255)
local tMax =1000+system.getTimer()
local function fadeOut(event)
local t = event.time
local rect = event.target
if t < tMax then
rect.alpha = 1 - t/tMax
else
rect.alpha = 0
-- 完成,则删除 listener
Runtime:removeEventListener ("enterFrame", fadeOut )
end
end
-- Add listener to begin animation
Runtime:addEventListener(
"enterFrame", fadeOut )
local rect = display.newRect(
0, 0, 100, 100 )
rect:setFillColor(255,255,255)
transition.to(
rect, {time=1000, alpha=0})
transition.cancel( tween )
取消这个补间动画。
transition.dissolve( src, dst, duration, delayDuration )
Easing
Easing 库是一个中值函数的集合,被 transition库使用:
easing.linear( t, tMax, start, delta )
easing.inQuad( t, tMax, start, delta )
easing.outQuad( t, tMax, start, delta )
easing.inOutQuad( t, tMax, start, delta )
easing.inExpo( t, tMax, start, delta )
easing.outExpo( t, tMax, start, delta )
easing.inOutExpo( t, tMax, start, delta )
Y你可以创建你自己的easing函数,来在开始和最终值之间,生成中间值。这个函数必须具备下列参数:
t
从 transition 开始的瞬间,到现在经历过的时间
tMax
transition所用的时间周期
start
开始时的值
delta
改变的变化量。(最终值 = start + delta)
Movieclips
扩展的 movieclip库允许你从image序列,创建sprite动画(有时候叫“movieclips”),它可以使用和其他任何Corona display object同样的奇数,在屏幕上移动。有一些函数可以用来播放这些动画帧,或者这些帧当中的部分序列;比如向前或向后播放;跳到指定帧;向前或向后跳过帧;在序列完成后自动删除动画;让动画可以被拖放,通过press,drag和release事件来完成。
这个框架提供一个快速轻量级的方法,来创建动画,并有助于移植现有的flash内容到Corona中来。
这个 movieclip库是一个扩展模块,比需要在你的工程里包括 movieclip.lua文件,并且使用require命令。(参看 Loading external libraries 来获取关于require的细节)
注意,这个库是基于旧的sprite库。这个库的名字被改成了“movieclip”,因为“sprite”这个名字被sprite-sheet特性所保留,参看 Corona Game Edition. 后面讨论两者的不同。
使用 movieclip 的范例工程,见 Graphics/Movieclip project,或在 Ansca Mobile网站上代码。
Movieclip functions
movieclip.newAnim( frames )
用一个image文件名数组提供帧table,来创建一个动画sprite:
myAnim = movieclip.newAnim{ "img1.png", "img2.png", "img3.png", "img4.png" }
object:play()
开始向后播放一个animated sprite。当到达image列表的终点,它将会循环回来重新播放第一个image,并继续播放。如果一个特定帧序列已经被构造,调用play()而不带参数,将会简单地用那个序列恢复播放。
object:play{ startFrame=a, endFrame=b, loop=c, remove=shouldRemove }
开始向后播放一个animated sprite。当endFrame数字指定的帧到达时,它将会循环回 startFrame数字指定的帧来继续播放。
myAnim:play{ startFrame=1, endFrame=6, loop=3, remove=true }
loop参数接收一个数字,表示序列被循环的次数。0(默认值)表示永远循环下去。
The remove 参数时一个布尔标志,如果设置为true,这个 movieclip将会在指定序列播放完成后自动删除自己。这是件很有用的事,比如你要用一个爆炸动画或者一次性的动画。注意,这个object只有在你的lua代码没有任何引用指向它时,才会被垃圾手机。默认值是false。
所有参数都是可选的,但是 startFrame和 endFrame 默认被设置为第一和最后一个image上。所以显式地提供这两个值,来避免未预期的行为,也是个很好的实践。
穿参数给play()表示一个新序列将被构造,并且所有当前的序列信息将被重置。如果你只是想恢复播放当前序列,可以不带参数调用play()。
object:reverse()
反方向播放这个 animated sprite。当image集合的开始到达,它将会循环回最后的image,继续反向播放。如果一个指定的帧序列已经被构造,无参数调用reverse(),将只简单恢复向前播放这个序列。
object:reverse{ startFrame=a, endFrame=b, loop=c, remove=shouldRemove }
开始向前播放一个animated sprite。当endFrame数字指定的帧到达时,它将会循环回 startFrame数字指定的帧来继续播放。
loop参数接收一个数字,表示序列被循环的次数。0(默认值)表示永远循环下去。
The remove 参数时一个布尔标志,如果设置为true,这个 movieclip将会在指定序列播放完成后自动删除自己。这是件很有用的事,比如你要用一个爆炸动画或者一次性的动画。注意,这个object只有在你的lua代码没有任何引用指向它时,才会被垃圾手机。默认值是false。
所有参数都是可选的,但是 startFrame和 endFrame 默认被设置为最后和第一个image上。所以显式地提供这两个值,来避免未预期的行为,也是个很好的实践。
穿参数给reverse()表示一个新序列将被构造,并且所有当前的序列信息将被重置。如果你只是想恢复播放当前序列,可以不带参数调用reverse()。
object:nextFrame()
在进程中重置任何动画序列,移动动画到整个序列中下一个image,并停止。这可以被一个2帧movieclip使用,来使一个image在2个状态间切换。
object:previousFrame()
在进程中重置任何动画序列,移动动画到整个序列中前一个image,并停止。
object:stop()
在当前帧中,停止sprite动画。
object:stopAtFrame( frame )
把动画跳到指定帧,给一个帧的数字编号,或者一个可选的帧标签(参看后面的 setLabels函数)。
注意:从一个特定帧实现“go to and play”,可以简单发出一个 stopAtFrame命令,之后跟一个play或reverse命令。两个命令都将在显示更新前执行,所以这个变换将会立刻开始:
myAnim:stopAtFrame(4)
myAnim:play()
再比如:
myAnim:stopAtFrame("label")
myAnim:reverse()
object:setLabels( labels )
为之前创建的Anim对象添加一个可选的标签,用一个table来附加标签名到选好的帧号上:(firstLabel和anotherLabel是任意字符串)
myAnim:setLabels{ firstLabel=1, anotherLabel=3 }
object:setDrag
当 drag 被设置为true,任何 movieclip 就变成了一个可拖放的对象。 limitX和 limitY参数限制是否可以在x和y轴方向上拖动,还有bounds参数可以用来指定对象的拖放范围 {left, top, width, height}。onPress和onDrag,还有onRelease参数都需要一个当那些事件发生后会调用的函数名。所有参数都是可选的。
myAnim:setDrag{ drag=true, limitX=false, limitY=false, onPress=myPressFunction, onDrag=myDragFunction,
onRelease=myReleaseFunction, bounds={ 10, 10, 200, 50 }}
为了再关闭可拖放属性,设置"drag" to false:
myAnim:setDrag{ drag=false }
注意上面的例子,大括号是lua中的简略用法,就是直接通过一个名值组合的table穿参数给函数。换句话说,这个函数实际上收到一个单值,实际就是一个lua table,比如:funtion({values})。这个简略用法允许小括号被省略,以提高代码的可读性。
movieclips 和 sprite sheets的区别
Corona Game Edition 包括了一个 “sprite sheet”特性,即通过从内存中一个大纹理图片中拷贝小的矩形区块,来构造animated sprite。
Sprite sheets对纹理内存的使用是非常高效率的,因为无论源图片有多大尺寸,OpenGL-ES为所有纹理分配的内存大小,都是其向上到2的N次方对齐的。例如,一个大小80X300的单个图片,将需要一个128X512像素的纹理内存的分配。
因此, sprite sheets被推荐用于制作复杂的角色动画,或者任何有大量动画状态的程序。
然而, sprite sheets需要更多编码和更多设置;例如,你必须构造一个动画帧的很大的sheet。 当然movieclip库更容易使用,也可以更快速的移植flash内容,因为 movieclip的帧可以从flash中,作为PNG序列被导出。
为获取更多关于 sprite sheets的信息,参看 Game Edition: Sprite Sheets.
自定义/可编程动画
通常你需要创建你自己的动画,这时候可能没法使用transition库。这时候你就不得不取编写自定义的代码来产生动画序列,这就是常说的可编程动画。
为了创建这样的动画,你需要随时间变化而改变屏幕上的内容。某些情况下,在例如一个for或while循环中修改对象属性,这是很自然的。然而在Corona里,你不可能用这样的循环生产动画,因为在你的代码块执行完成之前,屏幕是绝对不会刷新的(参看 Screen Updates)。
相反,动画的产生,是借助重复的调用listener。这些listener修改屏幕上的display object,然后退出,然后允许屏幕刷新。这样的listener例如“enterFrame”,你可以注册你的listener到“enterFrame”事件上。在绘图循环中,“enterFrame”事件在屏幕刷新之前被分发,使得你的代码有机会修改屏幕上的内容(参见 Drawing Cycle)。通过“enterFrame”事件,你可以产生各种动画。事实上,transition库也是用这些事件构建的。
下面是一个如何制作弹球动画的例子:
listener函数animate每当一次“enterFrame”事件发生就会被调用一次。它负责在小球碰到屏幕边缘的时候,让其反弹以及改变其位置。
因为“enterFrame”事件发生在全局级别,所以你需要向全局运行时对象注册listener。
帧频
"enterFrame"事件发生有一个固定的时间间隔,这就是我们说的帧频。所以你的listener将会在每个帧频时间开始时被调用。然而,如果你的listener消耗太久的时间而没有退出的话,那么实际帧频将小于所需的帧频。
基于时间 vs 基于帧
在上面的例子中,动画是基于帧的方式。如果实际帧频变慢下来,因为中间帧的渲染也是在每次事件调用发生的,所以小球会看起来移动的越来越慢;也没有中间帧会跳过。如果你打算让声音和动画同步,这样就变得有点糟糕了。
解决这个问题的方案是采用基于时间的动画。通过计算我们listener每次被调用之间的时间,和适当的改变速度,我们把上面的例子,改为基于时间的方式。这将导致下面的改变:
注意我们是如何利用“enterFrame”事件包含一个存储毫秒数的参数属性的。我们通过之前保存的时间来确定小球应该运动到多远处。另外,我们上一个例子中的x,y方向的速度(2.8和2.2)显式的假设是用帧来测量的。默认情况下,它会被设置成30fps,或33.3毫秒。所以我们可以用原来的速度(30/1000)来获得新的基于时间的速度。
Lost or Missing Time
看上去,基于时间的动画有一个问题,就是当设备挂起app以后,你需要计算“丢失”的时间。在模拟器中,你可以模拟挂起,通过使用键盘上的 ⌘↓ (command-down arrow),这表示硬件挂起/恢复你的app。(注意你需要知道,通过菜单导致一个app挂起,就好像被挂起但是并不产生挂起事件)
在弹球的动画里,如果app被挂起半秒钟,球就会看起来象在屏幕上跳跃一样。在上面的例子里,解决的方案就是调节 tPrevious,把丢失的时间算上。