すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

【openFrameworks】自作クラスのvector対応時におけるeventバインディングでドハマった話【ofEvent】

こんばんは.1000chです. 情報社会においては,いろいろなところでアカウントを作成する機会がありますね. 最近はソーシャルアカウント連携で登録なんてのもありますが. アカウント登録と切っても切れない関係なのがパスワード. よくないよなーと思いつつも流用してしまうこと多々ですよね.

で,一定期間経つと「パスワード変更してください」ってなることもあるじゃないですか. あーそんな時期かーとか思って思いつきで変えたりすると,後で悲惨になったりするんですよね.

...反省します.

お決まり文字列3パターンくらいと,サービスごとにprefixでもつくればいいのかなーとか思いました.

さて,今日も元気にoF使っていきましょう.

前回はofつかってボタンUIをつくりました. 今回は,「画面上でドラッグできるボール」をたくさん作ってみましょう.例によって環境はof_0.8.3_ios,iPadAir(1st)で試してます.

「ドラッグできるボール」クラスをつくる

設計はシンプルに,

  • 描画位置,サイズ,色をインスタンス変数として持つ
  • タッチイベント,ドローイベントにバインディング
  • 円内をタップされたら移動可能にする
  • ドラッグされた地点を新しい描画中心にする
  • drawのタイミングで自動で描画(メインクラスのdrawメソッド内で明示的にdrawを呼ばない)

という感じ.

上記を実装したDraggableCircleクラスが以下.

// DraggableCircle.h

#include "ofMain.h"


class DraggableCircle{
private:
    ofVec2f pos;
    float   radius;
    ofColor col;
    bool    is_selected;
    
public:
    DraggableCircle();
    
    void draw(ofEventArgs & drawArgs);

    void touchDown(ofTouchEventArgs & touch);
    void touchMoved(ofTouchEventArgs & touch);
    void touchUp(ofTouchEventArgs & touch);
    void touchDoubleTap(ofTouchEventArgs & touch);
    void touchCancelled(ofTouchEventArgs & touch);
  
};

// DraggebleCircle.cpp

#include "DraggableCircle.h"

DraggableCircle::DraggableCircle()
{
    radius = 100;
    
    pos = ofVec2f(ofRandom(500), ofRandom(500));
    col.set(ofRandom(255),ofRandom(255),ofRandom(255));
    
    is_selected = false;
}

void DraggableCircle::draw(ofEventArgs & drawArgs)
{
    ofPushStyle();
    ofFill();
    ofSetColor(col);
    
    ofCircle(pos, radius);
    
    ofPopStyle();
}

void DraggableCircle::touchDown(ofTouchEventArgs &touch)
{
    if (touch.distance(pos) < radius) {
        is_selected = true;
    }
};

void DraggableCircle::touchMoved(ofTouchEventArgs &touch)
{
    if (is_selected) {
        pos = touch;
    }
};

void DraggableCircle::touchUp(ofTouchEventArgs &touch)
{
    is_selected = false;
};

void DraggableCircle::touchDoubleTap(ofTouchEventArgs &touch){};
void DraggableCircle::touchCancelled(ofTouchEventArgs &touch){};

ドローイベントのバインド部分は,ちょっと癖があるかもしれません.

DraggableCircle::Draw(ofEventArgs &drawArgs)と実装するのですが,. ofMainのようにDraggableCircle::Draw()と引数無しにしてしまうと,ofAddListenerコンパイルエラーを吐いてしまいます. どうやらリスナー登録関数のテンプレートの中に対応する宣言が無い感じですね.

実際drawイベントはどんな感じで発火されるんだ?と探してみると...

// ofxiOSEAGLView.mm Line171
- (void)drawView {
    
    ofNotifyUpdate();
 
    // 中略
    ofNotifyDraw();
   
    // 後略 
}

コレが怪しい.ofNotifyDrawをみてみると...

// ofEvent.h Line 172
void ofNotifyDraw(){
    if(ofGetCurrentRenderer()){
        ofNotifyEvent( ofEvents().draw, voidEventArgs );
    }

    nFrameCount++;
}

見覚えあるヤツがいます!ofNotifyEvent( ofEvents().draw, voidEventArgs )でドローイベントを発火しています. ここの引数としてるvoidEventArgsは中身空のダミーみたいです.

ともあれ,Notifyで返ってくる変数と同型の変数の参照を引数にするようなdraw関数を作っておかないと,ってことですね.

(でも void ofNotifyEvent(ofEvent<void> & event) ってのも定義されてるんだよなー.どう使うんだろ)

「ドラッグできるボール」クラスを使う

ofAppはこんな感じ.

// ofApp.h

#include "DraggableCircle.h"

class ofApp : public ofxiOSApp {
    
    public:

    // 中略

    DraggableCircle circle;
};

// ofApp.mm

#include "ofApp.h"

void ofApp::setup(){
    ofSeedRandom();
    ofBackground(0);
    ofSetCircleResolution(32);
    ofSetOrientation(OF_ORIENTATION_90_LEFT);
    
    // タッチイベントのバインド
    ofRegisterTouchEvents(&circle);
    // ドローイベントのバインド
    ofAddListener(ofEvents().draw, &circle, &DraggableCircle::draw);
}

エラいシンプルですね.先のクラスにほとんどの内容を任せてしまっているおかげです. というわけでofAppですべきなのは

  • タッチイベントをバインドする
  • ドローイベントをバインドする

の2点だけです.

ここでの注意点,というか注目したい点としては,「ofAppが,所有するDraggableCircleのインスタンスをイベントにバインドしている」という点です.

今までは

ofAddListener(tgl_btn.onClick, this, &ofApp::toggleLock);

というようにonClickイベントを,自分(this)が監視し,感知したら自身のメソッド(&ofApp::toggleLock)を実行する.という形でしたが,今回のように参照さえ持っていれば別のオブジェクトをイベントバインドできるんですね. 応用広そうです.

動かす

こんな感じのものができました.

ドラッグできてます.

配列化する

複数個同時にいじってみましょう.

コードはちょろっとかえるだけで済みます.

// ofApp.h

#include "DraggableCircle.h"

class ofApp : public ofxiOSApp {
    
    public:

    // 中略

    DraggableCircles circles[4];
};

// ofApp.mm

#include "ofApp.h"

void ofApp::setup(){
    ofSeedRandom();
    ofBackground(0);
    ofSetCircleResolution(32);
    ofSetOrientation(OF_ORIENTATION_90_LEFT);
    
    for(int i=0; i<4; i++){
        // タッチイベントのバインド
        ofRegisterTouchEvents(&circles[i]);
        // ドローイベントのバインド
        ofAddListener(ofEvents().draw, &circles[i], &DraggableCircle::draw);
    }
}

配列で宣言して,for文回すだけですね.

vector化する

じゃ,ということで,動的配列化しましょう.いわゆるvector<>というやつです.

コードはこんな感じで.

// ofApp.h

#include "DraggableCircle.h"

class ofApp : public ofxiOSApp {
    
    public:

    // 中略

    vector<DraggableCircle> circles;
};

// ofApp.mm

#include "ofApp.h"

void ofApp::setup(){
    ofSeedRandom();
    ofBackground(0);
    ofSetCircleResolution(32);
    ofSetOrientation(OF_ORIENTATION_90_LEFT);
    
    for(int i=0; i<4; i++){
        DraggableCircle circle = DraggableCircle();
        // タッチイベントのバインド
        ofRegisterTouchEvents(&circle);
        // ドローイベントのバインド
        ofAddListener(ofEvents().draw, &circle, &DraggableCircle::draw);
        circles.push_back(circle);
    }
}

vectorへの追加はpush_backを使います.for文回してインスタンスいっこずつぶっ込むだけ...

では動きません!!!!

これで実行すると,画面が真っ黒になってしまいます...

はて?という感じですが,次を記述すると円が描画されます.

void ofApp::draw(){
    
    ofEventArgs arg = ofEventArgs();
    for (int i=0; i<4; i++) {
        circles[i].draw(arg);
    }
}

しかし,ドラッグイベントが無効になっています!!

どうやら,イベントのバインドだけ無効になっているようですね... バインドしたオブジェクトとvector内部にのこっているオブジェクトが別物という事と考えられます.

試してみましょう.

    for (int i=0; i<4; i++) {
        cout << "-- start loop " << i <<" --" << endl;
        DraggableCircle circle = DraggableCircle();
        ofRegisterTouchEvents(&circle);
        ofAddListener(ofEvents().draw, &circle, &DraggableCircle::draw);
        circles.push_back(circle);
        cout << "addr tmp circle:" << &circle << endl;
        cout << "addr vector[i]:" << &circles[i] << endl;
        cout << "-- fin loop " << i <<" --" << endl;
    }

出力

-- start loop 0 --
addr tmp circle:0x27dfcc30
addr vector[i]:0xd976b0
-- fin loop 0 --
-- start loop 1 --
addr tmp circle:0x27dfcc30
addr vector[i]:0xd976e4
-- fin loop 1 --
-- start loop 2 --
destructed!
addr tmp circle:0x27dfcc30
addr vector[i]:0xd97728
-- fin loop 2 --
-- start loop 3 --
addr tmp circle:0x27dfcc30
addr vector[i]:0xd9773c
-- fin loop 3 --

あー...なるほど...という感じですね. どーやらvectorにpushしてる時点でコピーコンストラクタが呼ばれ,別のオブジェクトになってるようです.

「じゃーpushしたvectorの要素にイベントバインドすれば良いか?」というと,そう簡単な話ではありません. なぜなら,vectorが動的にメモリを確保するため,「想定最大値を超えた際に,より大きいサイズのvectorにコピーされる」為です. この仕様により,はじめのうちにバインドしたオブジェクトが消失,という事になってしまいます.

先ほどのループで常に&circles[0]をみてみると,,,

-- start loop 0 --
addr tmp circle:0x27dfcc30
addr vector[0]:0xc36450
-- fin loop 0 --
-- start loop 1 --
addr tmp circle:0x27dfcc30
addr vector[0]:0xc35fd0
-- fin loop 1 --
-- start loop 2 --
destructed!
addr tmp circle:0x27dfcc30
addr vector[0]:0xc36000
-- fin loop 2 --
-- start loop 3 --
addr tmp circle:0x27dfcc30
addr vector[0]:0xc36000
-- fin loop 3 --

loop0,1,2-3 でcircles[0]のアドレスが変わってることがわかりますね.

じゃどーすんの?

とりあえずこれでしのげました.

// ofApp.h(一部)
    
    //vector<DraggableCircle> circles;
    vector<DraggableCircle *> circles;

// ofApp.mm(一部)
void ofApp::setup(){

   // 中略

    for (int i=0; i<4; i++) {
        DraggableCircle *circle = new DraggableCircle();
        ofRegisterTouchEvents(circle);
        ofAddListener(ofEvents().draw, circle, &DraggableCircle::draw);
        circles.push_back(circle);
    }
   
}
//--------------------------------------------------------------
void ofApp::exit(){

    for(int i=0; i<circles.size(); i++){
        delete circles[i];
    }
}

変更点は以下. ・circlesを「DraggableCircleのポインタ」のvectorとして定義 ・DraggableCircleの生成時にnewを利用,動的に領域を確保 ・vectorには生成されたオブジェクトへのポインタを格納

これにより,vectorがコピーされても,「vectorの各アドレスが指す要素」自体は変更されない,ということになります. 中身をみてみるとこんな感じです.

-- start loop 0 --
addr tmp circle:0xd5b460
addr vector[0]:0xd5b460
-- fin loop 0 --
-- start loop 1 --
addr tmp circle:0xd48d00
addr vector[0]:0xd5b460
-- fin loop 1 --
-- start loop 2 --
addr tmp circle:0xd48f70
addr vector[0]:0xd5b460
-- fin loop 2 --
-- start loop 3 --
addr tmp circle:0xd48f90
addr vector[0]:0xd5b460
-- fin loop 3 --

ループをまたいでも,vectorの中身が指す場所自体は不変であることがわかります.

実際,このコードによって配列の時と同様にイベントを監視して操作することが可能でした.

..ただ,自分でnewとか使うときちんとdeleteしなくてはいけないことに気をつけないといけないです.

まとめ

  • ofEventを利用するクラスのvector化に挑戦
  • newすればokっぽい
  • delete忘れちゃダメ絶対
  • C++のオブジェクト生成周りの話は,一度キチンと勉強しないといけないなぁと実感.

次回はnewの仕様とかコピーコンストラクタとかそのへんを勉強したい所存