AnimationManager 2015 winter update

  • 2
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

以前Gistとして公開したヘッダライブラリ「AnimationManager」(https://gist.github.com/Pctg-x8/91951196fb4509f80941 )を、記事のネタを作るという体で更新しました。
リポジトリはこちら: https://github.com/Pctg-x8/AnimationManager
ダウンロードはこっち: https://raw.githubusercontent.com/Pctg-x8/AnimationManager/master/AnimationManager2015w.h

対応バージョン

AnimationManagerおよびAnimationManager 2015 winterは、Siv3D(January 2015)およびSiv3D(June 2015 v2)で動作を確認できております。というよりもEasing以外にSiv3Dの機能を使っていないため、Easingが実装されているバージョンであればどれでも動作するはずです。

ライセンス

Gistで公開しているAnimationManagerはライセンスを明記していなかったので、今更制限を付け加えるのもアレですしパブリックドメインとします。AnimationManager 2015 winterはBoost License 1.0の元で公開されるものとします。

ライブラリについて

AnimationManagerはSiv3Dのイージング関連の機能を使いやすくするために制作されました。以下の特徴を持ちます。

1. ヘッダファイル一つに収まるほどコンパクト

Gistの段階ではヘッダファイルとして分離していませんが、まぁもともと公開予定ではなかったので(?

2. 様々なアニメーションをほぼ自由に、そして簡単に記述することができる

いわゆる「キーフレーム」を定義する形でアニメーションを記述、作成できるため、他のアニメーションソフトで制作したものもほぼそのままゲーム内で使用することが可能です。
(他ソフトから出力したファイルがそのまま使えるということではないです。念のため)

3. 実時間に即した値を使って計算を行う

「現在までのフレーム数」によるアニメーションの場合、FPS値(秒間フレーム数)が一定でないことにより必ずしも望んだ動きをしない場合があります。
そのため、ある程度精度を確保できるタイマーを用いて実際の時間で計算を行うことにより、例えば「フレームスキップ(処理が間に合わないなどで1フレーム分処理を飛ばす)」などが発生したとしても望んだ通りに動いてくれるようになります。

4. Siv3Dの機能「イージング」を用いて、コンパクトかつほぼ自由にアニメーションを作成することができる

これは1.および2.と内容がほぼ同じなのですが、イージングと連携することにより直線的でない、すこし複雑な挙動をするアニメーションを簡単に作成することができます。

新AnimationManagerについて

新しいAnimationManagerでは幾つかの点が修正されました(修正というか、ぶっちゃけ全面書き換えなので旧バージョンと互換性は全然無いです。コンセプトだけ同じ)。旧AnimationManagerとの対比を交えて紹介します。

1. 値の受け渡しがPull型になった

DataFlowGraph.png
Push型: 登録すると勝手に値を計算し、その計算した値のみを描画側から使う仕組みのことをいいます
Pull(Reactive)型: 必要な場合にのみ値を要求し、要求された瞬間に値を計算する仕組みのことをいいます

(ここではAnimationManagerに則した説明にしているので、厳密にこういう定義ではありません。たぶん)
従来のPush型の場合、たくさんのパラメータを一斉にアニメーションさせようとした場合に更新処理が全てにかかります。もしかしたら現在のシーンでは使わない(もしくは既に役目を終えた)ものもあるかもしれません。
この場合、Pull型ならば必要な値のみ計算させることが可能なので、多くの場合で負荷を軽減することができます(そこまで大きくはないですが)。
また、アップデートをいちいち指示する必要がないので、旧バージョンにあった「updateAnimation関数」は姿を消しました。

2. キーフレームを「連結」する形に変わった

旧バージョンでは時間の範囲(開始時間を終了時間)をいちいち指定してアニメーションを記述していました。この場合の問題は2つあります。

  • 継続するアニメーションを作成する場合、前のアニメーションの長さが変わったらその後ろの、すべてのアニメーションの範囲を書き換える必要がある。
  • キーフレーム間は独立して管理しているため、重なった範囲を指定してもなんともない(動作は未定義となります)。

新バージョンでは、アニメーションは「キーフレーム(Section)の連続」として記述、管理されます。

3. 柔軟な使い方

旧バージョンでは、アニメーションさせるパラメータを参照として保持していました。
新バージョンでは、アニメーションさせるパラメータもAnimationManagerクラスが生成、管理するようになります。このため、

  • パラメータをローカル変数に格納して使う(従来と似たような使い方)
  • AnimationManagerクラスが提供するValue<T>関数を使い、文字列で参照して使う

という2つの方法で使うことが可能になります。

サンプルプログラム

#include <Siv3D.hpp>
#include "AnimationManager2015w.h"

void Main()
{
    // 変数の宣言
    // perfMonitor、updateCounterはFPS計測用のやつ
    AnimationManager am;
    EventTimerSec perfMonitor;
    size_t updateCounter = 0;
    size_t lastFPS = 0;

    Window::SetTitle(L"AnimationManager Sample");
    perfMonitor.setEvent(L"FPSUpdate", 1);
    perfMonitor.start();

    // 3つのアニメーションを定義
    auto col = am.Var<ColorF>(L"col").EnableLoop().SetInit(Palette::Red)
        .AddSection(1000, Palette::Green, Easing::Linear)
        .AddSection(1000, Palette::Blue, Easing::Linear)
        .AddSection(1000, Palette::Green, Easing::Linear)
        .AddSection(1000, Palette::Red, Easing::Linear);
    // 赤から(SetInit(Palette::Red))
    // 1秒かけて緑に(AddSection(1000, Palette::Green, Easing::Linear))、
    // そして一秒かけて青に(AddSection(1000, Palette::Blue, Easing::Linear))、
    // さらに1秒かけて緑に(AddSection(1000, Palette::Green, Easing::Linear))なった後、
    // 1秒かけて赤に(AddSection(1000, Palette::Red, Easing::Linear))戻ります。
    // これを繰り返します(EnableLoop())。
    // EnableLoopはどこに置いても全体をループさせます。
    auto offsetter = am.Var<Vec2>(L"offsetter").EnableLoop(1).SetInit(Vec2(0.0, 0.0))
        .AddEmptySection(1000)
        .AddSection(500, Vec2(64.0, 64.0), Inv(Easing::Quart))
        .AddSection(500, Vec2(0.0, 128.0), Inv(Easing::Quart))
        .AddSection(500, Vec2(-64.0, 64.0), Inv(Easing::Quart))
        .AddSection(500, Vec2(0.0, 0.0), Inv(Easing::Quart));
    // AddEmptySectionは「何もしない」キーフレームを追加する関数です。
    auto metronome = am.Var<double>(L"metronome").EnableLoop().SetInit(60.0)
        .AddSection((1000 * 60) / 140.0, 120.0, Inv(Easing::Back))
        .AddSection((1000 * 60) / 140.0, 60.0, Inv(Easing::Back));
    Array<AnimatedVariable<double>> CellAlphas;
    for (int i = 0; i < Window::Width() / 8; i++)
    {
        CellAlphas.push_back(am.Var<double>(Format(L"cellAlpha#", i)).EnableLoop(1)
            .SetInit(0.0).AddEmptySection(i * 25)
            .AddSection(500, 1.0, Bipolar(Easing::Sine)).AddSection(500, 0.0, Bipolar(Easing::Sine)));
    }

    // OldSample(Gistにあるやつの移植)
    // Vec2(構造体)の要素をアニメーション
    auto rectPosX = am.Var<double>(L"rectPos.x").SetInit(0.0)
        .AddSection(2000, 400.0, Bipolar(Easing::Quad));
    auto rectPosY = am.Var<double>(L"rectPos.y").SetInit(0.0)
        .AddSection(500, 100.0, Easing::Quint)
        .AddSection(500, 0.0, Inv(Easing::Quint))
        .AddSection(500, 100.0, Easing::Quint)
        .AddSection(500, 0.0, Inv(Easing::Quint));
    auto round = am.Var<double>(L"round").SetInit(0.0)
        .AddSection(2000, 720.0, Bipolar(Easing::Quart));
    auto rectCol = am.Var<Color>(L"rectCol").SetInit(Color(255, 255, 255))
        .AddSection(1000, Color(255, 0, 0), Easing::Circ)
        .AddSection(1000, Color(0, 255, 0), Inv(Easing::Circ));

    // アニメーションを開始する
    am.Start();
    while(System::Update())
    {
        // FPS計算(1秒ごとに経過フレーム数を保存するだけ)
        perfMonitor.update();
        if (perfMonitor.onTriggered(L"FPSUpdate"))
        {
            lastFPS = updateCounter;
            updateCounter = 0;
            perfMonitor.restart();
        }

        ClearPrint();
        Println(L"FPS: ", lastFPS);
        Println(L"Controls: ");
        Println(L"  R: Restart");
        Println(L"  P: Pause");
        Println(L"  S: Start");
        if (Key('R').pressed) am.Restart();
        if (Key('P').pressed) am.Pause();
        if (Key('S').pressed) am.Start();

        // am.updateAnimation()が不要なことに注意
        // 辞書(TextureAsset,SoundAsset)っぽくアクセスできる
        Println(L"Offsetter: ", am.Value<Vec2>(L"offsetter"));

        // 関数っぽく書くと(変数名の後に()を書くと)値が取れる
        RectF(Vec2(rectPosX(), rectPosY() + 120.0), Vec2(48.0, 48.0))
            .rotatedAt(Vec2(rectPosX() + 24.0, rectPosY() + 120.0 + 24.0), Math::Radians(round())).draw(rectCol());
        RectF(Window::Center() + offsetter(), Vec2(48.0, 48.0)).draw(col());
        Line(Window::Center(), Window::Center() + (Vec2::Left * 64.0).rotated(Math::Radians(metronome()))).draw(Palette::Red);
        // ローディングアニメーションみたいの(上に出る)
        int ca_index = 0;
        for (auto& cellv : CellAlphas)
        {
            RectF(Vec2(ca_index * 8.0 + 1.0, 1.0), Vec2(6.0, 6.0)).draw(ColorF(0.0, 1.0, 0.0, Decimate<4>(cellv())));
            ca_index++;
        }
        updateCounter++;
    }
}

20151202-020051-730.gif

超簡単な利用手順

  1. AnimationManagerのインスタンスを作る
  2. パラメータを作成(パラメータ変数を得る)
  3. メソッドチェーンでアニメーションを形作る
  4. 値を取得して描画とかする

AnimationManagerクラスのメソッド

メソッド 返り値型 説明
Var<T>(const String& id) AnimatedVariable<T> パラメータを作成する
Value<T>(const String& id) T 値を取得する
Start() void アニメーションを開始する
Pause() void アニメーションを一時停止する
Reset() void アニメーションをリセットする(停止する)
Restart() void アニメーションを最初からスタートする

AnimatedVariable<T>(パラメータ変数)クラスのメソッド

// InterpolateFuncTはdouble(0から1の相対時間)を受け取ってdouble(値)を返す関数
using InterpolateFuncT = std::function<double(double)>;
メソッド 返り値型 説明
SetInit(const T& value) AnimatedVariable<T>& 初期値を設定する
AddSection(double time, const T& value, InterpolateFuncT interpF) AnimatedVariable<T>& キーフレームを追加する
AddEmptySection(double time) AnimatedVariable<T>& 何もしないキーフレームを追加する
EnableLoop(size_t loopSkips = 0) AnimatedVariable<T>& ループを指定する
DisableLoop() AnimatedVariable<T>& ループをしないようにする
IsSequenceFinished() const bool アニメーションが終了したかどうかを得る
GetFinishTime() const double アニメーション全体の時間を得る
value() T 値を取得する
operator()() T value()のショート(値を取得する)

補助関数

AddSectionにはいろいろな都合でSiv3D側の関数がそのまま使えないのと、個人的にどっちがどっちだかよく混乱することがあるので別途関数を用意してます。

関数 返り値型 説明
Inv(InterpolateFuncT f) InterpolateFuncT EaseOutのラッパ(関数を反転する)
Bipolar(InterpolateFuncT f) InterpolateFuncT EaseInOutのラッパ

set.png

欠点/改良点

改良できる点を思いつくだけ書きます(修正するとは言ってない)

1. 結局使い方次第では高負荷

同じ時間中に値を取得しても、以前取得したかどうかに関わらず計算します。もちろんこの場合は同じ値が返ってきます。
これはメモ化を実装することである程度改善されます。たぶん

2. AnimationManager::Value<T>

文字列を指定して値を取得する関数ですが、これのTにAnimationManager::Var<T>のTと違う型を渡したらどうなるの、という話です。
結論から言うと、良くて例外が発生します。最悪何も言わずに異常終了します。
Valueの中身では実際に値を要求する際にdynamic_castを使って型変換をしているのですが、dynamic_castは正しいキャストではない場合に(ここではVar側のTと違う型が渡された場合に)nullptrを返すようで、Value内ではさらにこれをごにょごにょして最終的にnullポインタ参照となります。
これについてはなんというかうまく防ぎようがないような気がしてくるので、パフォーマンス重視ならValue関数を廃止するか、あるいはdynamic_castの結果を調べて、nullならTのデフォルト値を返すようにするのが最善かなと思います。
ちなみにVar<double>で登録してValue<float>で取得するのもダメです。

3. 任意のタイミングでの(パラメータレベルでの)停止/再開ができない

UIのアニメーションとしてAnimationManagerを採用しようとする場合、「ボタンが押されたらアニメーションする」というようなものに(AnimationManager単体では)対応できません。
AnimationManagerを複数用意するか、パラメータにちょっと細工を入れればできるかなと思います。

使用リソースとか

図表フォントに下記フォントを使用しています。

この投稿は Siv3D Advent Calendar 20157日目の記事です。