第9回 点の表現7
【表現的側面】運動の記述
【技術的側面】モジュール化 (関数)、クラス、メソッド
今回は、かなり強引ですが、モジュール化(関数)からクラスまで一気にやってみたいと思います。
かなり混乱も予想されるのですが、モジュール化とクラスは考え方としては同じものなので、一緒にやった方が理解しやすいと思います。
モジュール化 (関数)
まずはモジュールとは、
電気機器やコンピューター装置などの、構成要素の単位。独立の完成した機能をもち、交換や着脱が可能で、より大きなシステムに構成される。
(大辞泉より引用)
とあります。簡単に言ってしまえば「部品化」ということです。自動車の部品がそれぞれが独立していて異なる車種にも使われるように、プログラムも部品化してやれば他のプログラムに再利用することが可能になります。
また、このモジュール化の最大の利点は「プログラムが見やすくなる」ということです。
そして、その部品化されたものを関数(function)と呼びます。
なぜ日本語の名前として「関数」と付けられたのかはよく分からないのですが、英語名のfunction(機能)から、その性質はよく分かりますよね。
以下がその概念図です。
メインのプログラム内で関数(function)がその都度実行されています。
ここで気を付けなければいけないのは、この考え方が適用できるのは、C言語やBasicなどの手続き型言語になります。手続き型言語は、プログラムが上から下に順番に実行されていく形のものです。JavaやC++などのオブジェクト指向言語はまた違った概念になります(下の方で解説してます)。
また、式は以下の形になります。
型 関数名( 型 引数, 型 引数, 型 引数 ・・・)
ここで、引数という関数に値を渡す機能が出てきます(例題として出てます)が、この引数はいくつでも作ることができます。
と、概念的なことだけ説明されても分からないでしょうから、ここで例題をやってみましょう。以下は、直径最大5ピクセルの円を画面上に描くプログラムです。
(以上のようなグラフィックスが生成される)
void setup()
{
size(400, 400);
background(255);
noStroke();
smooth();
ellipseMode(CENTER); //中心に描画
}
void draw()
{
//ここは画面の描画だけをしている
}
void mouseDragged()
{
//pmouseとは、現在のマウスの位置の一つ前のマウスの位置を指します。
//よって、現在の位置から前の位置を引くことによってマウスの移動の早さを検出することができます
float speed = abs(mouseX-pmouseX) + abs(mouseY-pmouseY); //マウスの早さを検出
fill(255-speed*10); // マウスの早さを線の色にする
//speedの値を制限することによって、円の大きさを最大5までに制限
if(speed > 10) speed = 10;
ellipse(mouseX, pmouseY, speed, speed); //マウスの位置で円を描画
//println(speed);
}
これをモジュール化するとどうなるでしょうか。ちょっと強引ですが、以下のようにまとめてみます。
void setup()
{
size(400, 400);
background(255);
noStroke();
smooth();
ellipseMode(CENTER); //中心に描画
}
void draw()
{
//ここは画面の描画だけをしている
}
void mouseDragged()
{
//pmouseとは、現在のマウスの位置の一つ前のマウスの位置を指します。
//よって、現在の位置から前の位置を引くことによってマウスの移動の早さを検出することができます
variableEllipse(mouseX, mouseY, pmouseX, pmouseY); //様々な大きさ、色の円を描く関数
}
/////////円の描画関数////////////////////
void variableEllipse(int x, int y, int px, int py)
{
float speed = abs(x-px) + abs(y-py); //マウスの早さを検出
fill(255-speed*10); // マウスの早さを線の色にする
//speedの値を制限することによって、円の大きさを最大5までに制限
if(speed > 10) speed = 10;
ellipse(x, y, speed, speed); //マウスの位置で円を描画
//println(speed);
}
このように、円の描画の部分だけ抜き出して関数にしています。
実際のところ、この程度のコードなら、わざわざ関数にする必要もないのですが、関数の利点は、コピペで他のプログラムに簡単に移植できるというところですね。
また、関数は値を返すこともできます。以下はBoolean型の値を返す関数が使われている例です。
object_pressed_circle
練習問題
それでは、練習問題をやってみましょう。bounceX()とbounceY()という、折り返しをする機能を持った関数を作ってみて下さい。
int eSize = 5; //オブジェクトのサイズ
int xSpeed = 5; //オブジェクトのスピード
int ySpeed = 3; //オブジェクトのスピード
int x = 0, y = 0; //オブジェクトのx座標
//初期化
void setup() {
size(400, 400);
noStroke(); //輪郭を描かない
frameRate(30); //再生速度を1秒30フレームに設定(初期値は1秒60フレーム)
smooth(); //オブジェクトにアンチエイリアスをかける
}
//プログラムが終了するまで、draw()の中は繰り返される
void draw() {
background(255); //背景は白
x = x + xSpeed; //xの値にxSpeedを足す
y = y + ySpeed; //xの値にxSpeedを足す
//もし、xの値が画面の右端より大きくなるか、左端より小さくなった場合、
if(x > width || x <0) {
xSpeed = -xSpeed; //xSpeedの正負を入れ替える
}
if(y > height || y <0) {
ySpeed = -ySpeed; //xSpeedの正負を入れ替える
}
fill(0); //黒で描く
ellipse(x, y, eSize, eSize); //円を描く
}
解答
int eSize = 5; //オブジェクトのサイズ
int xSpeed = 5; //オブジェクトのスピード
int ySpeed = 3; //オブジェクトのスピード
int x = 0, y = 0; //オブジェクトのx座標
//初期化
void setup() {
size(400, 400);
noStroke(); //輪郭を描かない
frameRate(30); //再生速度を1秒30フレームに設定(初期値は1秒60フレーム)
smooth(); //オブジェクトにアンチエイリアスをかける
}
//プログラムが終了するまで、draw()の中は繰り返される
void draw() {
background(255); //背景は白
x = x + xSpeed; //xの値にxSpeedを足す
y = y + ySpeed; //xの値にxSpeedを足す
xSpeed = bounceX(x, xSpeed);
ySpeed = bounceY(y, ySpeed);
/*
//もし、xの値が画面の右端より大きくなるか、左端より小さくなった場合、
if(x > width || x <0) {
xSpeed = -xSpeed; //xSpeedの正負を入れ替える
}
if(y > height || y <0) {
ySpeed = -ySpeed; //xSpeedの正負を入れ替える
}
*/
fill(0); //黒で描く
ellipse(x, y, eSize, eSize); //円を描く
}
int bounceX(int bx, int xSp) {
if(bx > width || bx <0) {
xSp = -xSp; //xSpeedの正負を入れ替える
}
return xSp;
}
int bounceY(int by, int ySp) {
if(by > height || by <0) {
ySp = -ySp; //xSpeedの正負を入れ替える
}
return ySp;
}
この関数は非常に強引に作ってしまったので、通常だとこのような関数は書かないでしょうが、勉強ということで。
さて、次のステップはクラス、メソッドです。
これは、「オブジェクト指向言語」の中で使われる概念です。オブジェクト指向は何??という人も多いかもしれませんが、これは、「オブジェクト単位によって設計された」と考えて下さい。
オブジェクト単位ということをもう少し説明しましょう。
オブジェクト指向言語でのクラスとは「型」のことを指します。例えば紅葉まんじゅうの型ですね(人形焼きでもいいんですけど)。
「もみまんの歴史」より参照
http://www.aa.alpha-net.ne.jp/usaco3/momiman/rekisi.htm
重要なのは、紅葉まんじゅうの型(クラス)は同じなのですが、出来たまんじゅう(オブジェクト)はちょっとずつ違うということです。当然焼き色も違うし、中に入っている餡の量も違うでしょう。
このように、オブジェクトは一つの型(クラス)を元にしていますが、それぞれに個体差を付加することができます。
概念的には以下のような図になります。
このように、オブジェクト指向とは、現実の世界でのものの捉え方(車、人間、犬など)に近い考え方でプログラミングを行なうもだと考えていいと思います。
ここで注目して欲しいのは、オブジェクトはそれぞれメソッドを持っていることです。メソッドとは、既に習った関数(function)と機能的には同じだと考えていいでしょう。つまり、手続き型言語における関数がオブジェクト指向言語におけるメソッドなのです。
また、オブジェクトはインスタンス(実体)とも呼ばれます。
例題
それでは、実際のプログラムはどのようにすればいいのでしょう。ここまで散々点をやって来たので、今回も点を基本にして考えてみましょう。
まずは点の型(クラス)を作ってみましょう。
以下は、横方向に設定されたスピードで移動する円を生成するクラスです。
//moveCircleクラスの宣言
class moveCircle {
float xPos, yPos, speed;
moveCircle (float y, float sp) { //初期化用のメソッド(コンストラクタ)
xPos = 0.0; //x座標の初期値は0.0
yPos = y; //y座標の初期値はオブジェクトを宣言した際の引数になる
speed = sp; //speedの初期値もオブジェクト宣言の際の引数
}
//円の位置を更新するメソッド
void update() {
xPos += speed; //speed分のx座標を移動
if (xPos > width) { //円のx座標がウィンドウの幅を超えたら、
xPos = 0; //位置を0に戻す
}
ellipse(xPos, yPos, eSize, eSize); //円を描く
}
}
上の式で分かりづらいのは、初期化用のメソッド(コンストラクタ)でしょう。これは、そのクラスを初期化するために同じ名前のメソッドは必ず入れなければならないという規則があるのだと考えて下さい。
updateメソッドを実行するたびに、円のx座標であるxPosの値にspeedが足されていきます。完全なコードは以下の通りです。
int eSize = 5; //オブジェクトのサイズ
moveCircle c1, c2; //moveCircleクラスのオブジェクト(インスタンス)を宣言
void setup()
{
size(200, 200);
smooth(); //図形の輪郭をきれいに描画
fill(0); //黒で描画
c1 = new moveCircle(20, 1.0); //オブジェクトを作成
c2 = new moveCircle(50, 3.5);
}
void draw() {
background(255); //背景は白
c1.update(); //c1, c2オブジェクトのupdateメソッドを実行
c2.update();
}
//moveCircleクラスの宣言
class moveCircle {
float xPos, yPos, speed;
moveCircle (float y, float sp) { //初期化用のメソッド(コンストラクタ)
xPos = 0.0; //x座標の初期値は0.0
yPos = y; //y座標の初期値はオブジェクトを宣言した際の引数になる
speed = sp; //speedの初期値もオブジェクト宣言の際の引数
}
//円の位置を更新するメソッド
void update() {
xPos += speed; //speed分のx座標を移動
if (xPos > width) { //円のx座標がウィンドウの幅を超えたら、
xPos = 0; //位置を0に戻す
}
ellipse(xPos, yPos, eSize, eSize); //円を描く
}
}
どうでしょうか?c3、c4と、いくつもオブジェクトを増やしてみて下さい。
練習問題
次はもう少し複雑になります。ドラッグして投げると、円が投げた方向とスピードを記憶していて、そのまま画面内をバウンスし続けます。
上のクラスは、それぞれ生成された円が、マウスの投げるスピードによってその円自体のスピードを決定できるというものです。
練習問題としてはかなり難しいかもしれませんが、Circleクラスの中を記述して、プログラムが動くように変更して下さい。
// 円の生成のサンプル
int count = 0; //マウスクリックによって生まれる円の番号
//Circleクラスのcirclesオブジェクトを宣言
Circle[] circles = new Circle[20]; //20個のオブジェクトを作成
void setup()
{
size(200,200);
background(255);
smooth();
// circlesオブジェクトの生成と配列の初期化
for (int i = 0; i < circles.length; i++) circles[i] = new Circle();
}
void draw()
{
background(255);
//常にcircles[]を更新
for (int i = 0; i < circles.length; i++) circles[i].update();
}
void mouseDragged () {
circles[count].x = mouseX;
circles[count].y = mouseY;
}
//マウスドラッグによるイベント
void mouseReleased () {
circles[count].xSpeed = mouseX - pmouseX;
circles[count].ySpeed = mouseY - pmouseY;
count ++; //順番に更新
if(count >= circles.length) count = 0;
}
// Circle クラスを作成
//////////////////////////////////////////////////////////
class Circle {
float x; //円のx座標
float y; //円のy座標
float radius; //円の直径
float xSpeed; //x軸方向のスピード
float ySpeed; //y軸方向のスピード
//初期化用のメソッド(コンストラクタ)
Circle()
{
xSpeed = 0.0; //スピードの初期値は0.0
ySpeed = 0.0;
x = width/2; //x,yは画面の中心に設定
y = height/2;
radius = 5.0; //円のサイズを設定
}
//円の描画更新用メソッド
void update()
{
// speedで設定された値を足す
x += xSpeed;
y += ySpeed;
if(x > width || x < 0 ) xSpeed = -xSpeed;
if(y > width || y < 0 ) ySpeed = -ySpeed;
//noStroke();
fill(0);
ellipse(x,y,radius,radius); //円を描画
}
}
【サンプル】object_pressed_circle
int circleX, circleY; // 円の位置
int eSize = 100; // 円の直径
boolean circleEnter = false; //マウスカーソルが円のなかに入ったかを判定
int bgColor = 0; //背景色の設定(初期値は黒)
void setup()
{
size(400, 400);
background(0);
noStroke();
smooth();
circleX = width/2; //円は、画面の中央に表示
circleY = height/2;
ellipseMode(CENTER);
}
void draw()
{
if(enterCircle(circleX, circleY, eSize)) { //もし、マウスカーソルが円の中に入ったら
circleEnter = true;
fill(200); //円の色を200のグレーに変更
}
else {
circleEnter = false;
fill(100); //そうでなかったら100のグレー
}
ellipse(circleX, circleY, eSize, eSize); //円の描画
}
void mousePressed()
{
if(circleEnter) { //もし、マウスカーソルが円の中に入ったら
//背景色が黒の場合は白、白の場合は黒に変更
switch(bgColor) {
case 0:
bgColor = 255;
break;
case 255:
bgColor = 0;
break;
}
background(bgColor); //背景色を設定
}
}
//マウスカーソルが円の中に入ったかどうかを判定する関数
boolean enterCircle(int x, int y, int diameter)
{
float disX = x - mouseX;
float disY = y - mouseY;
//sqrt関数を使って、円の中心からの距離を算出
if(sqrt(sq(disX) + sq(disY)) < diameter/2 ) {
return true;
}
else {
return false;
}
}
【課題】今まで自分が書いたコードを関数を使って書き直す。3つ以上書くこと。
参考デモンストレーション: 画像処理
【表現的側面】コンピュータヴィジョンによる様々な表現
【技術的側面】2値化、エフェクト、背景差分、物体の追跡