flutter – 라이브(Rive) 정리

개요

Rive는 애니메이션과 상호작용을 디자인하고, 이를 다양한 플랫폼(앱, 웹, 게임 등)에 실시간으로 구현할 수 있도록 지원하는 인터랙티브 그래픽 툴입니다. Rive를 사용하면 디자이너와 개발자가 하나의 툴에서 협업하며, 정적 애니메이션에서 상호작용 애니메이션까지 손쉽게 구현할 수 있습니다. 특히 Rive는 애니메이션을 효과적으로 관리하고, 런타임에서 반응형 애니메이션을 생성하도록 최적화된 기능을 제공합니다.

상태 머신(State Machine)

상태 머신은 Rive에서 애니메이션 간의 전환과 상호작용을 구성하는 기본적인 시스템입니다. 상태 머신을 통해 서로 다른 애니메이션을 연결하고, 트리거 및 조건에 따라 애니메이션이 어떻게 전환되는지를 설정할 수 있습니다. 이를 통해 버튼의 클릭 애니메이션이나 캐릭터의 상태 변화 등 다양한 상호작용을 구현할 수 있습니다.

상태 머신의 사용 예시

상태 머신을 활용하면 다음과 같은 다양한 상호작용 애니메이션을 구현할 수 있습니다:

  • 버튼 상호작용: 클릭이나 호버 시 버튼이 변하거나 눌림 애니메이션이 실행되도록 상태 머신을 구성할 수 있습니다.
  • 캐릭터 동작 전환: 게임 캐릭터가 달리기, 걷기, 대기 등의 상태 간에 자연스럽게 전환되도록 설정할 수 있습니다.
  • 스크롤 애니메이션: 사용자의 스크롤에 따라 특정 그래픽이 변하는 애니메이션을 구현할 수 있습니다.

상태(States)

상태(States)는 상태 머신에서 재생될 수 있는 개별 애니메이션을 의미하며, 각 상태는 특정 시점에서 애니메이션이 어떤 상태에 있는지를 정의합니다. 상태를 통해 애니메이션이 어떤 모습으로 표시될지를 설정하고, 이를 트리거나 조건에 따라 변경할 수 있습니다.

상태의 유형

Rive의 상태 머신에서 사용할 수 있는 주요 상태 유형은 다음과 같습니다:

  • 기본 상태(Default State): 기본적으로 모든 상태 머신에는 기본 상태가 포함됩니다. 이 상태는 애니메이션이 시작되는 지점으로 설정되며, 초기 상태와 종료 상태가 포함되어 있습니다.
  • 단일 애니메이션(Single Animation): 하나의 애니메이션을 재생하는 상태로, 주로 특정 이벤트(예: 버튼 클릭, 호버)에서 재생되는 간단한 애니메이션을 구현할 때 사용합니다. 예를 들어, 버튼이 눌릴 때 색이 변경되는 애니메이션을 단일 애니메이션 상태로 설정할 수 있습니다.
  • 블렌드 상태(Blend State): 여러 애니메이션을 혼합하여 부드러운 전환 효과를 구현할 수 있는 상태입니다. 1D 블렌드와 Additive 블렌드 상태가 있으며, 캐릭터의 다양한 동작이나 스크롤과 같은 연속적인 상호작용이 필요한 경우에 유용합니다. 예를 들어, 캐릭터의 표정이나 자세를 부드럽게 전환할 때 블렌드 상태를 사용할 수 있습니다.

전환(Transitions)

상태 머신 내에서 한 상태에서 다른 상태로 이동하는 경로를 설정하며, 상태 머신이 상호작용을 통해 애니메이션을 변화시키는 논리적 흐름을 정의하는 요소입니다. 이를 통해 버튼 클릭 시 애니메이션이 변하거나, 캐릭터가 상태에 따라 동작을 바꾸는 등 다양한 시나리오를 구현할 수 있습니다.

입력(Inputs)

입력(Inputs)은 상태 머신에서 상태 전환을 제어하는 조건을 설정하는 요소로, 입력을 통해 사용자의 상호작용(예: 클릭, 호버)이나 특정 조건에 따라 상태 머신의 애니메이션이 어떻게 반응할지를 정의할 수 있습니다.

입력 유형

Rive의 상태 머신에서는 세 가지 주요 입력 유형을 지원합니다:

  • 불리언(Boolean): 불리언 입력은 true 또는 false 값을 가지며, 상태 머신의 특정 상태 전환 조건으로 사용됩니다. 예를 들어, 스위치나 체크박스와 같은 간단한 논리 제어에 활용할 수 있습니다. 불리언 값이 true일 때 버튼이 눌리는 애니메이션을 실행하는 식으로 활용할 수 있습니다.
  • 트리거(Trigger): 트리거는 불리언과 유사하지만, 짧은 시간 동안만 true가 되었다가 자동으로 초기화됩니다. 특정 이벤트(예: 총 발사, 점프 동작 등)에서 한 번만 실행되어야 하는 애니메이션을 제어할 때 유용합니다. 예를 들어, 버튼을 한 번 클릭할 때마다 애니메이션이 일회성으로 재생되도록 설정할 수 있습니다.
  • 숫자(Number): 숫자 입력은 특정 정수 값을 가질 수 있으며, 다양한 조건에 따라 상태 머신을 제어할 때 매우 유연하게 사용할 수 있습니다. 진행 상황을 추적하거나 캐릭터의 스킨과 같은 상태를 변경할 때 유용합니다. 예를 들어, 숫자 입력 값이 3 이상일 때 특정 상태로 전환하는 식으로 설정할 수 있습니다.

입력의 사용 및 설정

입력을 생성하려면 입력 패널에서 플러스(+) 버튼을 눌러 원하는 입력 유형을 선택합니다. 생성된 입력은 상태 머신의 조건으로 설정되어, 특정 입력 값에 따라 전환이 발생하도록 구성할 수 있습니다.

  • 불리언 조건 설정: 불리언 값을 true 또는 false로 설정하여 전환이 발생할 시점을 지정합니다.
  • 숫자 조건 설정: 숫자 입력은 특정 값과 같거나, 크거나 작은지에 따라 전환을 설정할 수 있습니다.
  • 트리거 설정: 트리거는 특정 이벤트가 발생할 때마다 전환이 실행되도록 설정할 수 있습니다

리스너(Listeners)

리스너(Listeners)는 상태 머신 내에서 특정 사용자 동작(예: 클릭, 호버, 마우스 이동)을 감지하여 상태 머신의 입력을 변경하거나 애니메이션을 트리거할 수 있는 도구입니다.

리스너의 주요 구성 요소

리스너는 세 가지 주요 구성 요소로 이루어져 있습니다:

  1. 타겟(Target): 리스너가 사용자 동작을 감지할 위치로, 마치 히트박스처럼 특정 영역을 정의합니다. 타겟으로 지정된 아트보드나 객체는 해당 영역 내에서 사용자의 상호작용을 감지하게 됩니다.
  2. 사용자 동작(User Action): 리스너가 감지할 사용자 상호작용 유형입니다. Rive에서는 다양한 사용자 동작을 지원하며, 사용자의 상호작용에 따라 특정 상태로 전환하거나 애니메이션이 실행되도록 설정할 수 있습니다.
    • 포인터 다운(Pointer Down): 마우스 클릭이나 터치스크린 장치의 손가락 눌림.
    • 포인터 업(Pointer Up): 마우스 클릭 해제 또는 손가락 떼기.
    • 포인터 진입(Pointer Enter): 마우스나 손가락이 타겟 영역에 진입할 때.
    • 포인터 나가기(Pointer Exit): 마우스나 손가락이 타겟 영역에서 나갈 때.
    • 마우스 이동(Mouse Move): 타겟 영역 내에서 마우스 또는 손가락의 움직임.
    • 클릭(Click): 타겟 영역 내에서 포인터 다운과 포인터 업이 모두 발생할 때.
    • 이벤트 청취(Listen For Event): 특정 아트보드의 이벤트가 발생할 때 청취하여 해당 이벤트가 실행되도록 설정합니다.
  3. 리스너 동작(Listener Action): 리스너가 지정된 사용자 동작이 발생할 때 수행하는 작업입니다. 리스너는 세 가지 동작 유형을 통해 상태 머신과의 상호작용을 제어할 수 있습니다.
    • 입력 변경(Input Change): 리스너가 특정 입력 값을 변경하여 애니메이션 전환을 유도합니다. 예를 들어, 불리언 값을 변경하거나 트리거를 활성화하여 클릭이나 호버와 같은 상호작용을 바로 반영할 수 있습니다.
    • 이벤트 보고(Report Event): 특정 사용자 상호작용이 발생할 때 이벤트를 보고하는 기능으로, 개발자가 런타임 중에 이벤트를 감지하거나 다른 아트보드와 상호작용을 연결할 수 있습니다.
    • 타겟 정렬(Align Target): 마우스 커서 위치에 맞춰 타겟 객체를 정렬하여, 예를 들어 마우스 따라다니는 아바타 효과를 구현할 수 있습니다.

레이어(Layers)

레이어(Layers)는 Rive 상태 머신에서 한 번에 하나의 애니메이션만 재생할 수 있는 구조로, 다양한 애니메이션을 혼합하거나 추가 상호작용을 추가할 수 있도록 도와줍니다.

Flutter 적용 예제

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  Artboard? _riveArtboard;
  SMIBool? _hoverInput;
  SMITrigger? _clickTriggerInput;

  @override
  void initState() {
    super.initState();

    _loadRiveFile();
  }

  Future<void> _loadRiveFile() async {
    final riveFile = await RiveFile.asset('assets/riv_test.riv');

    final artboard = riveFile.mainArtboard.instance();
    var controller = StateMachineController.fromArtboard(artboard, 'State Machine 1');
    if (controller != null) {
      artboard.addController(controller);
      _hoverInput = controller.getBoolInput('Btn Over');
      _clickTriggerInput = controller.getTriggerInput('Btn Click');
    }
    setState(() => _riveArtboard = artboard);
  }

  @override
  Widget build(BuildContext context) {
    if(_riveArtboard == null) {
      return const SizedBox();
    }

    return Scaffold(
      body: GestureDetector(
        onTap: () {
          _clickTriggerInput?.change(true);
          Future.delayed(const Duration(milliseconds: 300), () {
            _clickTriggerInput?.value = false;
          });
        },
        onDoubleTap: () => _hoverInput?.value = true,
        onLongPress: () => _hoverInput?.value = false,
        child: Rive(
          fit: BoxFit.fitWidth,
          artboard: _riveArtboard!,
        ),
      ),
    );
  }
}

학습 자료

https://rive.app/community/doc

https://pub.dev/packages/rive

https://github.com/rive-app/rive-flutter

https://pub.dev/documentation/rive/latest