yoppa.org


メディア芸術演習 VI - メディア・アート II

openFrameworksで、オブジェクト指向プログラミング(OOP) 前編

オブジェクト指向プログラミングとopenFrameworks

オブジェクト指向プログラミング(OOP)とは相互にメッセージを送りあうオブジェクトの集まりとしてプログラムを構成するプログラミングの技法です。現在使用されているプログラミング言語の多くは、OOPのパラダイムをベースに設計されていて、現在主流のプログラミングの手法と言えるでしょう。オブジェクト指向プログラミング言語を列挙してみると、Smalltalk、C++、Objective-C、Python、Ruby、Perl(5.0以降)、Java、JavaScript、C#など数多くの言語があげられます。

openFrameworksはC++をベースにしたフレームワークです。ですので、openFrameworksもオブジェクト指向のプログラミング言語です。OOPの利点を最大限に利用することで、より効率的に強力なプログラムをしていくことが可能となります。

「プロパティ」「メソッド」「インスタンス」「カプセル化」「ポリモーフィズム」など慣れない言葉が頻出するので、最初はとまどうかもしれませんが、その考え方の根本はシンプルなものです。今回はopenFrameworksを用いてOOPをしていくための基本についてじっくりとやっていきたいと思います。

オブジェクト指向プログラミングの基本概念

このセクションでは、まずオブジェクト指向プログラミングの根幹を成す「オブジェクト」という概念自身について考えていきます。

オブジェクトとメッセージ

オブジェクト指向プログラミングとは、オブジェクトというプログラム機能の部品の集合が、相互にメッセージを送りあいながらプログラムを構成していくというプログラミング技法です。それぞれのオブジェクトは独立していて、自分自身で値や処理手順を保持しています。オブジェクトの一つ一つが、小さなプログラムのモジュールあると考えられます。そうした小さなモジュール同士がが相互にメッセージを送りあうことで大きなプログラムを構成するというのが、オブジェクト指向の基本的な考えかたとなります。

オブジェクト = プロパティ(属性) + メソッド(動作)

オブジェクト指向の考え方を、より日常的で具体的な例で考えてみましょう。オブジェクトは日本語にすると「物」です。オブジェクト指向プログラミングの構成単位であるこのオブジェクトは、私達の周囲にある物で例えてみることができます。オブジェクト指向プログラミングでは、物を二つの観点から整理します。一つはその物固有の「属性」、もう一つはその物自身に対する「動作」です。属性のことを「プロパティ」、動作のことを「メソッド」と言い換えることもできます。この複数のプロパィとメソッドから構成されるオブジェクトの概念を図示すると、次のようになります。

この考えかたは、実際に身の回りのものに当てはめてみるとより容易に理解可能になるでしょう。

  • オブジェクト:犬
    • 属性(プロパティ):犬種、年齢、性別
    • 動作(メソッド):歩く、吠える、餌を食べる、寝る
  • オブジェクト:テレビ
    • 属性(プロパティ):画面サイズ、チャンネル、ボリューム
    • 動作(メソッド):点ける、消す、チャンネル変更、音量変更
  • オブジェクト:リンゴ
    • 属性(プロパティ):色、重量、味
    • 動作(メソッド):成長する、熟す、落下、腐る

もちろん、現実の犬やテレビやリンゴはこんなに単純ではありません。もっと沢山の属性や動作を持っています。しかし重要なことは、物の全ての性質を写しとるのではなく、プログラムで必要となる属性と動作を抽出するという操作です。プログラムの中のオブジェクトは、あくまでも物そのものではなく、現実世界の中からそのプログラムの機能で必要なものだけをとりだした抽象化された「物 = オブジェクト」なのです。

クラス = オブジェクトの設計図

では、どのようにしたらオブジェクトをプログラム内に生成することができるのでしょうか。オブジェクト指向プログラミングでは、オブジェクトを生成するには、まずその設計図を作成しなければなりません。この設計図のことを「クラス」と呼びます。クラスに属性と動作の詳細を記述することで、オブジェクトのふるまいを決定します。

クラスは設計図なので、そのままではプロブラムのモジュールとして動作することはできません。クラスを実際に使用するには、クラスをオブジェクトとして生成する必要があります。この操作を「インスタンス化」といいます。

クラスからインスタンス化されるオブジェクトは一つとは限りません。一つのクラスから、必要に応じて複数のオブジェクトを生成することも可能です。例えば、車の設計図を一つ作成すれば、それを元にして何台でも車を製造することが可能となるということと同じです。このようにして、共通の属性を持ち同じ動作が可能なオブジェクトを、一度に沢山生成することが可能となります。

オブジェクト指向プログラミングの特徴

カプセル化

カプセル化とは、外部からアクセスする必要の無い場合、オブジェクトのメソッドやプロパティを外部からは見えないように隠す(隠蔽)という概念です。このことにより、プログラムの変更に対する耐久性が上がったり、プログラムの抽象度が上がるという利点があります。

これは時計を例にとるとわかりやすいかもしれません。我々は時計を利用する際には、時刻を調整する機能(メソッド)や、時刻を知るための針の角度の状態(プロパティ)を知ることができます。しかし、時計の内部でどのような機構によってその機能が実現されているのかは、時計を使用する範囲においては知る必要はありません。ですので時計は必要な機能以外には触れることができないようにその機能を隠しています。これがカプセル化の考え方です。

カプセル化した結果、外部からアクセスできなくなった領域をプライベート(Private)、外部に公開してアクセス可能にした領域をパブリック(Public)と呼びます。

インヘリタンス (継承)

継承とは、既存のクラスから、その機能や構造を共有する新たなクラスをサブクラスとして派生することができるという機能です。そのようなクラスは「親クラス(スーパークラス)を継承した」と表現します。

例えば、「車」という既存のクラスを派生して「パトカー」「消防車」「タクシー」といった、その機能を共有し新たな機能を付加したクラスを生成することが可能となります。

ポリモーフィズム (多態性、多相性)

ポリモーフィズムとは、オブジェクトの処理の実態は、メッセージの名前からではなく、オブジェクトごとに決定された方法で異った処理を行うことが可能であるという性質です。

用語が難解なので難しく考えがちなのですが、簡単に言えば「メッセージに対する処理の方法はオブジェクト自身が決めてよい」ということです。例えば、「犬」「猫」「ライオン」というオブジェクトがあったとします。ここに「鳴く」という共通のメッセージをそれぞれ送った際に、犬「ワンワン」、猫「ニャー」、ライオン「ガオー」とそれぞれ異った鳴きかたをします。それぞれのオブジェクトによって異なる鳴き方(メソッド)が決められているからです。これがポリモーフィズムです。

オブジェクト指向プログラミングの視点からtestAppを見直す

オブジェクト指向プログラミングの考えかたを一通り理解した上で、もう一度、いままで使用してきた testApp.h について眺めてみましょう。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
};
#endif

testApp.h の6行目に「class testApp : public ofBaseApp」という記述があります。この記述のは「testAppクラスは、公開されたofBaseAppというクラスを継承している」ということを意味しています。つまり、いままで編集してきた「testApp」というものは、ひとつのクラス、つまりオブジェクトの設計図なのです。

また、このtestAppは、ofBaseAppというクラスを継承しています。このofBaseAppクラスには、setup()、update()、draw() などのopenFrameworksの基本的な構造に関する動作(メソッド)、また keyPressed()、mouseMoved()、mouseReleased() などのインタラクションに関する動作(メソッド)が定義されています。実は今までtestAppで行ってきたのは、このofBaseAppクラスの機能を共有して、サブクラスとしてさらに新たな機能を付加するという作業であったと捉えることもできます。

プロパティ = 変数、メソッド = 関数

オブジェクトの説明のところで、オブジェクトはプロパティ(= 属性、状態)とメソッド(= 動作、ふるまい)から構成されると解説しました。では、実際にクラスをC++で記述する際にはこれらのプロパティとメソッドはどう記述されるのでしょうか。

実際にはとても単純で、C++では、クラスの属性は変数で定義します。変数は一定の値を格納できる機能をもっているからです。また、クラスのメソッドは関数で表現します。関数はプログラムの一連の処理をまとめて表現している機能だからです。つまり、C++のプログラムにおいては、クラス共通の変数はクラスのプロパティ、関数はメソッドをあらわしていると考えます。

ヘッダーファイルとソースファイル

C++では、あらかじめ宣言されていない変数や関数を使用することができません。C++では、あらかじめクラスで使用される変数(クラスのプロパティに相当)や関数(クラスのメソッドに相当)を宣言してそれを共有して使用できるようにヘッダーファイルというものを作成して、そこに宣言をまとめることが一般的です。言い換えると、ヘッダーファイルにはそのクラスの機能の概要が書かれていると読みとることも可能です。ヘッダーファイルの拡張子は「.h」になります。例えば、testAppクラスのヘッダーファイルはtestApp.hとなります。

ヘッダファイルに変数(= プロパティ)や関数(= メソッド)を記述する場合、それが公開されているのか(public)、それとも隠蔽されているのか(private)によって分けて記述されます。これがカプセル化の機能に相当します。publicの変数や関数は、「public:」という記述の後に、privateの変数や関数は「private:」という記述の後に宣言します。

ソースファイルは、そのヘッダーファイルで宣言された情報を元に、実際の処理の内容を記述していきます。拡張子は「.cpp」です。testAppクラスであれば、testApp.cppとなります。

UMLクラス図を作成する

XCodeでopenFrameworksのプロジェクトを開き、プロジェクトのアイコンを選択した上で、メニューから「設計」→「クラスモデル」→「クイックモデル」を選択します。すると、自動的に現在の状態を図に整理されます。この生成される図を、「UMLクラス図」と呼びます。それぞれの四角のブロックがひとつのクラスを表しています。クラスの中は3つの領域に分割されていて、上からそのクラスのクラス名、属性(プロパティ)、動作(メソッド)が記述されています。また、それぞれのクラスの継承関係も図の中で表現されています。

openFrameworksで、クラスを作成する

では、いよいよopenFrameworksで、オリジナルのカスタムクラスを追加して、それをtestAppから呼びだして操作してみましょう。

まずは簡単な例から、徐々に複雑なサンプルへと変化さえていきたいと思います。

円を表示するクラス

まずはじめに、円を表示するだけのシンプルな機能のクラスを作成してみたいと思います。円を表示するために下記のようなクラスを設計してみます。

  • クラス名:MyCircle
  • プロパティ:なし
  • メソッド:draw() – 円を描画する

XCodeに新規にクラスファイルを追加する

クラスを作成するには、まずXCode左コラムの「グループとファイル」のリストの中から「src」フォルダを右クリックします。表示されるメニューから「追加」→「新規ファイル」を選択します。

すると新規ファイルのテンプレートを選択する画面が表示されます。まず、「C and C++」>「C++ File」を選択し、ファイル名を「MyCircle.cpp」にします。その下にある「同時に”MyCircle.h”も作成」のチェックボックスをチェックします。保存場所やその他の設定はそのままで、「完了ボタン」を押します。この操作で、Sourceフォルダの中にMyCircle.hとMyCircle.cppの2つのファイルが追加されます。

クラスのヘッダーファイルのテンプレート

まず、クラスのヘッダーファイルを記述します。クラスのヘッダーファイルは、下記のような構造になっています。

クラステンプレート

#ifndef /* ユニークな名前 */ //インクルードガード
#define /* ユニークな名前 */

#include "ofMain.h" //ofMain.hをインクルード

class /* クラス名 */ { //クラスの開始

public: 
    //公開のプロパティ、メソッドを宣言する
    
private:
    //非公開のプロパティ、メソッドを宣言する

}; //セミコロンを忘れずに!!

#endif //インクルードガードの終了

最初の2行は「インクルードガード」と呼ばれる仕組みです。これはコンパイルする際にヘッダーファイルを二重に読み込んでしまわないようにするための仕組みです。#ifdef と #define の後ろには大文字の英文字とアンダーバー「_」を組み合わせて、そのクラスを定義する一意の名前を命名します。例えばMyCircleクラスの場合は「_MY_CIRCLE」などとするのが良いでしょう

また、openFrameworksの機能を利用したクラスを生成する場合には、必ず include文を使用して「ofMain.h」を読み込むようにします。この「ofMain.h」ファイルはopenFrameworksの様々な機能(アニメーション基本機能、図形の描画、音声、動画、フォントなど)を一括して読みこむためのヘッダーファイルとなっています。

では、このテンプレートにMyCircle.hの情報をあてはめてみましょう。

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {

public: 
    
private:

};
#endif

メソッドを追加する

では、ヘッダーファイルに円を描画するメソッドdraw()を追加してみましょう。draw() メソッドは、外部から参照される必要があるので公開(public)の関数として記述します。

ヘッダーファイルの関数は、下記のような記述のルールになっています。

戻り値の型 関数名(引数1, 引数2, 引数3...);

引数がない場合は、空白の括弧 () にします。また関数の戻り値がない場合は、「void」という記述を戻り値の型に記述します。このルールを守って、ヘッダーファイルにdraw() メソッドを記入します。

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {

public: 
    void draw();
    
private:

};

#endif

ソースファイルを記述する

次にヘッダーファイルで設計した情報に基いて、実際の処理内容を記述するソースファイルを記述します。ソースファイル内のメソッド(関数)は、「戻り値の型 クラス名::関数名(引数1, 引数2, 引数3 …) { 処理の内容 }」というような記述をする必要になります。例えば、MyCircleクラスのdrawメソッドの場合は下記のようになります。

void MyCircle::draw()
{
    /* 処理の内容 */
}

また、ソースファイルの先頭には必ず、include文を使用して自身のヘッダーファイルを読み込むようにします。

メソッド draw() の処理はまずはシンプルに ofSetColorで色を指定して、ofCircleで画面の中心に円を描画するようにしてみましょう。

MyCircle.cpp

#include "MyCircle.h"

void MyCircle::draw()
{
    //色を指定
    ofSetColor(31, 127, 255); 
    //画面の中央に円を描く
    ofCircle(ofGetWidth()/2, ofGetHeight()/2, 100);
}

UMLクラス図を確認

XCodeの機能として、現在作成しているプロジェクトのクラス図を自動作成する機能が搭載されています。この機能を利用して現在のクラズ図を書出してみましょう。XCodeのメニューから「設計」→「クラスモデル」→「クイックモデル」を選択します。すると、しばらくレンダリングした後にクラス図が生成されます。

これでMyCircleクラスの実装は完了です。

作成したクラスを使用する

クラスの実装は完了したのに、現在のプロジェクトをビルドしてみても画面には何も表示されません。冒頭のオブジェクト指向プログラミング解説に書いたようにクラスというのはあくまで設計図に過ぎません。クラスはインスタンス化という処理を行うことにより実体化して始めて、実際に機能するプログラムの機能単位として動きます。作成したクラスをインスタンス化するには、メインのクラスであるtestAppクラスからインスタンス化の処理をする必要があります。

インスタンス化

実際のインスタンス化の処理は簡単です。まずメインのクラスtestAppとMyCircleの定義を関連させるために、testApp.hの冒頭で、include文を使用してMyCircleのヘッダーファイルであるMyCirle.hを読み込みます。

その上で、変数を定義するようにクラスを宣言することでインスタンス化は完了します。例えばMyCircleクラスをインスタンス化して、circleというインスタンス(実体)を生成するには下記のように行います。

MyCircle circle;

ではtestApp.hにMyCircleのインスタンス化の処理を加えてみましょう。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスをインスタンスcircle
    MyCircle circle;
};

#endif

メソッドの実行

クラスのインスタンス化が完了したので、そのインスタンスのメソッドを呼び出すことでクラス内で定義された処理が実行されます。インスタンスのメソッドを呼びだすには、下記のように記述します。

インスタンス名.関数名(引数1, 引数2, 引数3...)

この書式に従って、testAppのdraw()関数より、MyCircleのdraw()メソッドを呼びだして円を描画してみましょう。

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //MyCircleクラスのインスンタンスcircleのdraw()メソッドを実行
    circle.draw();
}

/* 後略 */

また、プロジェクトのUMLクラス図も更新されています。testAppクラスにMyCircleのインスタンスcircleがプロパティとして加わりました。

プロパティを操作する

では、このMyCircleで描画される円の色、位置、半径をクラスの公開されたプロパティとして定義して、testAppから操作できるようにしてみましょう。それぞれのプロパティの名前と型を下記のように定義するものとします。

  • プロパティの内容 – プロパティ名:型
  • 円の色 – color:ofColor
  • 円の位置 – pos:ofPoint
  • 円の半径 – radius:float

プロパティを操作するには、公開された(public)プロパティを直接操作する方法と、非公開の(private)プロパティを、メソッドを介して操作するという2種類の方法があります。

Publicなプロパティを直接操作する

シンプルな方法は、操作したいプロパティをPublicとして宣言し、直接操作する方法です。Publicなプロパティを操作する方法はメソッドの実行とよく似ています。

インスタンス名.プロパティ = 設定する値;

この方法で、testAppから、MyCircleのcolor、pos、radiusのそれぞれを操作してみましょう。

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {

public: 
    //円を描く
    void draw();

    //円の色
    ofColor color; 
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
    
private:

};

#endif

MyCircle.cpp

#include "MyCircle.h"

void MyCircle::draw()
{
    //colorで指定された色を塗る
    ofSetColor(color.r, color.g, color.b); 
    //指定した場所に、指定した半径で円を描画
    ofCircle(pos.x, pos.y, radius);
}

testApp.h はそのまま変化なし。

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    
    //MyCircleのインスタンスcircleのプロパティを設定
    //位置
    circle.pos = ofPoint(200, 200);
    //色
    ofColor col;
    col.r = 255;
    col.g = 127;
    col.b = 31;
    circle.color = col;
    //半径
    circle.radius = 300;
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //MyCircleクラスのインスンタンスcircleのdraw()メソッドを実行
    circle.draw();
}

/* 後略 */

クラスMyCircleにプロパティが3つ追加されました。その結果として、このプログラムのUMLクラス図は下記のようになります。

プログラムを実行すると、指定した色、位置、半径で円が描画されます。

アクセサを介してプロパティの操作する – setterとgetter

クラスのプロパティを操作する方法として、もう一つの手法を紹介します。クラスのメソッドを介してPrivateなプロパティにアクセスするという方法です。プロパティを取得したり設定したりするメソッドのことを、アクセサ(Accessor)と呼びます。また、取得するメソッドのことをgetter、設定するメソッドはsetterと呼び、それぞれgetXXX、setXXXという関数名をにするのが一般的です。

アクセサを用いる利点はいくつか挙げられます。

  • 値のチェック:setterで値を設定する際に、不正な値をsetterの中でチェックして正しい値に修正することができる
  • デバッグの利便性:setterやgetterの中にログを出力する機能を備えておくと、デバッグの際にとても便利
  • 更新性:setterやgetterの処理内容が変化した際にも、呼び出し側ではなくsetter、getter側で対処することで、他のクラスへの波及を防ぐことができる

これらの利点を考えると、多少面倒でもアクセサを利用するほうが良いでしょう。先程の3つのプロパティ、color、pos、radiusを、アクセサを介して設定できるようにしましょう。今回は値を設定するだけなので、setterだけを定義してみます。それぞれsetColor()、setPos()、setRadius()というメソッドとなります。

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {

public: 
    //円を描く
    void draw();
    //アクセサ
    void setColor(ofColor color);
    void setPos(ofPoint pos);
    void setRadius(float radius);
    
private:
    //プロパティはprivateで宣言
    //円の色
    ofColor color; 
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
};

#endif

MyCircle.cpp

#include "MyCircle.h"

void MyCircle::draw()
{
    //colorで指定された色を塗る
    ofSetColor(color.r, color.g, color.b); 
    //指定した場所に、指定した半径で円を描画
    ofCircle(pos.x, pos.y, radius);
}

void MyCircle::setColor(ofColor _color)
{
    color = _color;
}

void MyCircle::setPos(ofPoint _pos)
{
    pos = _pos;
}

void MyCircle::setRadius(float _radius)
{
    radius = _radius;
}

testApp.h はそのまま変化なし。

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    
    //アクセサを介してcircleのプロパティを設定
    //位置
    circle.setPos(ofPoint(400, 600));
    //色
    ofColor col;
    col.r = 31;
    col.g = 255;
    col.b = 127;
    circle.setColor(col);
    //半径
    circle.setRadius(300);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //MyCircleクラスのインスンタンスcircleのdraw()メソッドを実行
    circle.draw();
}

/* 後略 */

MyCircleクラスにアクセサが追加されました。URLクラス図も変更されています。

複数のインスタンスを生成する

ひとつのクラスから複数のインスタンスを生成することも可能です。方法は簡単で、インスタンス名を変えて、それぞれに対してインスタンス化をするだけです。例えば、MyCircleクラスから3つのインスタンス、circle1, circle2, circle3を生成してみましょう。

MyCircle.hとMyCircle.cppはそのまま変化なし。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスのインスタンスを3つ生成
    MyCircle circle1, circle2, circle3;
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    ofColor col;

    //circle1のプロパティ設定
    col.r = 31;
    col.g = 255;
    col.b = 127;
    circle1.setColor(col);
    circle1.setPos(ofPoint(600, 400));
    circle1.setRadius(300);

    //circle2のプロパティ設定
    col.r = 255;
    col.g = 127;
    col.b = 31;
    circle2.setColor(col);
    circle2.setPos(ofPoint(300, 400));
    circle2.setRadius(200);
    
    //circle3のプロパティ設定
    col.r = 31;
    col.g = 127;
    col.b = 255;
    circle3.setColor(col);
    circle3.setPos(ofPoint(800, 200));
    circle3.setRadius(150);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //circle1, circle2, circle3それぞれを描画
    circle1.draw();
    circle2.draw();
    circle3.draw();
}

/* 後略 */

UMLクラス図

インスタンスの配列を生成する

クラスから生成するインスタンスの数が増えてくると、それぞれインスタンスとして生成して操作していくことに限界が生じてきます。いままで用いてきた複数の変数を配列にする方法と同じ方法で、クラスのインスタンスの配列を使用することも可能です。MyCircleの配列、circlesを生成して一気に大量のクラスを生成してみましょう。

MyCircle.hとMyCircle.cppはそのまま変化なし。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP
#define CIRCLE_NUM 100

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスのインスタンスの配列を生成
    MyCircle circles[CIRCLE_NUM];
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    ofColor col;
    
    //配列の数だけプロパティを設定
    for (int i = 0; i < CIRCLE_NUM; i++) {
        //ランダムな色に
        col.r = ofRandom(0, 255);
        col.g = ofRandom(0, 255);
        col.b = ofRandom(0, 255);
        circles[i].setColor(col);
        //ランダムな場所に
        circles[i].setPos(ofPoint(ofRandom(0, ofGetWidth()), ofRandom(0, ofGetHeight())));
        //ランダムな半径に
        circles[i].setRadius(ofRandom(10, 100));  
    }
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //配列の数だけ描画
    for (int i = 0; i < CIRCLE_NUM; i++) {
        circles[i].draw();
    }
}

/* 後略 */

UMLクラス図

今日のまとめ

今回の授業では、オブジェクト指向プログラミングの概念について解説しました。また概念を理解した上で、openFrameworksでカスタムのクラスを設計し、そのクラスのインスタンスを生成し、プロパティやメソッドを操作するということを行いました。

しかしながら、今回のプログラム例はあくまでオブジェクト指向プログラミングの理解のためのサンプルという意味合いが強く、あまり現実的なものではありませんでした。円を描くというだけの機能であれば、わざわざクラスに分割する必要性はないでしょう。

次回はオブジェクト指向プログラミングの後編ということで、より実践的なプログラムを通して、オブジェクト指向プログラミングの利便性をより強く実感するような内容を取り扱っていく予定です。またその過程でC++における最も難解な部分である、ポインタという概念や、メモリの管理について併せて学んでいきたいと思います。

サンプルファイルのダウンロード

今日とりあげたプログラムは全て下記のリンクからダウンロードできます。