[Unity 教學] 加速開發!Figma + Rive 無縫整合 UI 動畫高效工作流

2024.10.20 / Unity 引擎
Figma 是目前非常熱門的 UI 設計工具,但其中的動畫效果則難以導入到 Unity 裡面,需要工程師在 Unity 中想辦法重現在 Figma 中實現的效果。而最近的一款新工具「Rive」號稱能無縫整合成品到 Unity,Rive 提供了完整的狀態機等動畫設計工具和完整的 Unity SDK,並且支援匯入現有的 Figma 或 Adobe illustrator、Inkscape 向量圖,讓動畫製作的工作能回歸專業。Rive 的出現,加速了遊戲的 UI 動畫開發效率,工程師不再需要煩惱如何重現效果,實現 Figma + Rive + Unity 的高效率工作流!

在 Figma + Rive + Unity 高效工作流之中,得先清楚理解這 3 個工具分別的定位:

名稱說明
Figma設計 UI 元素雛形
Rive導入在 Figma 製作的雛形,接續設計 UI 動畫
Unity導入在 Rive 製作的動畫,整合至遊戲中

Figma 的功能非常強大,熟練的設計師甚至能用它來設計絢麗的動畫,但是在我們的工作流之中,Figma 僅負責 UI 介面的基礎設計,不做任何的陰影、動畫等額外效果,最後再用向量圖的方式輸出到 Rive 中。另外,Rive 現階段還不支援向量圖夾帶點陣圖的功能,因此不能在向量圖中夾帶任何點陣圖片。

輸出 Figma 設計到 Rive

在開始之前,我們得先使用 Figma 製作一個 Start 按鈕,你可以自己做一個或拿我的 StartButtonUI 範本 來練習。

製作好後,請選取 Start 按鈕,點擊滑鼠右鍵 > Copy/Past as > Copy as SVG,將按鈕以 SVG 格式複製到剪貼簿中。

接下來打開 Rive,建立一個新的 Rive 專案,並新增一個 300x150 的 Artboard (中譯為工作區域,請重新命名為 StartButtonArtboard),再點擊鍵盤 Ctrl + V,將我們的 Start 按鈕以 SVG 格式匯入到 Rive 中。

切換到 Assets,在 Images 中可找到我們從剪貼簿匯入的檔案,預設名稱為 pasted,我通常會重新命名為 StartButton 來讓它比較好辨識。

現在我們可以拖曳 StartButton 到 Rive 中間的畫布,並稍微調整一下位置,讓他保持在工作區域的中央。

最後,切換回 Hierarchy,可以看到我們用 Figma 設計的按鈕已順利匯入到 Rive 中,下一步開始設計按鈕動畫。

使用 Rive 製作按鈕動畫

Rive 提供了完整的有限狀態機功能,就像 Unity 的 Animator 和 Animation 一樣,可以透過多個參數來控制動畫在狀態機之間的轉換。

建立狀態機與動畫時間軸

首先要來建立最基本的狀態機和時間軸,請先切換到 Animate 模式,再新增 1 個 State Machine 和 3 個 Timeline,並分別命名為:

名稱類型說明
ButtonStateState Machine按鈕的狀態機
NormalTimeline按鈕的一般狀態
HoverTimeline滑鼠放在按鈕上的狀態
DownTimeline滑鼠按下按鈕的狀態

建立動畫控制參數

新增 2 個 Boolean 的 Inputs,用來告訴狀態機現在滑鼠和按鈕之間的狀態,分別命名為:

名稱類型說明
IsPointerHoverBoolean滑鼠是否放在按鈕上
IsPointerDownBoolean滑鼠是否按下按鈕

接下來選取 StartButton (StartButtonArtboard 的子物件),新增 4 個 Listeners,將 Rive 內建的滑鼠事件功能連結到狀態機裡面,並分別命名與設定參數為:

名稱事件設定值
PointerEnterPointer EnterIsPointerHover = true
PointerExitPointer ExitIsPointerHover = false, IsPointerDown = false
PointerDownPointer DownIsPointerDown = true
PointerUpPointer UpIsPointerDown = false

拖曳 Normal、Hover 與 Down Timeline 到狀態機畫布中,並參考下圖來連接:

再參考下圖分別設定各個動畫過渡的 Conditions,並統一設定 Duration 為 200 毫秒。

最後分別設定 Normal、Hover 與 Down Timeline 的動畫,在第 1 幀為按鈕設定不同的顏色變化。這 3 個 Timeline 的用途分別如下:

名稱說明
Normal按鈕的一般狀態
Hover滑鼠放在按鈕上的狀態
Down滑鼠按下按鈕的狀態

可以試著點擊上方的播放按鈕,動動滑鼠來觀察按鈕和滑鼠互動的效果。到這裡已完成按鈕的動畫,下一步要來輸出到 Unity。

輸出 Rive 動畫到 Unity

首先,展開左上角選單 > Export > For runtime,將動畫設計匯出成 .riv 格式的檔案,並將 .riv 檔直接儲存到 Unity 專案中。Unity 無法直接存取 .riv 檔,因此下一步驟要在 Unity 中安裝 Rive 的 SDK。

導入 Rive SDK 到 Unity

開啟 Unity 專案,開啟頂部選單 Window > Package Manager。

點擊左上角的 + > Install package from git URL。

輸入以下網址並點擊 Install 來導入 v0.1.69 版本的 Rive SDK。

https://github.com/rive-app/rive-unity.git?path=package#v0.1.69

完成導入後即可關閉 Package Manager。

建立 Unity C# 腳本整合 Rive 動畫

進入重頭戲,Rive 雖然提供了 SDK,也包含了完整的 API 來讓工程師整合,但目前沒有提供開箱即用的軟體元件 (Component)。因此苦了工程師,還是得再寫一些腳本來控制它,但是和過去的做法相比,只需要理解這點程式還是減輕了很多工作的!

整合 Rive 動畫和按鈕事件

首先,我們得先順利渲染 .riv 檔到 Unity UI 上,Rive 需要解析 .riv 檔後,再將影像渲染到 RenderTexture 中,我們再透過 RawImage 來讓它顯示出來。而玩家和 UI 互動的程式則可以保留 Unity 原本的寫法,例如用 IPointerEnterHandler 等介面來接收滑鼠在 RawImage 上的各種事件。

保留 Unity 原本寫法的好處是可以減少一些學習 Rive 整合的技術門檻,但相對的就得重複實作滑鼠事件和動畫控制參數之間的設定。

請新增一個 MonoBehaviour 腳本,命名為 StartButtonRenderer.cs,並貼上以下程式碼:

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.UI;

[RequireComponent(typeof(RawImage))]
public class StartButtonRenderer : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler
{
    // RIV 檔案
    public Rive.Asset Asset;

    // 按鈕點擊事件 (當按鈕被點擊時觸發)
    public UnityEvent OnClick;

    // Rive 專案、工作區域、狀態機
    private Rive.File _file;
    private Rive.Artboard _artboard;
    private Rive.StateMachine _stateMachine;

    // Rive 動畫控制參數
    private Rive.SMIBool _isPointerHover;
    private Rive.SMIBool _isPointerDown;

    // Rive 透過 RenderTexture 渲染到 RawImage
    private Rive.RenderQueue _renderQueue;
    private Rive.Renderer _renderer;
    private CommandBuffer _commandBuffer;
    private RenderTexture _renderTexture;
    private RawImage _rawImage;

    private void Start()
    {
        // FPS 鎖定為 60
        Application.targetFrameRate = 60;

        // Rive 取得工作區域與狀態機
        _file = Rive.File.Load(Asset);
        _artboard = _file.Artboard(0);
        _stateMachine = _artboard.StateMachine();

        // Rive 取得動畫控制參數
        _isPointerHover = _stateMachine.GetBool("IsPointerHover");
        _isPointerDown = _stateMachine.GetBool("IsPointerDown");

        // Rive 根據工作區域尺寸建立 RenderTexture (DirectX 11 要求 enableRandomWrite = true)
        _renderTexture = new RenderTexture(Mathf.FloorToInt(_artboard.Width), Mathf.FloorToInt(_artboard.Height), 32);
        _renderTexture.enableRandomWrite = true;

        // Rive 使用主相機來渲染
        _renderQueue = new Rive.RenderQueue(_renderTexture);
        _renderer = _renderQueue.Renderer();
        _renderer.Draw(_artboard);
        _commandBuffer = _renderer.ToCommandBuffer();
        _commandBuffer.SetRenderTarget(_renderTexture);
        _commandBuffer.ClearRenderTarget(clearDepth: true,
                                         clearColor: true,
                                         backgroundColor: Color.clear,
                                         depth: 0);
        _renderer.AddToCommandBuffer(_commandBuffer);
        Camera.main.AddCommandBuffer(CameraEvent.AfterEverything, _commandBuffer);

        // RawImage 取得 RenderTexture 並上下翻轉 (DirectX 11 坐標系影響)
        _rawImage = GetComponent<RawImage>();
        _rawImage.texture = _renderTexture;
        _rawImage.transform.localScale = new Vector3(1, -1, 1);
    }

    private void Update()
    {
        // Rive 更新狀態機時間
        _stateMachine.Advance(Time.deltaTime);
    }

    private void OnDisable()
    {
        // 釋放渲染資源
        if (Camera.main != null && _commandBuffer != null)
            Camera.main.RemoveCommandBuffer(CameraEvent.AfterEverything, _commandBuffer);
    }

    // UI 滑鼠事件 (同步 Rive 的 Listeners 設定)
    public void OnPointerEnter(PointerEventData _)
    {
        _isPointerHover.Value = true;
    }

    public void OnPointerExit(PointerEventData _)
    {
        _isPointerHover.Value = false;
        _isPointerDown.Value = false;
    }

    public void OnPointerDown(PointerEventData _)
    {
        _isPointerDown.Value = true;

        // 當按下按鈕時觸發點擊事件
        OnClick.Invoke();
    }

    public void OnPointerUp(PointerEventData _)
    {
        _isPointerDown.Value = false;
    }
}

觀察上面這串程式碼,可以看到我先用 _isPointerHover 和 _isPointerDown 兩個私有變數來將 IsPointerHover 和 IsPointerDown 動畫控制參數從狀態機調出來操作,再從 IPointerEnterHandler 系列介面實作的 OnPointerEnter 方法中按照先前在 Rive 中新增 Listeners 時使用的設定值來控制參數的 true/false。

在 Start 裡面實作的甚麼 RenderQueue、CommandBuffer 等雜七雜八的內容,猜測八成是要讓 Unity 能成功渲染 .riv 檔的一些必要操作,也沒太多需要注意的參數,只要理解這樣寫他可以正常作動就好!

建立實驗用計數器腳本

按鈕除了有動畫,按下去也是要有些反應的吧?所以我們在偵測滑鼠按下的 OnPointerDown 這邊觸發了公開的 OnClick 事件,這樣就可以讓這個 Start 按鈕動起來。

為了觀察按鈕有被點擊的效果,我們來實作個簡單的計數器。再新增一個 MonoBehaviour 腳本,命名為 Counter,並貼上以下程式碼:

using TMPro;
using UnityEngine;

[RequireComponent(typeof(TMP_Text))]
public class Counter : MonoBehaviour
{
    private TMP_Text _tmpText;
    private int _count;

    private void Start()
    {
        _tmpText = GetComponent<TMP_Text>();
        _count = 0;
        UpdateText();
    }

    private void UpdateText() => _tmpText.text = _count.ToString();

    public void Increase()
    {
        _count++;
        UpdateText();
    }
}

這段腳本很單純的做了 1 個呼叫 Increase 後會讓數字加 1 的功能,並更新到 TextMeshPro 的文字上。

最後,請在場景上分別建立 1 個尺寸為 300x150 的 RawImage (命名為 StartButton) 和 TextMeshPro 的 Text (命名為 CountText),再將 StartButtonRenderer 腳本附加到 StartButton,並把專案中的 .riv 檔指派到公開的 Asset 欄位上。

而 Counter 腳本則附加到 CountText,再回到 StartButton,新增一個 OnClick 事件的監聽者,讓他觸發 CountText 上的 Counter 的 Increase 方法。

執行與觀察結果

最後執行看看,滑鼠放上和按下按鈕時理應都會正確顯示我們在 Rive 裡面製作的動畫,並且在點擊時會正確的讓上面的數字加 1。

當初找到 Rive 這工具時真的是又驚又喜,一看到官網上的「With Rive, complex designer-developer handoff is a thing of the past. Reduce development times by empowering designers to build functional graphics with rich interactivity and animation.」就決定一定要趕快騰出時間來驗證它是否這麼神。

實際實驗過後,使用 Rive 目前最大的問題應該會在無法匯入含有點陣圖的向量圖這邊,有些之前做好的設計就只能分批輸出,其它的狀態機、時間軸等功能就只是需要時間熟悉。Rive 的動畫工具比 Unity 內建的 Animator 好用太多了,有被 Animator 坑過的工程師應該都能很輕易的上手。

我認為 Rive 能很好承接 Figma 的設計和可無縫整合 Unity 的特性,讓它很有納入高生產力工作流程的潛力。目前從初次探究到大略摸熟的時間約半天,如果你有興趣請馬上來試試看,想實際操作看看最終效果的也可以來參考我的 GitHub。

在這篇文章中實作的 Figma、Rive 和 Unity 專案分別公開在這裡,歡迎拿來練習!

參考資料

相關文章

Ted Liou

雲科碩士在讀中,專注於 Unity C#、TouchDesigner 技術,常把技術筆記分享到部落格,偶爾還直接挪用文章來當教材的研究生。