LoginSignup
15
11

More than 5 years have passed since last update.

ofxCommand(ProgressionのCommandライクに書けるopenFrameworksアドオン)を作った

Last updated at Posted at 2016-12-23

こちらの記事は FLASHer Advent Calendar 2016 24日目に投稿にしようとしていた記事です(代わりの記事は こちら )。
Great thanks to Progression by nium

目次

幸せを求めて

プログラムのコードは確実に汚くなる。これはエントロピー増大の法則からも明らかだ。

最初に言っておくと、綺麗に書くことが目的ではない。気持ちのよいものを作りたいのだ。気持ちのよいものを作り続けることに己のアイデンティティをフルベットしてしまったのがFlash界隈の変態達だ。だが、気持ちのよいものを作るには試行錯誤が必要だ。限られた時間の中で、できるだけ多くの可能性を試しながら、少しずつ答えに近づいていく。ここで重要なのは試行錯誤にかけられる時間だ。試行錯誤のサイクルが速いほど基本的には精度が上がる。ここでコードの綺麗さが効いてくる。

見通しの悪いコードで試行錯誤にとりかかるとしよう。気になる部分の動きを調整したいのでコードをいじってみる。するとどうだ、思わぬ場所で不具合が生じ、まるで動かなくなる。あれ〜、なんで動かないんだっけと、ひとたび地下に広がる巨大迷路に足を踏み入れれば次に地上に戻って来られるのはいつになるか分からない。バグはプログラム内にフィラメント状にネットワークを形成しているため、部分的な修正が次のバグのトリガーになる。地獄だ。1

よりよいものをつくるとき、視野のコントロールも大切だ。グッと深くのめり込みながら繊細なチューニングをしていたかと思えば、パッと距離を取り視野を広げて全体を見渡し、自分の固定観念にパラダイムシフトを起こす。そしてまた1点に集中し、繰り返す。そのような視野の筋肉をフル活用しながら、自分と自分のコラボレーションを起こすのだ。だが、見通しの悪いコードでは、プログラムの構造を作り替えること自体に非常にコストがかかる。現実の実装が想像力に追いつけないのだ。バグの連鎖が、ピュアなクリエイティビティを忘却の彼方に追いやっていく。えーと、これなんのためにやってたんだっけ。これは甚大なフラストレーションとなっていく。

おまけに進捗というものは線形ではない。進捗とは一般にプロジェクト後半にかけて急激に伸びることが知られているので、〆切間際にどれだけ試行錯誤できるかどうかが運命の分かれ道だ。胃が痛い。(※進捗のイージングカーブは人間性の影響を受けます)

幾多の徹夜を越えて、なんとか納得いくものができた。これはマスターピースだ、だがもう2度とプロジェクトファイルは開きたくない。1年後、修正依頼がやってきたとき、マスターピースはオーパーツになっている。地獄の第二章だ。

人を幸せにすることは我々の使命だ。だが同時に、我々自身も幸せにならなければならない。

というわけで、openFrameworksの自作アドオン ofxCommand を作りました。

ofxCommandはコーディング時に以下の点を解決し、試行錯誤にかけるリソースを少しでも増やすことを目的としています。

  • 素早く直感的に書くことができる
  • 柔軟で構造を組み替えやすい
  • ある部分が別の部分に影響しにくい
  • 試行錯誤で汚れても読み解きやすい

なお、ofxCommandは頻繁に更新してるため、万が一サンプルコードが動作しない場合はご連絡ください。

ofxCommandの基本

アドオンのダウンロードは以下からできます。

ofxCommandは、小さな命令(コマンド)を組み合わせながら大きくて複雑なプログラムを作れるライブラリです。
実際に流れをみてみましょう。

今回は、何かを順番に実行していくプログラム構造を作ってみます。
ofApp.hにofxCommandをインクルードし、好みに応じて名前空間cmdを解放します。

#include "ofxCommand.h"
using namespace cmd;

また、順番に色々実行できるSerialコマンドを宣言しておきます。

class ofApp : public ofBaseApp {
    public:
        Serial* sequence; //Serial型のポインタを宣言する

ofApp.cppのsetup内に以下のコードを書いて、Serialコマンドを定義していきます。

void ofApp::setup() {
    sequence = new Serial({ });
    sequence->run();
}

空の配列をコンストラクタに渡したあとに、runを呼び出して実行しています。この時点ではコンストラクタの引数が空なので何も起こりません。

寂しいのでコンソールに何か出してみます。

sequence = new Serial({
    new Log("Happy")
});
sequence->run();

Happyと表示されました。ついでにもうひとつ表示してみます。

sequence = new Serial({
    new Log("Happy"),
    new Log("New Year", OF_LOG_WARNING)
});
sequence->run();

世界の危機を訴えていきたいので警告レベルで"New Yearを表示してみました。余韻をもたせたいので間に1秒挟んでみます。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING)
});
sequence->run();

このように、Serialコマンドを使うことで、処理を上から順番に実行することができるようになりました。そしてLogやWaitもひとつのコマンドなので、今回のようにSerialに入れ込まなくても単体で使うこともできます。

新年といえばおみくじなので、おみくじを実装しましょう。

画面をクリックしたときにコンソールに結果を表示してみます。なので、画面をクリックしたらイベントが起こるようにclickEvent変数を宣言して、mouseReleasedで実際にイベントが発火するようにします。これはopenFrameworksの標準機能ですね。

class ofApp : public ofBaseApp {
    public:
        Serial* sequence;
        ofEvent<void> clickEvent;  //クリックしたとき用のイベントを宣言する
void ofApp::mouseReleased(int x, int y, int button) {
    ofNotifyEvent(clickEvent); //クリックしたときイベントを発火する
}

土台が整ったので、先ほどのSerialコマンドにイベントを待つ処理を入れてみます。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING),

    new Log("画面をクリックしておみくじを引く"),
    new Listen<void>(clickEvent),
    new Log("おみくじの結果は!?")
});
sequence->run();

画面をクリックすると、ListenコマンドがclickEventイベントを検出するまで待機するようになりました。結果を表示してみましょう。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING),

    new Log("画面をクリックしておみくじを引く"),
    new Listen<void>(clickEvent),
    new Log("おみくじの結果は!?"),

    new Function([&] {
        if (ofRandom(1) < 0.2) {
            ofLog() << "ラッキーポン吉";
        } else {
            ofLog() << "凶";
        }
    })
});
sequence->run();

Functionコマンドを使うことで、任意のタイミングで任意の処理を実行したり、Serialコマンドに別の処理を割り込ませることができます。実行したい処理はラムダ式(ActionScriptのfunction() {}みたいなもの)に入れてFunctionコマンドに渡します。

ここでは、20%の確率でラッキーポン吉、それ以外は凶にしています。

ここでは使っていませんが、Function内でSerialコマンドのinsertメソッドでコマンドを割り込まることもできますし、addメソッドを使えば割り込みではなく末尾にコマンドを挿入することもできます。処理構造自体を動的に変化させたい場合に便利です。

結果を表示したところで、最後にメッセージを表示してみます。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING),

    new Log("画面をクリックしておみくじを引く"),
    new Listen<void>(clickEvent),
    new Log("おみくじの結果は!?"),

    new Function([&] {
        if (ofRandom(1) < 0.2) {
            ofLog() << "ラッキーポン吉";
        } else {
            ofLog() << "凶";
        }
    }),
    new Log("本年もよろしくお願いします")
});
sequence->run();

結果が表示された後にメッセージが表示されました。
運悪く凶を引いてしまった人のために、おみくじを何度でもおみくじを引けるようにしてみます。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING),

    new Loop(
        new Serial({

            new Log("画面をクリックしておみくじを引く"),
            new Listen<void>(clickEvent),
            new Log("おみくじの結果は!?"),

            new Function([&] {
                if (ofRandom(1) < 0.2) {
                    ofLog() << "ラッキーポン吉";
                } else {
                    ofLog() << "凶";
                }
            })
        })
    ),

    new Log("本年もよろしくお願いします")
});
sequence->run();

Loopコマンドを使うと、コマンドを繰り返し実行することができます。LoopコマンドはSerialコマンドと違ってコマンドを配列では受け取れないので、複数のコマンドを繰り返し実行したい場合はこのように一度Serialコマンドでまとめてしまって、それをLoopコマンドに渡すとよいです。

実行するとわかりますが、無限ループしてしまって最後のメッセージが表示されません。なので、ループは3回とします。

sequence = new Serial({
    new Log("Happy"),
    new Wait(1),
    new Log("New Year", OF_LOG_WARNING),

    new Loop(3,
        new Serial({

            new Log("画面をクリックしておみくじを引く"),
            new Listen<void>(clickEvent),
            new Log("おみくじの結果は!?"),

            new Function([&] {
                if (ofRandom(1) < 0.2) {
                    ofLog() << "ラッキーポン吉";
                } else {
                    ofLog() << "凶";
                }
            })
        })
    ),

    new Log("本年もよろしくお願いします")
});
sequence->run();

繰り返しになりますが、ofxCommand の設計方針は、小さな命令(コマンド)を柔軟に組み合わせることで大きくて複雑な処理を作り出すというものです。

組み合わさった処理もまた、小さなコマンドと同じインターフェースを備えているので、再帰的に部品として使えます。Serialコマンドの中にLoopコマンドやSerialコマンドを入れることができるのはそういう仕組みです。他にも、複数のコマンドを同時に実行するParallelコマンドを組み合わせれば、より思い通りの設計ができるはずです。

これは Progression 2という、nium氏によるActionScript製のフレームワークに内包されていたCommandライブラリの考え方を参考にしています。ただし内部処理はフルスクラッチで実装しているため、Progressionの挙動とは異なるかも知れません。

記事の末尾に、紹介できていない他のコマンドを含めた簡単なリファレンスを用意しておきましたので参照ください。

ofxCommandとアニメーション

ofxCommandはトゥイーンエンジンをもっているので、トゥイーンをひとつのコマンドとして組み入れることができます。
Tweenコマンドというものがそれにあたります。

ひとつの値をアニメーションさせる

使い方はこのような感じです。

//ofApp.h
Tween* tween;
float x;

//ofApp.cpp
tween = Tween::create(1, Expo::Out)->animate(x, 0, 100);
tween->run();

ここでは、xという変数を1秒かけてExpo::outのイージングで0から100まで変化させる、というトゥイーンを実行しています。

Tweenコマンドで指定できるイージングはLinear::None Sine::In Sine::Out Sine::InOut Quad::In Quad::Out Quad::InOut Cubic::In Cubic::Out Cubic::InOut Quart::In Quart::Out Quart::InOut Quint::In Quint::Out Quint::InOut Expo::In Expo::Out Expo::InOut Circ::In Circ::Out Circ::InOut Back::In Back::Out Back::InOut Elastic::In Elastic::Out Elastic::InOut Bounce::In Bounce::Out Bounce::InOut といった定番品は網羅しています。カスタムイージングにも近々対応予定です。

イージングについては本Advent Calendar 1日目のfeb19氏の投稿が詳しいのでそちらを見るとよいかと思います。

複数の値をアニメーションさせる

同じ秒数やイージングで複数の値をトゥイーンさせることもできます。

//ofApp.h
Tween* tween;
float x;
float y;

//ofApp.cpp
y = -50;

tween = Tween::create(1, Expo::Out)->animate(x, 0, 100)->animate(y, -100);
tween->run();

先ほどのxに加えて、yという変数をトゥイーンさせてみました。メッシュの頂点など大量のデータを一度に移動させる場合などに便利なのではないでしょうか。ここで、変数yに関する座標指定が一つしかないことに注目してください。このような指定の仕方をした場合、トゥイーン開始時の値(-50)から-100まで変化させることができます。

パラメータを共通化する

秒数やイージングをひとまとめにして、異なるTweenコマンドで使い回すこともできます。

//ofApp.h
Tween* tweenA;
float a;

Tween* tweenB;
float b;

//ofApp.cpp
TweenParameter param(2, Quart::InOut);

tweenA = Tween::create(param)->animate(a, 50, 80);
tweenB = Tween::create(param)->animate(b, 10, 20);

このように共通化できる部分は共通化することでハードコーディングの量が減り、後々のチューニングが楽になることがあります。

コールバックやイベントを受け取る

Tweenコマンドは様々なタイミングでコールバックを実行したりイベントを発火するので、ofxCommand以外の処理との連携が手軽におこなえます。

//ofApp.cpp
tween = Tween::create(1, Expo::Out)->animate(x, 0, 100)
    ->atStart([&](Tween& tween) {
        ofLog() << "start : " << tween.getTimeRatio() << " : " << tween.getValueRatio();
    })->atStop([&](Tween& tween) {
        ofLog() << "stop : " << tween.getTimeRatio() << " : " << tween.getValueRatio();
    })->atUpdate([&](Tween& tween) {
        ofLog() << "update : " << tween.getTimeRatio() << " : " << tween.getValueRatio();
    })->atComplete([&](Tween& tween) {
        ofLog() << "complete : " << tween.getTimeRatio() << " : " << tween.getValueRatio();
    });
//ofApp.cpp
tween = Tween::create(1, Expo::Out)->animate(x, 0, 100);

//tweenStartHandler(Command& command); //適宜Tweenにキャストする
ofAddListener(tween->onStart, this, &ofApp::tweenStartHandler);

//tweenStopHandler(Command& command); //適宜Tweenにキャストする
ofAddListener(tween->onStop, this, &ofApp::tweenStopHandler);

//tweenUpdateHandler(Command& command); //適宜Tweenにキャストする
ofAddListener(tween->onUpdate, this, &ofApp::tweenUpdateHandler);

//tweenCompleteHandler(Command& command); //適宜Tweenにキャストする
ofAddListener(tween->onComplete, this, &ofApp::tweenCompleteHandler);

ofxCommandリファレンス

  1. 各種コマンド
    • Log : 文字列をコンソールに出力する
    • Wait : 一定時間待機する
    • Var : 変数に値を代入する
    • Function : 関数を実行する
    • Listen : イベントが発火するまで待機する
    • Notify : イベントを発火させる
    • Tween : 値をアニメーションさせる
    • Serial : 複数のコマンドを順番に実行する
    • Parallel : 複数のコマンドを並行に実行する
    • Loop : コマンドを繰り返し実行する

Log

コンソールに文字列を出力します。

new Log(const string& message, ofLogLevel level)

引数 説明 デフォルト値
const string& message 出力する文字列 ""
ofLogLevel level 出力するレベル OF_LOG_NOTICE
command = new Log("Hello World");
command->run();

// コンソールに "Hello World" という文字列を出力する
command = new Log("An error has occurred", OF_LOG_ERROR);
command->run();

// コンソールに "An error has occurred" という文字列をエラーレベルで出力する

Wait

指定した時間待機します。

new Wait(float duration, bool isFrameBased)

引数 説明 デフォルト値
float duration 待機する秒数 or フレーム数 1
bool isFrameBased 秒数ではなくフレーム数を指定する場合はtrue false
command = new Serial({
    new Wait(5),
    new Log("5 seconds is elapsed"),
});
command->run();

// 5秒経過後、コンソールに "5 seconds is elapsed" という文字列を出力する
command = new Serial({
    new Wait(60, true),
    new Log("60 frames is elapsed"),
});
command->run();

// 60フレーム経過後、コンソールに "60 frames is elapsed" という文字列を出力する

Var

変数に値を代入します。

new Var<T>(T& target, T value)

引数 説明 デフォルト値
T& target 代入対象の変数 NULL
T value 代入する値 Tのデフォルト値
command = new Var<int>(position, 100);
command->run();

// int型の変数 position に 100 を代入する

Function

任意の関数を実行します。引数にラムダ式(ActionScriptでいう function() { 〜〜 } みたいなもの)を与えることで、任意の処理をおこなうことができます。

new Function(const function<void()>& f)

引数 説明 デフォルト値
function<void()>& f 実行したい関数 NULL
command = new Function([&] {
    ofBackground(ofRandom(255));
});
command->run();

// 背景色をランダムに変える

また、Serialなどのフロー制御コマンドと組み合わせることで、動的にコマンドを追加することもできます。

Serial* command = new Serial({
    new Function([&] {

        if (ofRandom(1) < 0.5) {
            command->insert({
                new Log("delay"),
                new Wait(1)
            });
        }

    }),
    new Log("complete")
});
command->run();

// 50%の確率で1秒待つ

Functionコマンド内で親となるSerialコマンドのcommand->insert()メソッドを呼び出すことで、自分自身の直後(new Log("complete")の前)にLogコマンドとWaitコマンドを挿入しています。
また、command->insert()の代わりにcommand->add()を使えば末尾にコマンドを追加することもできます。

Listen

特定のイベントが発火するまで待機します。

new Listen(ofEvent<T>& event)

引数 説明 デフォルト値
ofEvent<T>& event 監視対象のイベント NULL
command = new Listen<int>(myEvent);
command->run();

// int型のパラメータを持つイベント myEvent が発火するのを待つ

Notify

特定のイベントを発火させます。

new Notify(ofEvent<T>& event, T argument)

引数 説明 デフォルト値
ofEvent\<T\>& event 発火させるイベント NULL
T argument イベント引数 Tのデフォルト値
command = new Notify<int>(myEvent, 10);
command->run();

// int型のパラメータを持つイベント myEvent を発火させる
command = new Notify<void>(myEvent);
command->run();

// パラメータを持たないイベント myEvent を発火させる

Tween

float型の数値を指定した時間、イージングで変化させます。
同じTweenコマンドでたくさんの数値をアニメーションさせることができます。

new Tween(float duration, const Easing& easing, bool isFrameBased)

引数 説明 デフォルト値
float duration アニメーションの秒数 or フレーム数 1
Easing& easing アニメーションのイージング Linear::None (イージングなし)
bool isFrameBased 秒数ではなくフレーム数を指定する場合はtrue false

使い方は ofxCommandとアニメーション をご覧ください。

Serial

複数のコマンドを順番に実行します。

new Serial(const vector<Command*>& commands)

引数 説明 デフォルト値
vector<Command*>& commands 順番に実行するコマンド群 NULL
command = new Parallel({
    new Log("Hello"),
    new Wait(1),
    new Log("World"),
});
command->run();

// "Hello" → 1秒経過 → "World"

Parallel

複数のコマンドを並列に実行します。

new Parallel(const vector<Command*>& commands)

引数 説明 デフォルト値
const vector<Command*>& commands 同時に実行するコマンド群 NULL
command = new Parallel({
    new Log("Hello"),
    new Wait(1),
    new Log("World"),
});
command->run();

// Hello と World が同時に出力される

SerialParallel、後述するLoopコマンド自体もひとつのコマンドとして扱われるため、SerialParallelLoopの中に入れることができます。これにより、非常に柔軟にプログラム構造を定義することができます。

command = new Serial({
    new Parallel({
        new Serial({
            new Wait(5),
            new Log("5秒経過しました"),
        }),
        new Serial({
            new Listen<void>(myEvent),
            new Log("myEventが発火しました"),
        }),
    }),
    new Log("5秒経過しつつ、かつmyEventが発火しました"),
});
command->run();

// 「5秒経過する」、「myEventが発火する」、両方の条件を満たしたとき「完了」と表示される。
// 条件を満たす順序は関係ない。

Loop

複数のコマンドを指定した回数繰り返します。

new Loop(Command* target, int loopCount)

引数 説明 デフォルト値
Command* target ループさせるコマンド NULL
int loopCount ループ回数 -1(無限)
command = new Loop(
    new Serial({
        new Log("Hello"),
        new Wait(0.5),
        new Log("World"),
        new Wait(1),
    }),
3);
command->run();

// (Hello表示 → 0.5秒待機 → World表示 → 1秒待機) を3回繰り返す

楽しいopenFrameworksライフを

ofxCommandの機能は他にもたくさんありますので、今後もコツコツ情報を拡充していきます。

また、現在ofxCommandTunerを開発中です。
ofxCommandはプログラムの構造を柔軟に作ることができるライブラリですが、現場レベルでの動きの細かいチューニングにはやはり時間がかかります。ofxCommandTunerは、Waitコマンドの秒数、Tweenコマンドの秒数とイージングと値を、実行時リアルタイムにカスタマイズし、気に入った状態で保存できるといったものになります。ほぼ完成しているのですが、ドキュメントを整理中です。



  1. もちろんバグが新しい表現を生むことはありますが、それはコードが綺麗だろうが汚かろうがあまり確率は変わらないように思います。それよりも重要なファクターは構造的な部分にまで踏み込んで試行錯誤をしようとする意志とその回数だという立場です。そもそもコンパイルエラーがバンバン出てたら新しい表現もクソもありません。 

  2. Flashサイト最盛期に存在したActionScript3.0製フレームワーク。元々アニメーション制作ソフトとして出発したFlashでウェブサイトをつくるとき、制作者たちはインタラクションの気持ちよさを実装するのと同じくらい、画面遷移のシステムに多くの労力をかけなければなりませんでした。そこで彗星のように現れたのが、 @nium 氏によるProgressionフレームワークでした。その力たるや強大で、数多くのFlash制作者を魅了し、面倒な部分は全てProgressionが引き受けてくれたおかげで、人々は多くの時間を動きやインタラクションに費やすことが可能となり、その結果として数々の魅力的なウェブサイトが世に送り出されました。誇張でなく、一時期ほとんどの国内発のFlashサイトを右クリックするとProgressionのクレジットが入っているほどでした。 

15
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11