使用 Easing Function 製作動畫

在製作遊戲的時候,動畫是不可或缺的一部份,即使是與遊戲核心無關的 GUI 部份,若少了動畫就顯得粗製濫造。然而大多數的 GUI 效果:像是視窗飛進畫面中央、按下按鈕時放大效果之類的動畫等等,其內容都有很高的相似性,也就是「在某段時間內,把物件的某些狀態轉移到另一個狀態」。若要使用 3D Max 之類的軟體一一製作,將顯得麻煩而沒有效率。

在這篇文章中,我會以 Unity 作為範例,介紹如何實作一個簡單的 easing function 元件。

範例需求

想像一下我們正在製作遊戲過程中的暫停選單。當玩家按下暫停鈕時,畫面上會出現暫停的面板以及三個按鈕。當然,直接讓它們出現是很粗糙的作法,因此我們希望面板及按鈕可以從畫面外飛進來。

使用線性內插

最簡單的呈現方式是使用線性內插法,指定好某個物件的起始狀態(位置、大小、顏色等)與結束狀態,再指定動畫時間。只要有了這些資訊,我們可以用簡單的數學運算內插出動畫播放時每一格 frame 的物件狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using UnityEngine;
using System;
public class EasingDemo : MonoBehaviour {
public Vector3 destination;
public float duration = 1;
public float delay = 0;
private Vector3 source;
private Vector3 delta;
private float elapsed = 0;
void Start()
{
source = transform.position;
delta = destination - source;
}
void Update()
{
elapsed += Time.deltaTime;
if(elapsed > delay && elapsed < (delay+duration)){
float ratio = Math.Min(1, (elapsed-delay)/duration);
transform.position = source + ratio*delta;
}
}
}

這個簡單的元件讓我們可以在 Unity 編輯器中直接設定物件的起始位置、結束位置及動畫播放的時間。接下來,我們把面板與按鈕放在鏡頭外面,然後把它的結束位置設定在畫面內,按下 play 後馬上可以看到它的效果......非常地普通 XD

這個動畫之所以看起來很無聊,大部份的原因在於我們使用了很無聊的線性內插法。想像一下如果這個按鈕是用以下的方式飛進畫面:

  • 很快地衝進來,然後緊急剎車停在目的地。
  • 衝過頭然後拉回目的地。
  • 撞到目的地後像皮球般反彈,最後再落回目的地。

這樣的動畫,顯然會比死板的等速度飛入更活潑、更有動感。然而製作這種動畫效果就沒辦法用線性內插了。當然,若使用 3D Max 就可以藉由貝茲曲線 (Bézier curve) 來編輯這種較複雜的動畫,或是直接用 Unity 的動畫編輯器也能辦到。然而編輯貝玆曲線的各個控制點卻是一件很花時間的工作,尤其像第三個例子,為了達到反彈的效果,我們得額外加入兩三個 key frame 才行。過程並不是很難,但就是很花時間。

使用 Unity 動畫編輯器製作反彈動畫

使用非線性內插法

我們不希望動畫是平板的線性內插法,但又覺得編輯貝茲曲線的控制點太麻煩,那麼答案就呼之欲出了:使用其它的數學曲線來代替直線。比如說如下的曲線:

f(x)=(x-1)^3+1

儘管物理上並不正確,但它看起來就像是「很快地衝進來,然後緊急剎車」的樣子。因此我們可以把它套用到內插計算式中:

1
2
3
4
5
6
7
8
9
void Update()
{
elapsed += Time.deltaTime;
if(elapsed > delay && elapsed < (delay+duration)){
float x = Math.Min(1, (elapsed-delay)/duration);
float ratio = (float)Math.Pow(x-1, 3) + 1;
transform.position = source + ratio*delta;
}
}

結果如下,同樣都是一秒鐘的動畫,感覺起來是不是有微妙的差異呢?

Easing Function 慣例格式

Easing function 具有一項優勢:只要抽換不同的 easing function,不需要另外編輯 key frame 或是曲線控制點,就馬上可以得到另一種動畫。但為了達到這個「可抽換」的目的,我們得要使用大家所慣用的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Easing {
public static float Linear(float t, float b, float c, float d)
{
return c*(t/d) + b;
}
public static float OutCubic(float t, float b, float c, float d)
{
t /= d;
t--;
return c*(t*t*t+1)+b;
}
}

一般化的 easing function 有四個參數:

  • t (time): 代表動畫開始播放到現在所經過的時間。
  • b (beginning): 代表某項屬性的初始值。
  • c (change): 代表到動畫結束時,該屬性的變化值。亦即動畫結束後,這項屬性的值應為 b+c。
  • d (duration): 代表整段動畫的播放時間。

因此我們可以改寫原本的 Update()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public delegate float EasingFunction(float t, float b, float c, float d);
EasingFunction easing = new EasingFunction(Easing.OutCubic);
void Update()
{
elapsed += Time.deltaTime;
if(elapsed > delay && elapsed < (delay+duration)){
Vector3 p = new Vector3();
float t = elapsed - delay;
p.x = easing(t, source.x, delta.x, duration);
p.y = easing(t, source.y, delta.y, duration);
p.z = easing(t, source.z, delta.z, duration);
transform.position = p;
}
}

這麼一來,我們就可以利用別人寫好的 easing function 套用到我們的動畫元件中。事不遲疑,馬上就來看看範例。

彈跳動畫

上述「像是皮球般地彈跳」顯然是一種很困難的曲線,但早就有人寫好了它的 easing function,只要 google 一下馬上就能找到如下的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static float Bounce(float t, float b, float c, float d)
{
if ((t /= d) < (1.0f / 2.75)) {
return c * (7.5625f * t * t) + b;
}
else if (t < (2.0f / 2.75)) {
return c * (7.5625f * (t-=(1.5f/2.75f)) * t + 0.75f) + b;
}
else if (t < (2.5f / 2.75)) {
return c * (7.5625f * (t-=(2.25f/2.75f)) * t + 0.9375f) + b;
}
else {
return c * (7.5625f * (t-=(2.625f/2.75f)) * t + 0.984375f) + b;
}
}

然後我們把 EasingDemo 中的 easing function 設定為 Easing.Bounce,馬上就能看到彈跳的面板與按鈕:

這樣的效果是不是比前面慢慢飛進來的動畫要生動許多呢?

結語及參考資源

只要指定起始狀態、結束狀態、播放時間及 easing function 的種類,即可組合出千變萬化的各種效果,因此 easing function 是相當廣泛的 2D 動畫技術。知名的 JavaScript 函式庫 jQuery 也內建了許多不同種類的 easing function,可以在 這個網頁 看到它們的效果。

Tim Groleau 的 easing function 產生器 則是一個幫你產生程式碼的工具。你可以藉由拖拉控制點的方式編輯出你想要的曲線形式,然後旁邊就會出現這個 easing function 的 JavaScript 程式碼。這個工具也事先寫好了許多常用的 easing function 讓你可以直接利用。

最後,這篇教學的 Unity 範例場景可以在 這邊 下載。只要開一個新的 Unity 專案並選擇匯入套件,即可看到範例程式碼及實際的動畫效果。

分享到 評論