CCActionEase想說愛你也不難(上)


尊重作者勞動,轉載時請標明文章出處。
作者: Bugs Bunny
地址: http://www.cnblogs.com/cocos2d-x/archive/2012/03/13/2393898.html

本文函數圖像使用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 {
3 time /= 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]。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM