iOS游戏开发:COCOS2D-X动作CCActionEase

文函数图像使用GeoGebra绘制,感谢它才华横溢的作者。

为了方便用户灵活地控制精灵运动,cocos2d-x提供了CCActionEase类系的动作。它们拥有相似的名字——CCEaseXxxxIn、CCEaseXxxxOut、CCEaseXxxxInOut,同时也拥有相似的行为——速度由慢至快、速度由快至慢、速度先由慢至快再由快至慢。但是除了这些,我们对CCActionEase一无所知。就算查阅参考手册,我们能得到的信息也不过是类似Ease Sine In的简短说明。它们究竟是什么模样,我们该如何选择?

今天我们就来解决这个问题。鉴于CCActionEase类系的庞大,文章可能会分成两到三篇。

1)CCEaseSineIn

在《cocos2d-x动作系统浅析》一文中提到:
update函数接受一个百分比参数,它表示动作的完成进度。update根据这个百分比将目标对象做出相应的调整。
可以说这个update函数就是CCActionEase的灵魂。

1 void CCEaseSineIn::update(ccTime time)
2 {
3     m_pOther->update(-1 * cosf(time * (float)M_PI_2) + 1);
4 }

之前我们已经知道CCActionEase类系的动作就是调整其他动作的速度,变换出新的效果。这里的m_pOther就是那个被影响的动作,而一切魔力的源头就在它接受的参数上。CCEaseSineIn将传入的百分比参数进行了一系列变换,然后传给了m_pOther。

我们将这个变换公式提取出来,记作:
f(x)=1-cos(π/2*x) x∈[0,1]
这个就是已用时间百分比与实际完成进度的关系。在匀速运动中,它们应该是相等的,但是在变速运动中,它们的关系就会变幻莫测。

上图中的黑色曲线就是f(x)的函数图像。它的定义域从0开始,到1结束,值域也是这样。根据这条线的走势,可以粗略看出速度是越变越快的,但还是不够形象。
在运动学中,物体的位移对于时间的导数就是物体的瞬时速度。如果我们能得到这条瞬时速度的曲线,那就直观多了。上面的函数f(x)是已用时间百分比与实际完成进度的关系,这里可以近似地理解为时间与路程的关系。
所以我们对f(x)求导,得出:
f'(x)=π/2*sin(π/2*x) x∈[0,1]
它对应图中那条红色曲线。可以很明显地看出,速度越变越快,在C点达到了最高。
正如它名字说的那样,它的速度由慢至快,呈正弦变化。

2)CCEaseSineOut

我们再来看下CCEaseSineOut类。

1 void CCEaseSineOut::update(ccTime time)
2 {
3     m_pOther->update(sinf(time * (float)M_PI_2));
4 }

同理得出:
f(x)=sin(π/2*x) x∈[0,1]
f'(x)=π/2*cos(π/2*x)

同样我们更关注那条红色曲线,它从最高点C出发,一路下降到达A点。这表明在CCEaseSineOut动作中,速度是越来越慢的,它的图像也呈正弦变化。

3)CCEaseSineInOut

我们知道CCEaseXxxxInOut的速度变化是先由慢至快,再由快至慢。如果我们将上面两个图像拼在一起,然后在将横轴比例缩小一倍,那结果就是这条曲线的模样了。
一般情况下,我们需要将函数分成两段,第一段在0到0.5之间,第二段在0.5到1之间。我们来看看CCEaseSineInOut是如何实现的。

1 void CCEaseSineInOut::update(ccTime time)
2 {
3     m_pOther->update(-0.5f * (cosf((float)M_PI * time) - 1));
4 }

f(x)=-0.5*(cos(π*x)-1) x∈[0,1]
f'(x)=π/2*sin(π*x)

在CCEaseSineInOut中,这两段曲线正好是同一个函数(非分段函数)的图像。很巧妙是不是?
图中红色曲线从原点O出发,一路上升到达最高点C,然后又一路下滑降至D点。它同样也是一条正弦变化的曲线。动作的速度看起来就是由慢至快,再由快至慢的。

小结

CCEaseSineIn、CCEaseSineOut、CCEaseSineInOut这三个动作同属速度正弦变化,变化的范围是[0,π/2]。

4)CCEaseExponentialIn

有了前面的经验,后面就容易多了,先来看一下CCEaseExponentialIn的update函数。

1 void CCEaseExponentialIn::update(ccTime time)
2 {
3     m_pOther->update(time == 0 ? 0 : powf(2, 10 * (time/1 - 1)) - 1 * 0.001f);
4 }

大家可能已经注意到,这里使用了一个条件运算符,于是表达式变作了分段函数。
当x=0时,f(x)=0
当x∈(0,1]时,f(x)=2^(10*(x-1))-0.001

注意这条不是速度的曲线。
上面副绘图区中的图像就是这个函数的整体走势,我们在主绘图区给原点附近的曲线一个特写。可以看到,除了x=0的情况,曲线与x轴还有一个交点。
对2^(10*(x-1))-0.001=0求解,得出:
x=1-ln(1000)/(10*ln(2))=0.00342

现在我们开始在脑中想象一下精灵按照CCEaseExponentialIn动作移动的详细步骤。
首先,时间从零开始,精灵被设置到起始位置。这一步是正常的没有问题。
接下来,精灵猛地朝着反方向跳动了很小的一段距离。这个距离是非常非常小的,也就是图上的B点附近,大约只占整个移动距离的0.00234%
然后,精灵开始以变化的速度朝着目标点移动。经过点A时精灵回到初始位置。这时,我们设计的运动才刚刚开始。
如果我们将x=1代入公式,可以推算出:
f(x)=1-0.001=0.999
也就是说,图像最终没有到达终点,而是差了一小段距离。

简单来说,总时间的前0.342%部分以及最终的那一瞬间的运动是不太正常的。
如果你设计了一个超过1000秒的运动,那么前3秒内,精灵的准确位置不会在你设计的轨迹上。
当然如果想观察到这个问题,运动的距离也是一个关键。
假设你疯狂地设计了一个运动10万像素的精灵,并且运动时间超过1000秒,那你就能观察到这一现象了。3秒钟,反向2个像素。

但是为什么会这样呢?是引擎的bug吗?
确切来说,这应该算不上是bug,这只是精度引起的问题。

下面这段都是我自己的推测,也就是猜到,大家看看就好了。
我猜测这个公式的最初原型应该是:
f(x)=2^(10*(x-1)) x∈[0,1]
但是它有一个问题,那就是当x=0的时候,f(0)=1/1024
时间为零的时候,精灵大约就已经有了千分之一的位移,而且是在一个物体运动刚开始的时候,猛然地跳动是非常明显的。所以设计者将千分之一的误差移动到了末尾,也就是运动要结束的时候。
那公式现在的样子就是:
f(x)=2^(10*(x-1))-1/1024 x∈[0,1]

大家都知道cocos2d-x多使用单精度浮点型数字,以及写0.0009765625f比较麻烦等诸多因素,最后这个公式就简化成了现在的模样。

我的猜想说完了,我们接着来求导:
f'(x)=10*2^(10*(x-1))*ln(2) x∈[0,1]

按照最理想的那个公式绘制出图像,这里我们只看那条红色的曲线。这条曲线从D点开始一路上升,迅速到达C点。如果你对它再次求导,就能得出其加速度的变化规律。从DC曲线上应该可以看出其加速度也是越来越大的。
额,说得有点儿远了。我们把注意力先集中起来,计算出速度的最小值和最大值。
f'(0)=10*2^(-10)*ln(2)=0.006769
f'(1)=10*2^0*ln(2)=6.931472

CCEaseExponentialIn的速度由慢至快,从0.006769上升至6.931472,呈指数级变化。

5)CCEaseExponentialOut

1 void CCEaseExponentialOut::update(ccTime time)
2 {
3     m_pOther->update(time == 1 ? 1 : (-powf(2, -10 * time / 1) + 1));
4 }

CCEaseExponentialOut与CCEaseExponentialIn的实现是相似的,唯一的不同是CCEaseExponentialOut在最后一瞬间会有短距离的跳跃(千分之一的误差),而CCEaseExponentialIn是舍弃部分。个人认为CCEaseExponentialOut的处理方式更合理些。
好了直接上图

这里没有难点,我直接让工具生成的导函数图像。
我们关心的是A点(0,6.93147)和D点(1,0.00677),与CCEaseExponentialIn的速度范围是一样的。从6.93147下降至0.00677,速度为由快至慢的指数变化。

6)CCEaseExponentialInOut

在《知易游戏开发教程cocos2d-x移植版003》中有一段CCEaseExponentialInOut的演示代码,测试运行时会发现精灵最后以极快的速度飞出了屏幕,是笔者使用不当,还是别的什么原因?当时由于时间、精力的问题没有深入研究,今天借此机会将问题分析一下。

1 void CCEaseExponentialInOut::update(ccTime time) 

2{ 

3 time /= 0.5f; 

4 if (time < 1) 

5 { 

6 time = 0.5f * powf(2, 10 * (time - 1)); 

7 } 

8 else 

9 {

10 time = 0.5f * (-powf(2, 10 * (time - 1)) + 2);

11 }

12 

13 m_pOther->update(time);

14 }

呵呵,典型的分段函数。绘制函数图像如下:

图中这条蓝色的曲线就是CCEaseExponentialInOut使用的分段函数。很明显可以看到在A点处,曲线走向发生了90°的变化,向着点(1,-511)延伸。它没有像前面说过的函数那样逼近点C(1,1),这就解释了为什么精灵莫名其妙地飞出了屏幕。

这是一个bug,我们希望曲线的后半段能像那条绿色的曲线AC那样。(我只在Win32平台上测试的,不知其他平台上是否也存在这个问题,有兴趣的朋友可以测试下。)

我的修改如下:

 1 void CCEaseExponentialInOut::update(ccTime time)

2 { 

3time /= 0.5f; 

4 if (time < 1) 

5 { 

6 time = 0.5f * powf(2, 10 * (time - 1)); 

7 } 

8 else 

9 {

10 // 将(time - 1)变作(1 - time) 

11 time = 0.5f * (-powf(2, 10 * (1 - time)) + 2);

12 }

13 

14 m_pOther->update(time);

15 }

修正后,动作的行为正常了。
对新的函数求导,得出图中的红色曲线。其中点D、点E、点F的坐标分别为(0.5,6.93147)、(0,0.00677)、(1,0.00677)。
细心的朋友可能已经发现了点C没有到达(1,1)。是的,这里存在0.000488的误差,曲线的起始点也一样。即原来1/1024的误差被平分到了开头和末尾。

小结

CCEaseExponentialIn、CCEaseExponentialOut、CCEaseExponentialInOut这三个动作同属速度指数级变化,变化的范围是[0.00677,6.93147]。

相比之前的速度正弦变化动作(这个东西叫什么更好一些?渐变动画?)与速度指数级变化动作,CCEaseIn/CCEaseOut/CCEaseInOut更具灵活性。你可以设置运动的速率,甚至是在运动的过程中改变速率。它们拥有共同的基类——CCEaseRateAction。不要直接使用CCEaseRateAction,因为它没有实现任何变化效果。

7)CCEaseIn

按照惯例贴出update函数的源代码,以免版本更新导致文不对题。

1 void CCEaseIn::update(ccTime time)
2 {
3     m_pOther->update(powf(time, m_fRate));
4 }

根据此函数的实现推导出以下三个公式:

s(t)=t^r t∈[0,1]
v(t)=s'(t)=r*t^(r-1) t∈[0,1]
a(t)=v'(t)=r*(r-1)*t^(r-2) t∈[0,1]

s:路程 v:速度 a:加速度 t:时间 r:速率参数m_fRate
都是很基本的导函数推导,如果有不清楚的地方,建议先复习下导数。

下面我们着重分析下速率参数,也就是r的取值范围。因为是讨论二元函数,图像是三维的,画出后重叠在一起反而不利于理解,这里就没有作图。

1.r<0

当r<0时,v(t)将始终保持为负数,也就是说速度的方向与设定的方向正好相反。
又因为s(1)=1,这说明精灵最后移动到了目标点坐标,但它是从预设轨迹的延长线上反向移动到这一坐标点的。
这是一个很奇怪的动作行为,跟我们预想的设计不符,所以r的取值不应小于零。

2.r=0

当r=0时,s(t)恒等于1,也就说动作在开始之前就已经达到了完成的状态。我们这里说的是动作的表现,动作原本的执行时间是不受此影响的。
很明显,这与我们的设计不符,所以零也不是r的一个取值。

3.0<r<1

当r的取值范围在(0,1)时,a(t)恒为负数,加速度为负说明速度是越来越慢的,这与CCEaseXxxxIn动作应该由慢至快的设定不符,所以这也不是r应有的取值范围。

4.r=1

当r=1时,a(t)恒等于零,v(t)恒等于一,这说明此时的动作是速度为1的匀速运动。这貌似与设定的有慢至快也不相符。

5.1<r<2

当r的取值范围在(1,2)时,加速度a(t)恒大于零,但呈下降趋势。

6.r=2

当r=2是,a(t)恒等于2,也就是此时为匀加速运动。

7.r>2

当r>2时,加速度a(t)恒大于零,且呈上升趋势。

综上所述,当你使用CCEaseIn时,传入的速率参数应大于1.0f,并且根据取值范围的不同,会呈现出3种不太一样的加速运动。

8)CCEaseOut

我们再来看一下CCEaseOut的update函数。

1 void CCEaseOut::update(ccTime time)
2 {
3     m_pOther->update(powf(time, 1 / m_fRate));
4 }

第一步还是需要推导出路程、速度、加速度的公式:

s(t)=t^(1/r) t∈[0,1]
v(t)=s'(t)=1/r*(t^(1/r-1)) t∈[0,1]
a(t)=v'(t)=1/r*(1/r-1)*(t^(1/r-2)) t∈[0,1]

第二步分析r的取值范围:

1.r<0

当r<0时,CCEaseOut的情况与CCEaseIn一样,运动不在预设轨迹上,排除。

2.r=0

除数不能为零,排除。

3.0<r<1

当r的取值范围在(0,1)时,a(t)恒大于零,这说明速度是越来越快的,这与CCEaseXxxxOut由快至慢的设定不符,排除。

4.r=1

当r=1时,a(t)恒等于零,这说明是匀速运动,不符合设定,排除。

5.r>1

当r>1时,加速度a(t)恒小于零,但呈上升趋势。

综上所述,当你使用CCEaseOut时,传入的速率参数应大于1.0f,与CCEaseIn不同的是,CCEaseOut只有一类加速度变化趋势。

在继续后面的研究之前,我们来做一些额外的思考。CCEaseOut中r的取值为什么与CCEaseIn的有些不一样呢?是不是它的设计存在什么不合理的地方?

假设我们将r设定为3,同时画出CCEaseIn和CCEaseOut的图像:

颜色有点儿乱,不过没办法,里面包含3套对比数据,我直接说颜色,希望大家别迷糊。

那条红色曲线是CCEaseIn的v(t)函数,那条分数的是CCEaseOut的v(t)函数。
大家都知道CCEaseXxxxIn与CCEaseXxxxOut动作的区别是,前者由慢到快,后者由快到慢。如果说得更精确些,它们的表现应该是对称的——速度函数v(t)按照x=0.5直线轴对称。
但是显而易见的,红色曲线和粉色曲线根本不对称。

如果大家再仔细想想之前正弦变化和指数级变化的图像,这里还存在一处对称。那就是,路程函数s(t)的图像应该是按照点A(0.5,0.5)中心对称的。
那条蓝色曲线是CCEaseIn的s(t)函数,那条兰色的是CCEaseOut的s(t)函数。
可以很明显地看出,它们是按照y=x直线轴对称的,而不是按照点A中心对称。

我们将红色曲线按照x=0.5直线做轴对称镜像,得到那条黑色的抛物线。再对此抛物线求其反导函数图像,得到那条黑色曲线。瞧,它与那条蓝色曲线是不是按照点A中心对称的。
所以,我认为CCEaseOut的update函数应该修改一下。如果你有不同的见解,欢迎在评论区留言。

附上我修改后的代码:

1 void CCEaseOut::update(ccTime time)
2 {
3     m_pOther->update(1.0f - powf((1.0f - time), m_fRate));
4 }

如果按我这样修改,那么速率参数的取值范围与CCEaseIn中是一样的,大于一。

9)CCEaseInOut

好了,继续我们的研究:

 1 void CCEaseInOut::update(ccTime time)

2 { 

3int sign = 1; 

4 int r = (int) m_fRate; 

5 

6 if (r % 2 == 0) 

7 { 

8 sign = -1; 

9 }

10 

11 time *= 2;

12 if (time < 1)

13 {

14 m_pOther->update(0.5f * powf(time, m_fRate));

15 }

16 else 

17 {

18 m_pOther->update(sign * 0.5f * (powf(time - 2, m_fRate) + sign * 2));

19 }

20 }

必须指出,这个函数的实现是有问题的。

第一,难道引擎的设计者只希望我们传入整数型的速率吗?
问题出在powf函数调用上。
我们知道传入的time参数范围在[0,1],即便中间做了一次乘2的操作,到了后面time-2依然是小于等于零的。所以在某些情况下,powf会出现问题。
比如,当m_fRate设置为3.5f之类的小数时,这里的powf会返回"-1.#IND000"。
于是,当动作执行到后半段时,精灵会消失,直到动作全部完成,精灵才会出现在终点上。

第二,那个sign正负标志是用来解决问题的吗?怎么感觉引入后反而将问题复杂化了。
这里其实只需将前半段的函数按照点(0.5,0.5)做一次中点对称就可以了,用不着这么麻烦。
我修改的代码如下:

 1 void CCEaseInOut::update(ccTime time)

2 { 

3time *= 2; 

4 if (time < 1) 

5 { 

6 m_pOther->update(0.5f * powf(time, m_fRate)); 

7 } 

8 else 

9 {

10 m_pOther->update(0.5f * (2.0f - powf((2.0f - time), m_fRate)));

11 }

12 }

因为CCEaseInOut与CCEaseIn使用相同的算法,所以在这里速率参数的取值范围与CCEaseIn的一样,也是大于一。

小结

到目前为止,我们对CCActionEase的学习已经完成了一半。

我们一共学习了3类,9个动作。它们分别是CCEaseSineIn、CCEaseSineOut、CCEaseSineInOut、CCEaseExponentialIn、CCEaseExponentialOut、CCEaseExponentialInOut、CCEaseIn、CCEaseOut、CCEaseInOut。

它们与之后将要学习的动作的最大区别是,在这些动作的执行过程中,精灵会严格地按照内部动作指定的路径移动,绝对不会超出起始点与终点的范围。

在CCEaseIn/CCEaseOut/CCEaseInOut中,速度的变化范围是[0,m_fRate],m_fRate>1。

但是,如果cocos2d-x需要与cocos2d-iphone从原则上保持高度一致,即便是存在缺陷也不能破坏原则的话,那么我推测官方在短时间内是不会修改这个问题的。因为,在大约6个月之前,有朋友提出过此问题,但似乎被无视了。所以,在官方正式修正此问题之前,我建议大家只使用大于1的整数作为速率的参数,以提高兼容性。

我们前面介绍的动作主要是用来改变内部动作的执行速度,接下来要介绍的这几个动作主要是用来增加表现效果的,可以看作是简单的特效。

10)CCEaseBackIn

1 void CCEaseBackIn::update(ccTime time)
2 {
3     ccTime overshoot = 1.70158f;
4     m_pOther->update(time * time * ((overshoot + 1) * time - overshoot));
5 }

前面我们已经做过很多次了,大家也一定都是轻车熟路,推导出一下公式:
s(t)=(overshoot+1)*t^3-overshoot*t^2 t∈[0,1]
v(t)=s'(t)=3*(overshoot+1)*t^2-2*overshoot*t t∈[0,1]
a(t)=v'(t)=6*(overshoot+1)*t-2*overshoot t∈[0,1]

在GeoGebra中绘制出函数图像:

请看图中的蓝色曲线。在动作开始执行后,精灵首先朝着y轴负方向运动,大约运动到y=-0.1的时候,开始按照正常的运动方向朝着点G(1,1)移动。

听起来有点儿复杂,那我给大家讲个故事。从前武状元考试,有个代号1.70158的人也来参加了,他把箭搭在弓上,然后用力向后拉,可惜力气太小了,只能把弓拉开10%,手一滑,箭就飞出去了,没想到正中靶心点G(1,1),阴差阳错的就当了武状元。

这故事讲完了,那您也一定听出来了,这CCEaseBackIn其实就是一个搭弓射箭的过程。

那这个代号1.70158的武状元是不是把弓拉开了10%呢?有兴趣的朋友可以跟着我一起算一算。

图中的红色曲线代表速度,只要它在x轴下方,那就说明这个武状元正在拉弓。如果红色曲线到了x轴上方,那就表示他把箭放出去了。所以红色曲线与x轴的交点B就是他松开手的那一刻。过点B做x轴的垂线,交蓝色曲线与C点,这个C点的纵坐标就是武状元拉弓的程度。

按照这个过程我们计算得出点B的横坐标为:
B.X=2*overshoot/(3*(overshoot+1))=0.419897
C.X=0.419897
将此值带入s(t)函数内,得出:
C.Y=s(C.X)=-0.1

因为这个动作的主要目的是实现特效,所以它的速度是多少就不是很重要了,有需要的朋友请自行计算红色曲线在[0,1]范围内的极值。

11)CCEaseBackOut

1 void CCEaseBackOut::update(ccTime time)
2 {
3     ccTime overshoot = 1.70158f;
4     time = time - 1;
5     m_pOther->update(time * time * ((overshoot + 1) * time + overshoot) + 1);
6 }

将动作CCEaseBackIn倒着播放就是CCEaseBackOut了,请看图:

12)CCEaseBackInOut

Bug #961: fix mad behaviour in second stage of CCEaseBackInOut

cocos2d-1.0.1-x-0.12.0之前的版本中,CCEaseBackInOut动作的行为有点儿问题,如果你还在使用老的版本,先去升级一下吧。

1 void CCEaseBackInOut::update(ccTime time) 

2{ 

3 ccTime overshoot = 1.70158f * 1.525f; 

4 

5 time = time * 2; 

6 if (time < 1) 

7 { 

8 m_pOther->update((time * time * ((overshoot + 1) * time - overshoot)) / 2); 

9 }

10 else 

11 {

12 time = time - 2;

13 m_pOther->update((time * time * ((overshoot + 1) * time + overshoot)) / 2 + 1);

14 }

15 }

看到这段代码的你,可能会无比的迷惑。这迷惑不是莫名而来的,自打你见到overshoot第一眼的时候,这种子就已经种在你的心里了。

Why does 1.70158 equal a 10% "bounce"?
What does 1.70158 mean?
这里的1.525又是什么鬼东西?!

看来,如果我不把这些讲清楚,今天是收不了工了。

那我就简单地讲一讲。至于对与不对,各位您可擦亮了眼睛。

还记得上面我们计算C.Y的过程吗?如果我们不把overshoot的值代进去,再推导一次。

C.X=2*overshoot/(3*(overshoot+1))
C.Y=s(C.X)
C.Y=(2*overshoot/(3*(overshoot+1)))^2*((overshoot+1)*(2*overshoot/(3*(overshoot+1)))-overshoot)
C.Y=(2*overshoot/(3*(overshoot+1)))^2*(2*overshoot/3-overshoot)
C.Y=(2*overshoot/(3*(overshoot+1)))^2*(-overshoot/3)
C.Y=-(4*overshoot^3)/(27*(overshoot+1)^2)

因为我们要找出10%对应的overshoot是多少,并且C.Y是在x轴下方,所以我们令C.Y=-0.1。

C.Y=-0.1
-(4*overshoot^3)/(27*(overshoot+1)^2)=-1/10
40*overshoot^3=27*(overshoot+1)^2
40*overshoot^3-27*overshoot^2-54*overshoot-27=0

下面就是纯数学问题了,求解一元三次方程。

经过一系列运算,得出这个一元三次方程有一个实根和两个共轭复根。这个实根就是我们想要的值——1.70154。对,你没看错,我算出来的就是1.70154。我也不知道为什么不是1.70158,难道是神奇的误差?

我们再来看看这个1.525是怎么来的。

而CCEaseBackInOut其实就是把CCEaseBackIn和CCEaseBackOut的图像缩小成一半,然后分别放入[0,0.5]和[0.5,1]区间内。因为是缩小一半,所以我们需要重新计算overshoot的值,要让原来弹出10%变成20%,这样缩小后才能保持弹出的幅度是一样的。

令C.Y=-0.2,得出一元三次方程:
20*overshoot^3-27*overshoot^2-54*overshoot-27=0

求解overshoot的值为2.59239,它正好是原来的1.70154的1.52355倍。

哎呀,糗大了,好不容易算出来两个数,跟代码里大家用的还不一样。

这到底是怎么回事?是误差的问题?还是我的算法不对?等待高人指点。

13)CCEaseBounceOut

这次我们要稍微调整一下顺序,先来介绍CCEaseBounceOut动作,因为CCEaseBounceIn是按照它的定义做的镜像,所以CCEaseBounceOut才是实现的本体。

1 void CCEaseBounceOut::update(ccTime time)
2 {
3     ccTime newT = bounceTime(time);
4     m_pOther->update(newT);
5 }

这次的变换函数独立出来了,我们跟进去看看。

1 ccTime CCEaseBounce::bounceTime(ccTime time) 

2{ 

3 if (time < 1 / 2.75) 

4 { 

5 return 7.5625f * time * time; 

6 } else 

7 if (time < 2 / 2.75) 

8 { 

9 time -= 1.5f / 2.75f;

10 return 7.5625f * time * time + 0.75f;

11 } else 

12 if(time < 2.5 / 2.75)

13 {

14 time -= 2.25f / 2.75f;

15 return 7.5625f * time * time + 0.9375f;

16 }

17 

18 time -= 2.625f / 2.75f;

19 return 7.5625f * time * time + 0.984375f;

20 }

这回竟然换了4次计算公式,我们先把它画出来。

可能你还没有看出这是什么,为了让大家都能看明白,我们把它按照y=0.5做一次轴对称镜像。

现在都看出来了吗?

对了,CCEaseBounceOut动作就是模拟的小球掉落的弹跳运动。

这里小球一共弹起了3次。在第3次落地后,小球终于没有足够的力气再跳起来了。

14)CCEaseBounceIn

前面我们说过CCEaseBounceIn动作其实就是按照CCEaseBounceOut的定义镜像而来的。

1 void CCEaseBounceIn::update(ccTime time)
2 {
3     ccTime newT = 1 - bounceTime(1 - time);
4     m_pOther->update(newT);
5 }

镜像的方式是按照点(0.5,0.5)做的中心对称。

倒着播放小球掉落的画面是个什么样子?或者想象一下将静止在地面上的篮球拍打起来的过程。

15)CCEaseBounceInOut

1 void CCEaseBounceInOut::update(ccTime time) 

2{ 

3 ccTime newT = 0; 

4 if (time < 0.5f) 

5 { 

6 time = time * 2; 

7 newT = (1 - bounceTime(1 - time)) * 0.5f; 

8 } 

9 else 

10 {

11 newT = bounceTime(time * 2 - 1) * 0.5f + 0.5f;

12 }

13 

14 m_pOther->update(newT);

15 }

CCEaseXxxxInOut动作的实现永远都是最没意思的,无非就是把CCEaseXxxxIn和CCEaseXxxxOut缩小一半,然后再拼在一起。

小结

今天一共介绍了两类动作,第一类是在模拟弹射运动,第二类是在模拟小球掉落之类的弹跳运动。它们主要不是为了修改内部动作的速度,而是为其增加特殊的显示效果。

由于篇幅的限制,Ease Elastic类动作将放到下次介绍。

如果你去查看参考手册,在这三类动作的描述中,会看到这样的警告:

This action doesn't use a bijective fucntion. Actions like Sequence might have an unexpected result when used with this action.

所以不要把CCSequence之类的动作传递给它们作参数,但是你可以把它们传递给CCSequence来创建动作队列。

 

在上一篇中我们介绍了两种反弹效果动作——Ease Back和Ease Bounce,今天我们将要介绍第三种反弹效果动作——Ease Elastic。

那么这个Elastic是什么呢?

维基百科上说,Elastic是对弹性体或者可伸缩纤维的口语说法,或者是指小孩子玩的跳皮筋游戏。

下面我们就研究一下这个Elastic和皮筋有什么关系。

16)CCEaseElasticIn

要研究动作的行为,就离不开它的update函数。

 1 void CCEaseElasticIn::update(ccTime time)

2 { 

3ccTime newT = 0; 

4 if (time == 0 || time == 1) 

5 { 

6 newT = time; 

7 } 

8 else 

9 {

10 float s = m_fPeriod / 4;

11 time = time - 1;

12 newT = -powf(2, 10 * time) * sinf((time - s) * M_PI_X_2 / m_fPeriod);

13 }

14 

15 m_pOther->update(newT);

16 }

这是一个分段函数,两端的时间点被独立出来了。先抛开这两点不管,我们看主要的变换代码。

首先看到:
time = time - 1;
这其实就是做了一次以x=0.5为轴的轴对称。

为了方便我们将time - 1记作u,m_fPeriod记作p,那么变换公式如下:
u=t-1 t∈(0,1)
s(u)=-1*2^(10u)*sin((u-p/4)*2π/p) u∈(-1,0)
s(u)=-1*2^(10u)*sin(2πu/p-π/2) u∈(-1,0)
s(u)=2^(10u)*sin(π/2-2πu/p) u∈(-1,0)
s(u)=2^(10u)*cos(2πu/p) u∈(-1,0)

大家都知道余弦函数是以2π为周期的,这里公式中有一个除以p的操作,这说明cos(2πu/p)的周期为p。
又因为u∈(-1,0),1/1024<2^(10u)<1,这说明2^(10u)与cos(2πu/p)相乘最多影响函数曲线的幅度,绝不会改变原来的周期。
所以s(u)的函数周期就是p。

因为这是一个分段函数,所以可能存在的最大问题就是,在实际运动过程中,精灵的动作可能不是连续的,即(0,0)和(1,1)两点不在函数曲线上。所以如果你希望让精灵完美地运动,你就需要小心地选取这个周期参数。

如何选取周期参数呢?

假设我们有这样一个函数:
f(u)=cos(2πu/p)

我们希望它能满足以下条件:
1.f(0)=1
2.f(1)=0

也就是说,我们希望点(0,f(0))落在图中点A的位置,而点(1,f(1))落在图中点B、C、D、E、F这样的位置上。

条件一是时刻满足的,我们只要针对条件二来确定p就可以了。于是我们得出等式:

2π/p=π/2+N*π N≥0
p=4/(2*N+1) N≥0

于是我们可以得出,比较合适的p的取值有:4、4/3、4/5、4/7。。。

我们选取p=4/7,绘出完整函数图像,看看是什么样子的。

这是一条振荡曲线,或者叫做波动曲线,它描述的是精灵运动的幅度。

用什么来形容这个动作呢?我也说不好。地震监测仪上的那支笔?也许吧。

17)CCEaseElasticOut

 1 void CCEaseElasticOut::update(ccTime time)

2 { 

3ccTime newT = 0; 

4 if (time == 0 || time == 1) 

5 { 

6 newT = time; 

7 } 

8 else 

9 {

10 float s = m_fPeriod / 4;

11 newT = powf(2, -10 * time) * sinf((time - s) * M_PI_X_2 / m_fPeriod) + 1;

12 }

13 

14 m_pOther->update(newT);

15 }

将CCEaseElasticIn的图像按照点(0.5,0.5)中心对称镜像,得到的就是CCEaseElasticOut的图像。
但是,如果你直接将中心对称公式套入CCEaseElasticIn的函数中,推导出的并不是上面CCEaseElasticOut使用的计算公式。不过不用特别担心,如果再考虑上正弦函数的性质,你会发现他们是相等的。

18)CCEaseElasticInOut

这是最后一个CCActionEase类动作,胜利在望,加把劲。

1 void CCEaseElasticInOut::update(ccTime time) 

2{ 

3 ccTime newT = 0; 

4 if (time == 0 || time == 1) 

5 { 

6 newT = time; 

7 } 

8 else 

9 {

10 time = time * 2;

11 if (! m_fPeriod)

12 {

13 m_fPeriod = 0.3f * 1.5f;

14 }

15 

16 ccTime s = m_fPeriod / 4;

17 

18 time = time - 1;

19 if (time < 0)

20 {

21 newT = -0.5f * powf(2, 10 * time) * sinf((time -s) * M_PI_X_2 / m_fPeriod);

22 }

23 else 

24 {

25 newT = powf(2, -10 * time) * sinf((time - s) * M_PI_X_2 / m_fPeriod) * 0.5f + 1;

26 }

27 }

28 

29 m_pOther->update(newT);

30 }

在这段代码中,首先将time扩大一倍,然后在减去一,也就是将时间分成了前后两段。
接着对前后两段时间分别套用相应的公式,进行变换。

基本都挺正常的,除了下面这一段:

11 if (! m_fPeriod)12 {13     m_fPeriod = 0.3f * 1.5f;14 }

这个周期参数百分之百会被赋值,而且正常人也不会传入一个零作周期。我实在是看不出来这段代码有什么用途,所以还得请高人赐教。

小结

至此CCActionEase类系的动作就都介绍完了。其中最头痛的就是这个Ease Elastic类的动作,要把这类动作所做的运动描述清楚,感觉是挺费劲的。

到底该怎么更好的描述Ease Elastic类的动作呢?还得让大家一起动动脑子。
这类的动作用在游戏里什么地方最合适?谁想到了就积极的留言吧。

最后,总结一下本节的重点:
1.如果fPeriod的值越小,那么精灵弹的次数就越多。
2.如果想要精灵完美运动,即起始点(0,0)、结束点(1,1)以及连接点(0.5,0.5)都在函数曲线上,那么fPeriod的值应该符合下面的公式:
fPeriod=4/(2*N+1) N≥0
3.在逻辑上fPeriod的值要大于零,在实际应用中还要受float类型精度的限制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值