FlutterでRiveを使ってアニメーションをつける

はろはろ、題名通りだよ。
忘れないための個人的備忘録です。
※間違っているところがあると思うので参考にする人は注意してね!※
※適当に書いちゃったから誤字脱字があるよ※

さて、現在ゲームを作ったことがないからという理由でオセロを作っている。
基本的な動作(石をおく、挟まれるとひっくり返る、相手の動作)は一日でぱぱぱーっとつくってしまったのだけれど、表示するのがTextの”〇”と”●”ではいささかしょぼいので、ちょっとアニメーションを追加することにした。
(そしてこれがハマりへの入り口であった・・・)

FlutterでRiveを使用する方法はいろいろなところに転がっているので、それを参考にするとよい。

・・・のだが、どうも私の思う挙動をしてくれないので、メモ書きである。

まず、アニメーション設定だが、RiveAnimation.assetを使用すればいったんアニメーションは表示される。
最低限必要な項目は
 「asset(rivファイルのファイル名)」
  そして
 「Animations(動かすアニメーション※動かし方の指示ができない)」
  あるいは
 「Controllers(動かすアニメーション※動かし方の指示ができる)」
 である。
※ここで記述しているのは私の感覚上の話であるので、本当は違うかもしれない。

オセロに必要な動きとしては
 ・石をおく(白・黒)
 ・石がひっくり返る(白→黒、黒→白)
であるが、まぁ、石を置く動作はひっくり返る動画を流しても見栄えはそんなに変わらんだろということで、アニメーションとしては
 ・白→黒
 ・黒→白 
になるアニメーションの二つを準備した。
※Riveは便利なのだけれど、出力するアニメーションに「これはRiveでつくったよ!」っていう文字が入るようになったのでしょんぼりである。
 (買えばいいのはわかるが、私は貧乏である)

2023年末に透かしが廃止されたようです。ヤッタゼ!

さてさて、では作ってみよう。
まず、それぞれのエリア用にRiveAnimation.assetを準備する。
石の置かれていないときは何も表示しないので、三項演算子とやらを使って制御する。
石が置かれている状態を保持している要素、(「w_b」としておく)を見て判断する。
こんな感じだよ

Container(
child: w_b == 0 
  ? Text('')
  : RiveAnimation('asset/douba.riv',
      controllers[wToB,bToW],
    ),
),

w_bが0の時は何もない状態、それ以外の時(1,2)は、石が置いてある状態として、アニメーションを表示するように指定している。
なおControllersの状態は以下の感じ

RiveAnimationController wToB = OneShotAnimation('wToB');
RiveAnimationController bTow = OneShotAnimation('bToW');

Controllerにアニメーションをセットする方法はいろいろあるようですが、
今回はアニメーションを一回動かしたら止めたいので「OneShotAnimation
」を利用しました。
で、ここで一点問題が発生します。

動画「白→黒へひっくり返る」があります。
これをOneShotAnimationを利用して実行すると、、、
再生が終わった後の状態が「白」になるのです。

これはOneShotAnimationの
「動画を一回流し終わったら最初のフレームに戻って表示」
という処理が原因でした。

こちらの対応としてはOneShotAnimationのクラスをオーバーライド(だっけ?extends するw)して、自分用のクラスを作ることでした。
OneShotAnimationのonActiveChangedという処理の中で、
「動画が終了したら、初期状態に戻す」
という処理があるので、そちらを回避すれば問題なしです
こんな感じ

class OneShotCustomAnimation extends OneShotAnimation{

  OneShotCustomAnimation(
      String animationName,{
        double mix = 1,
        bool autoplay = true,
  }) : super(animationName,mix: mix,autoplay: autoplay);

  @override
  void onActiveChanged() {
    if(isActive) {
      super.onActiveChanged();
    }
  }
}

・・・と、思っていました、
これを回避するとどうなるか。

1回目:白→黒へひっくり返る動画が流れる
2回目:ひっくり返される(黒→白へひっくり返る動画が流れる)
3回目:黒い石が表示されるだけ
4回目:白い石が表示されるだけ
・・・と、なるのです。
勘が良ければすぐわかるでしょう。
OneShotAnimationの動きとしては
「動画を流す→流し終わったら最初に戻しておく」
であったのです、この
「流し終わったら最初に戻しておく」
をなくしたので、動画の状態は一番最後の状態で止まっています。
それを次回使用するとどうなるか、
「動画を流す(最後の状態から)」
なのだ。
要は、最後のフレーム(黒、白)だけが表示されて、アニメーションが表示されない。

この対応は簡単です。
「動画を流す」だけではなく、
「最初の状態に戻す→動画を流す」
と、動きを逆転させればよいのです。
こうするだけ。

class OneShotCustomAnimation extends OneShotAnimation{

  OneShotCustomAnimation(
      String animationName,{
        double mix = 1,
        bool autoplay = true,
  }) : super(animationName,mix: mix,autoplay: autoplay);

  @override
  void onActiveChanged() {
    if(isActive) {
      reset();
      super.onActiveChanged();
    }
  }
}

「reset();」を追加するだけでOKです。
楽勝!いいぇえええ!と思ったのもつかの間次の問題が頭をもたげます。
最初に動かす動画が決まっていない、、、!
これがなかなかに面倒なのです、
Controllersにアニメーションをセットしました。
もし、どちらのアニメーションを動かすのか決まっていれば、

RiveAnimationController wToB = OneShotAnimation('wToB',autoplay:false);
RiveAnimationController bToW = OneShotAnimation('bToW',autoPlay:true);

このように記述すれば、autoplay=falseは再生されず、autoplay=trueは自動で再生されるので、RiveAnimationが表示された瞬間にはautoplay=trueがセットされた要素が自動で動き出すのです。

しかし、しかしですよ。
オセロは白を置く人と、黒を置く人がいるわけで、最初に白が表示されるアニメーションと黒が表示されるアニメーションがじゅんばんにやってくるわけで、、このやり方ではよくないのです。

と、いうことで、どちらのautoplayもfalseとして、表示された瞬間にisActive=true(アニメーション再生)を実行すりゃいいんじゃねぇかとやっているんですが、どうもうまくいかない。
どちらの動画も動かずに、動画内で利用する要素が全表示した画像が表示される、、、

考えられることはRiveAnimationが表示された「瞬間」には、アニメーション情報が読み込み切れていないので、そこで「再生!」という命令を下しても、それをキャッチできていないのではないかという。。。
(ちなみに、最初の石を置くアニメーションは動かないが、それ以降、ひっくり返されたときのアニメーションは動くので、やはり、RiveAnimationの初期処理がらみではないかと、、、、、)

じゃぁ、これをどうやって対応するのか?と言われるとそれが私の今なんですよ!助けてだれか!w
(まぁ、事前にアニメーションをロードしておけば、、、という感じなのですが、そのロード操作をどうやるのかなぁーってのを今から調べるのです。)

手法としてはrootBundle.loadとやらを利用して(どっかで見た)
事前ロードを実装するか、
あるいは別の何かがあり得るのか、、
多分、、事前にRoadAnimatonを作って持っていてもロードが開始されるのが「その要素が表示された瞬間から」っぽいので、それを回避するすべがあるならば、、init時点で全要素を作り上げておけば、まぁ、いけるんではなかろうかという思惑があるが、、、、うーん。。。
三項演算氏じゃなくてvisibilityで制御したら裏側にあるからいけるか?
いっそのこと何もない動画を作ってその三つを表示しているという風にするのが一番簡単なのでは、、、?

結果どうなった?

Riveアニメーションに何もない動画を一つ追加して、それを初期値としました。
ひゅー♪こんなんでいけるんかー♪
なお、敵側の挙動は完全ランダムのはずなのにちょくちょく敗北をきたす私はオセロレベル相当低い気がする。

一応書いとく
initState()あるいは、オセロリセット処理の中で以下のように記述する。

  void reset() {
    //board:各マスの状態を保有している。何も無し:0 白:playerW(2) 黒:playerB(1)
    //全boardに0をセット
    board = List.generate(8, (_) => List<int>.filled(8, 0));
    //最初の白黒セット
    board[3][3] = playerW;
    board[4][4] = playerW;
    board[3][4] = playerB;
    board[4][3] = playerB;
    //次のプレイヤーが何色かを保有させる(最初は白)
    currentPlayer = playerW;
    //それぞれのマスのアニメーションのセット
    for(int a = 0;a<64;a++){
      //白になるアニメーションのAnimationController
      riveActionsW.add(OneShotCustomAnimation('round_w',autoplay: false));
      //黒になるアニメーションのAnimationController
      riveActionsB.add(OneShotCustomAnimation('round_b',autoplay: false));
      //何もないアニメーションのAnimationController
      riveActionsN.add(OneShotCustomAnimation('nothing',autoplay: true));
      //RiveAnimationにセット
      riveAnimation.add(RiveAnimation.asset('assets/othero.riv',controllers: [riveActionsN[a],riveActionsB[a],riveActionsW[a]],));
      //多分なくても良いけど一応
      riveActionsN[a].isActive = true;
    }
//ちょっとうまく動いてない、、、
    rootBundle.load('assets/othero.riv').then(
        (ByteData data) async{
          setState(() {
            riveActionsB[28].isActive = true;
            riveActionsB[35].isActive = true;
            riveActionsW[27].isActive = true;
            riveActionsW[36].isActive = true;
          });
        }
    );
}

で、ひっくり返った時は、こう!

//黒になるとき
riveActionsB[index].isActive = true;

//白になるとき
riveActionsW[index].isActive = true;

indexは、該当マスに紐づけられる番号が入るんですが、これは、、、作り方によるので割愛しますよ。
ちなみにRiveはどうなっとるかと言いますと、、

round_w

round_w
黒→白のアニメーション
round_b
白→黒のアニメーション
nothing
何もない状態

こんな感じですね。
ちょっと雑すぎたかなぁ、と思って追記しました。

ではでは。

この記事が気に入ったらサポートをしてみませんか?