小嶋秀樹 | 研究室
日本語 | English
iPhone アプリ開発道場(C++ 編)

openFrameworks は,メディア制作のための機能要素(グラフィクスや周辺機器など)をプログラミング言語 C++ からアクセスできるようにしたライブラリです.C++ で自由にプログラミングできるようになれば,openFrameworks をより効果的に使いこなすことができるでしょう.ここでは,C++ によるオブジェクト指向プログラミングを,アプリ制作をとおして段階的に学んでいきます.(C 言語の基礎をマスターしていることを前提として解説していきます.)

クラスとインスタンス

ここでは「iPhone 道場(実践)」で扱った『踊る人形』を素材として,それをクラス化し,そこからインスタンスを1つ生成することで,まずは同じ結果を得るようにします.つぎに,複数のインスタンス生成することで,踊る人形の集団をつくります.これらをとおして,クラスの定義方法やインスタンスの生成方法をマスターしてください.

【クラスとインスタンス】

たとえば「本」のクラスとは,さまざまな本に共通する性質をまとめたものと考えばよいでしょう.マンガから哲学書まで,さまざまな本に共通した属性(変数)として,著者名・タイトル・出版社・ページ数などが考えられます.また,可能な行為(メソッド)として,ページを開くこと,ある行を読むこと,あるページのコピーを取ること,本を閉じることなどが共通しています.さらに,紙で出来ていること,インクで文字や図形が描かれていることなど,より上位クラスである「印刷物」の性質等を受け継いでいます.

一方,個々の本(たとえば「アレクサンダとぜんまいねずみ」)は,「本」クラスの具体例であり,インスタンスと呼ばれます.クラスは設計図のようなもので,それを具体化したもの(つまり著者名・タイトルなどを設定したもの)が個々のインスタンスとなります.実体をもつインスタンスについて,ページを開くこと,ある行を読むことなど,「本」に対する行為(そして「印刷物」一般に対する行為)が可能になります.

もう少し直観的なイメージを語れば,クラスとは表をつくるゴム印のようなものです.たとえば「本」クラスであれば,下図(左側)のような感じでしょうか.ゴム印には,変数名やメソッド名が,表のように並べられています.

クラスとインスタンス

このゴム印をメモリ上に押すと,上図(右側)のようなメモリ領域が確保されます.このメモリ領域がインスタンスです.変数の値を格納する領域(白ヌキ部分)に具体的な値(数値や文字列など)を書き込むことで,1冊の本として具体的な操作(ページを開くなど)が可能になります.

【踊る人形のクラス】

それでは実際にクラス(ゴム印)を作成してみます.クラスの定義は,ゴム印そのものとなるヘッダファイル(拡張子 .h)と,メソッドごとの具体的な動作を記述したインプリメンテーション ファイル(拡張子 .cpp)によって行ないます.たとえば「iPhone 道場(実践)」で扱った『踊る人形』を Dancer クラスとしてクラス化するならば,Dancer.h と Dancer.cpp によって定義することになります.

emptyExample からコピー・名前変更した新しいプロジェクトを Xcode で開きます.File > New File... を開き,ターゲットを Mac OS X として C++ File を選んでください.ファイル名を Dancer.cpp とし,ヘッダファイル Dancer.h も生成されるようにチェックを入れて,Finish を押してください.これで Dancer.cpp と Dancer.h が src の中に生成されます.

Windows 上で VC++ を利用している場合は,プロジェクト > 新しい項目の追加... を開き,「C++ ファイル」を選択し,名前を Dancer.cpp,場所を src として「追加」を押します.同様に「ヘッダ ファイル」を Dancer.h として src に追加します.Code::Blocks を利用している場合は,File > New > Empty file を開き,Dancer.cpp と Dancer.h を src の中に保存するようにしてください.Debug と Release の両者をターゲットに指定するとよいでしょう.Projects ツリーの中では,Sources と Headers の下に置かれますが,testApp.mm などと同じ src フォルダに移動するとよいでしょう.

ヘッダファイル Dancer.h は,"ofMain.h" をインクルードしたのち,class Dancer {...}; という形になります.最後にセミコロンがつくことに注意してください.内部では,private: に続いて内部変数の宣言(個々のインスタンスごとに保持する変数の宣言)を並べ,public: に続いて外部に公開するメソッドのプロトタイプ宣言を並べます.Dancer クラスの利用者は,個々のインスタンスについて private な内部変数を直接参照することはできませんが,public なメソッドは呼び出すことができます.たとえば setColor() のようなセッタ (setter) によって内部変数に値をセットすることができます.また,ここには例示していませんが,たとえば float period() のようなゲッタ (getter) によって内部変数の値を読み出すことも可能です.

Dancer.h
#include "ofMain.h"
class Dancer {
    private:
        float posX, posY, scaleXY;          //  描画位置・スケール
        int colorR, colorG, colorB;         //  人形の色
        float rad;                          //  ダンスの位相
        float period, range;                //  ダンス周期・動作範囲
        float degLLeg, degRLeg,             //  左足・右足の角度
              degLArm, degRArm;             //  左腕・右腕の角度
        float movBody;                      //  胴体の上下位置
    public:
        void init();                        //  デフォルト初期化
        void setColor(int r, int g, int b); //  人形の色をセット
        void setPeriod(float msec);         //  ダンス周期をセット
        void setRange(float deg);           //  動作範囲をセット
        void setPosition(float x, float y); //  描画位置をセット
        void setScale(float scale);         //  描画スケールをセット
        void update();                      //  人形の姿勢を更新
        void draw();                        //  人形を描画
};

インプリメンテーションファイル Dancer.cpp では,"Dancer.h" をインクルードしたのち,このヘッダファイルに宣言したメソッドの本体,つまり関数定義を並べていきます.openFrameworks では,init(), update(), draw() という3つの基本メソッドと,内部変数のセッタ(setPeriod() など)やゲッタを用意するとわかりやすいでしょう.必要に応じてその他のメソッド(たとえば walk(), sleep() など)を追加してください.

Dancer.cpp
#include "Dancer.h"
void Dancer::init() {
    //  デフォルト値で初期化
    posX = 0; posY = 0; scaleXY = 1;
    colorR = 200; colorG = 200; colorB = 200;
    period = 1000; range = 30;
    rad = 0;
    degLLeg = degRLeg = 0;
    degLArm = degRArm = 0;
    movBody = 0;
}
void Dancer::setColor(int r, int g, int b) {
    //  人形の色をセット (200, 200, 200)
    colorR = r; colorG = g; colorB = b;
}
void Dancer::setPeriod(float msec) {
    //  ダンス周期 (msec) のセット (1000)
    period = msec;
}
void Dancer::setRange(float deg) {
    //  動作範囲 (deg) のセット(30)
    range = deg;
}
void Dancer::setPosition(float x, float y) {
    //  描画位置のセット (0, 0)
    posX = x; posY = y;
}
void Dancer::setScale(float scale) {
    //  描画スケールのセット (1)
    scaleXY = scale;
}
void Dancer::update() {
    //  現在の位相 (rad) で姿勢を更新
    float s = sin(rad);
    degLLeg = 30 + range * s;
    degRLeg = 30 - range * s;
    degLArm = 30 - range * s;
    degRArm = 30 + range * s;
    movBody = 15 * sin(rad * 2 - HALF_PI);
    //  位相 (rad) を動作周期に応じて 1/30s ぶんだけ進める
    rad += 0.5 * 1000 / period * TWO_PI / 30;
}
void Dancer::draw() {
    //  はじまり
    ofPushMatrix();
    ofPushStyle();
    ofSetCircleResolution(32);
    ofSetRectMode(OF_RECTMODE_CENTER);
    //  座標系の設定(標準型に変換し,描画位置・スケールを設定)
    ofTranslate(160, 239);
    ofScale(1, -1);
    ofTranslate(posX, posY);
    ofScale(scaleXY, scaleXY);
    //  人形の色をセット
    ofSetColor(colorR, colorG, colorB);
    //  胴体 body
    ofTranslate(0, movBody);
    ofRect(0, 0, 50, 80);
    //  頭 head
    ofPushMatrix();
    ofTranslate(0, 80);
    ofCircle(0, 0, 30);
    ofPopMatrix();
    //  左足 left leg (upper/lower)
    ofPushMatrix();
    ofTranslate(-10, -45);
    ofRotateZ(-(30 + degLLeg));
    ofTranslate(0, -35);
    ofRect(0, 0, 20, 50);
    ofTranslate(0, -35);
    ofRotateZ(60 + degLLeg);
    ofTranslate(0, -35);
    ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  右足 right leg (upper/lower)
    ofPushMatrix();
    ofTranslate(10, -45);
    ofRotateZ(30 + degRLeg);
    ofTranslate(0, -35);
    ofRect(0, 0, 20, 50);
    ofTranslate(0, -35);
    ofRotateZ(-(60 + degRLeg));
    ofTranslate(0, -35);
    ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  左腕 left arm (upper/lower)
    ofPushMatrix();
    ofTranslate(-35, 40);
    ofRotateZ(-(30 + degLArm));
    ofTranslate(0, -30);
    ofRect(0, 0, 20, 45);
    ofTranslate(0, -30);
    ofRotateZ(-(60 + degLArm));
    ofTranslate(0, -30);
    ofRect(0, 0, 20, 45);
    ofPopMatrix();
    //  右腕 right arm (upper/lower)
    ofPushMatrix();
    ofTranslate(35, 40);
    ofRotateZ(30 + degRArm);
    ofTranslate(0, -30);
    ofRect(0, 0, 20, 45);
    ofTranslate(0, -30);
    ofRotateZ(60 + degRArm);
    ofTranslate(0, -30);
    ofRect(0, 0, 20, 45);
    ofPopMatrix();
    //  おわり
    ofPopStyle();
    ofPopMatrix();
}

Dancer::draw() の中では,最初に ofPushMatrix() / ofPopMatrix(),最後に ofPopStyle() / ofPopMatrix() を呼び出しています.これは,Dancer::draw() による『踊る人形』の描画が,それ以外のグラフィック描画に影響を与えないようにするためです.

【踊る人形のインスタンス】

Dancer クラスが用意できたら,つぎに testApp の中で Dancer クラスのインスタンスを生成し,それを踊らせてみましょう.

まず,testApp.h の中で,"Dancer.h" をインクルードするようにします.これで Dancer クラスを利用できるようになります.つぎに,private 変数として,Dancer クラスの変数(つまり Dancer クラスのインスタンス)dancer を生成(つまりゴム印を押してメモリ領域を確保)するようにします.

testApp.h
#pragma once
#include "ofMain.h"
#include "ofxiPhone.h"
#include "ofxiPhoneExtras.h"
#include "Dancer.h"
class testApp : public ofxiPhoneApp {
    private:
        Dancer dancer;
    public:
        (... 変更なし ...)
};
testApp.mm
Dancer ひとり
#include "testApp.h"
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    //  dancer
    dancer.init();
}
void testApp::update() {
    dancer.update();
}
void testApp::draw() {
    dancer.draw();
}
(... 以下変更なし ...)

testApp.cpp の中では,インスタンス dancer について,dancer.init() を呼び出すことで初期化し,testApp::update() の中では dancer.update() を呼び出し,testApp::draw() の中では dancer.draw() を呼び出すことで,dancer を動かしています.ビルドし実行すると,右上図のように,「iPhone 道場(実践)」の『踊る人形』と同じような結果が得られるはずです.

クラス化のメリット

クラスはインスタンスの設計図をゴム印にしたものと考えられます.メモリ上にこのゴム印を押すことで,インスタンスを生成し,それを利用することができます.そのメリットのひとつは,1つのクラスから複数個のインスタンスを簡単に生成できることです.あるいは「クラスを再利用すること」と言い換えてもよいでしょう.

【2つの人形を踊らせる】

上のプログラムを,つぎのように変更してみてください.2つの(小さめの)人形が,同期して踊るはずです.

testApp.h
#pragma once
#include "ofMain.h"
#include "ofxiPhone.h"
#include "ofxiPhoneExtras.h"
#include "Dancer.h"
class testApp : public ofxiPhoneApp {
    private:
        Dancer dancer1, dancer2;
    public:
        (... 変更なし ...)
};
testApp.mm
Dancer ひとり
#include "testApp.h"
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    //  dancer1
    dancer1.init(); 
    dancer1.setPosition(0, -120); 
    dancer1.setScale(0.75);
    //  dancer2
    dancer2.init(); 
    dancer2.setPosition(0,  120); 
    dancer2.setScale(0.75);
}
void testApp::update() {
    dancer1.update();
    dancer2.update();
}
void testApp::draw() {
    dancer1.draw();
    dancer2.draw();
}
(... 以下変更なし ...)

発展課題: これが出来たら,setPeriod() によって,dancer1 には 900 [ms],dancer2 には 800 [ms] の周期を与えてみてください.また,setRange() によって動きの大きさ(30deg が標準)を変えてみてください.どのような踊りになるでしょうか.

【人形の集団を踊らせる】

25個の人形を,それぞれランダムに与えた周期と動作の大きさで踊らせてみましょう.これだけの個数になると,dancer1, dancer2, ... のような個別のインスタンス変数として扱うのは面倒です.そこで配列(インスタンスの配列)を利用します.

testApp.h
#pragma once
#include "ofMain.h"
#include "ofxiPhone.h"
#include "ofxiPhoneExtras.h"
#define DANCERS 25
#include "Dancer.h"
class testApp : public ofxiPhoneApp {
    private:
        Dancer dancers[DANCERS];
    public:
        (... 変更なし ...)
};
testApp.mm
#include "testApp.h"
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    //  dancers
    for (int i = 0; i < DANCERS; i++) {
        dancers[i].init();
        dancers[i].setScale(0.225);
        dancers[i].setPeriod(ofRandom(500, 1000));
        dancers[i].setRange(ofRandom(10, 40));
        dancers[i].setPosition(((i % 5) - 2) * 60, 
                               ((i / 5) - 2) * 90 );
    }
}
void testApp::update() {
    for (int i = 0; i < DANCERS; i++)
        dancers[i].update();
}
void testApp::draw() {
    for (int i = 0; i < DANCERS; i++)
        dancers[i].draw();
}
(... 以下変更なし ...)
Dancer 25

testApp::setup() に登場した ofRandom(min, max) は,openFrameworks によって提供される乱数発生器です.min から max までの乱数(float)を返します.(ofRandom(max) は 0 から max までの乱数(float)を返します.)

これをビルドし実行すると,左図のように,25個の人形がそれぞれの周期と動作の大きさで踊り出します.ひとたび内部変数(period, range など)を設定すれば,あとは update() と draw() を呼び出すことで,それぞれの人形は自律的に踊ることができます.ひとつひとつの人形が生きているかのように見えませんか.