コンテンツにスキップ

18.オブジェクト指向を学ぼう 1(クラス、フィールド、配列)

1.オブジェクト指向プログラミングとは?

Processingにおいてオブジェクト指向プログラミングは, 多くのものを同じように動かす際 に非常に便利な手法です。

たとえば,100個の円が同時に様々な方向に動くコード,多くの弾が行き交うシューティングゲームなどには,オブジェクト指向プログラミングにぴったりのテーマです。

これを読んでいる人の中には,Processingで「ブロック崩しゲーム」を作ったことがある人もいるかもしれませんが,それもオブジェクト指向プログラミングが活かせるテーマです。

ところで,Processingではいくつかの値をまとめて扱うには配列を使うことができます。

参考:Processing:配列を使おう

//配列の例
float[] floatArray = {0.5, 0.8, 9.0, 2.25};
int[] intArray = new int[3];
intArray[0] = 4;
intArray[1] = 8;
intArray[2] = 1;

配列でも,多くのデータを効率よく整理することができました。 100個の円の座標を保存することならば配列でもできます

なぜオブジェクト指向を使うと「多くのものを同じように動かす際に便利」なのでしょうか。配列ではだめなのでしょうか?

同じものを「配列のみ」使ったものと「オブジェクト指向」を使ったもので実装しながら比較してみましょう。

2.プログラム1-1

for文と配列を用いて,さまざまな大きさと色の玉が上へ上がっていくコードを一緒に書いてみましょう。

まずは,玉の数を変数 n で指定し,それぞれのx座標とy座標を格納する配列を宣言しましょう。

int n = 100;
float[] x = new float[n];
float[] y = new float[n];

ためしに玉を表示するようにコードを完成させてしまいましょう。

玉の個数をn とすることで,配列の大きさ(要素の個数)やfor文にわざわざ一度書いた数字を繰り返さなくて済みます。

配列の定義の仕方や,for文の書き方も不安があれば確認しましょう。

int n = 100;
float[] x = new float[n];
float[] y = new float[n];
void setup(){
size(1000, 600);
for(int i=0; i<n; i++){
x[i] = random(width);
y[i] = random(height);
}
}
void draw(){
for(int i=0; i<n; i++){
ellipse(x[i], y[i], 10, 10);
}
}

それから,玉の大きさや色に関する情報, 速度を追加していきましょう。 位置を配列x, 配列yに,速度を配列vyに入れています。

draw部分が一回実行されるたびに,玉がvyだけ動くように,ellipse関数で表示した後に y[i] -= vy[i] と書きました。 玉には上に動いて欲しいので,マイナスしています。 非常に短い時間で動く量なので random(1,3) が適切でしょう。

//プログラム1-1
int n = 100;
float[] x = new float[n];
float[] y = new float[n];
float[] vy = new float[n]; //変更箇所
float[] size = new float[n]; //変更箇所
float[] gray = new float[n]; //変更箇所
void setup(){
size(1000, 600);
noStroke(); //変更箇所
for(int i=0; i<n; i++){
x[i] = random(width);
y[i] = random(height);
vy[i] = random(1,3); //変更箇所
size[i] = random(5, 40); //変更箇所
gray[i] = random(255); //変更箇所
}
}
void draw(){
background(255);
for(int i=0; i<n; i++){
fill(gray[i]); //変更箇所
ellipse(x[i], y[i], size[i], size[i]); //変更箇所
y[i] -= vy[i]; //変更箇所
}
}

大きさや色についての配列を追加したので少しごちゃごちゃしましたが, ひとまず,配列を使ったやり方はここで終了です!

それではいよいよオブジェクト指向を見ていきましょう。

3.オブジェクト指向プログラミングの考え方

多すぎる配列は面倒!

それではいよいよ オブジェクト指向プログラミング(object oriented programming以下、OOP) をしていきましょう。

今までは,複数のデータを扱いたい時は配列を用いていました。 コードの中で 配列が全く出ない,または1つか2つしか出ないコードにはOOPはそこまで光りません

OOPが活用できるのは,「データの種類」が増えた時です。

先ほどのプログラム1-1で作成したコードがいい例です。 x座標,y座標,速度…など,データの種類が増えていきましたね 。(行頭の配列の宣言だけで5行です ! ) 今後色をフルカラーにしたり,x方向に動かしたくなった時,配列の数はより増えていくでしょう。

(ここからは少し抽象的な話になります。 意味があまりわからなくても大丈夫です! 正直な話,オブジェクト指向プログラミングは実際に何回か書いてみないと理解できないところがあります…)

ここで少し立ち止まって,先ほどのdrawの中の数行をみてみましょう。

fill(gray[i]);
ellipse(x[i], y[i], size[i], size[i]);

直感的に考えましょう。

「データxから円iのものを,データyから円iのものを,…」 という風にデータを参照するのは,このコードにのみ言えたことではありませんが,読む順番が逆だとは思えませんか? それぞれの値は,円 i が持つ情報のはずです!

//!これだけでは動きません!
fill(円iのgray);
ellipse(円iのx, 円iのy, 円iのsize, 円iのsize);

さきほど作ったものは元々「円iが,x,y,size…といった情報を持っている」というアイデアがありました。分かりにくいかもしれないので図で表します。

配列のみで書いたコードは図の左側のようになっていました。 しかし,右側の図のように,円に注目した設計ができればより直感的なコーディングが可能です。 これを可能にするのがOOPです。

オブジェクト指向プログラミングの考え方

さて,プログラム1-1では,円は「座標,速度,色」などの値を持っています。 OOPでは,「座標,速度,色」などのデータを持つ円を「オブジェクト」として扱います。

object 〔視覚や触覚で感知できる〕物、物体【名】

オブジェクトは,物体,物という意味です。 ここでは,「なにかふわふわした意味合いをもつ存在」として考えましょう。

大切なのは,同じオブジェクトは,同じ種類のデータを持っている。ということです。 OOPでは,【オブジェクト】と,それが持つ『データ』(いわゆる変数です)を決めます。

さきほどの【円】は,『座標,速度,白黒の色,大きさ』のデータを持っていました。 例えば【音楽】というオブジェクトは,『テンポ,調,メロディ,歌詞』などのデータを持っていると言えます。

OOPは,実際に例を見ないとピンとこないものではあります。いよいよ実装に入ってみましょう。

4.プログラム1-2

先に結果からお見せします。ある一つの玉が上がっていくプログラムです。 (これから書く内容は先ほどの配列を使ったコードと同じなので,見比べながら進めていきましょう)

//プログラム1-2
class Circle{
float x;
float y;
float vy;
float size;
float gray;
}
Circle c = new Circle();
void setup(){
size(1000,600);
noStroke();
c.x = random(width);
c.y = random(height);
c.vy = random(1, 3);
c.size = random(5, 40);
c.gray = random(255);
}
void draw(){
background(255);
fill(c.gray);
ellipse(c.x, c.y, c.size, c.size);
c.y -= c.vy;
}

OOPにおいて, これまでのProcessingの書き方は特別に変わりません。 行の後ろにはセミコロン「;」がつきますし,変数の代入はイコール「=」で行います。

OOPでは,新しく,class というものが登場します。

//クラスの宣言
class Circle{
float x;
float y;
float vy;
float size;
float gray;
}

上の部分で, 「Circleクラスのオブジェクトは,x, y, vy, size, grayという値を持つ」 ということを宣言しました。

ここで,たった今宣言した「x, y, vy size, gray」は,フィールドと呼びます。 今まで使ってきたような「変数」と同じ定義の仕方ですが,呼び方で区別しましょう。

! 以下のポイントを確認しましょう。

  • class クラスの名前{} という形をとります。その形さえ取れば,中身や名前は自由です。
    • クラスの名前(上の例では Circle )は自由です。最初を大文字にするのが習慣ですが,そうする必要もありません。
  • 2行目以降の括弧内に書かれたものが,オブジェクトの持つフィールド(要するに変数)です。変数と同じ形を取ります。
    • 基本は変数と同じです。 int abc = 123; といった風に,はじめから値を代入することもできます。
  • 開いた中括弧は閉じますし,行の後ろにはセミコロン【 ; 】が無いとエラーが起きます。基本は変わりません。

続いて,9行目のこちらも見てみましょう。

//インスタンス化
Circle c = new Circle();

ここでは,「cにCircleの性質を持たせる」ことを宣言しています。 この「インスタンス化」と呼ばれる動作をすることで,フィールドの定義が行われます。 つまり,これにより cは,「座標,速度,白黒の色,大きさ」を示す情報を持つことができます

こうして作られた, 「Circleの情報を持ったc」を,インスタンスあるいはオブジェクトと呼びます 。インスタンスを作成するこの動作を「インスタンス化」と(ときどきnewするとも)呼びます。

同じクラスからインスタンスは複数作ることができます。 また,変数を宣言してあとで値を代入するように,オブジェクトを宣言して,newするのも可能です。

Circle c2 = new Circle()
Circle c3;
c3 = new Circle();

instance【名】 コンピュータの分野においては主に「実体」や「実例」「事例」という意味で使われる。

= new Circle(); はひとまず第二回で解説します。 インスタンス化が 無いと「Null Pointer Exception」とエラーが起きます。setup()の中で,存在していないxやyを参照しようとしているためです。

ところで, Circle c = new Circle(); が,今までの変数の宣言( int a; , float b = 3.14; )と同じような形を取っているのに気づきましたか? int型で宣言した変数は,整数の性質しか持てませんでしたし,float型で宣言した変数も同様でした。 OOPおよびクラスは,ある意味型を作っていると言えます。


//一部抜粋(これだけでは動きません)
c.x = random(width);
c.y = random(height);
~~~中略~~~
ellipse(c.x, c.y, c.size, c.size);

クラスで宣言したフィールドへのアクセスは,インスタンスに「.(ドット)」をつけて呼び出せます。

ここまでの流れをまとめしょう。また,これらをふまえて,プログラム1-2 を確認してみましょう。

  • オブジェクトの性質は,クラスに記述します。
  • 実際に表示される一つ一つの円は「オブジェクト」「インスタンス」と呼びます。 宣言にあたる,インスタンス化は, クラス名 オブジェクト名=new クラス名() にて行います。
  • オブジェクトが持つそのクラス特有の値を,フィールドと呼びます。 インスタンス.フィールド名 で参照できます。

5.オブジェクトの配列

さて,再び配列の話です。 int型の変数の宣言は,以下のような形でした。

int a;

int型の配列は以下のように宣言しました。

int[] a_array = new int[]

さて,以下に示すCircleクラスのオブジェクトの宣言をみてみましょう。 オブジェクトは,インスタンス化することで使えるようにしていました。

//classの定義
class Circle{
float size;
}
//インスタンス化
Circle c1 = new Circle();

オブジェクトも,int型やfloat型のデータと同じように,配列でまとめることができます。

//オブジェクトの配列
Circle[] c = new Circle[20];

int型の配列の宣言と全く同じ文法をとっているのが分かりますか? ここで注意しなければならないのが, 「大きさ(要素数)が20のCircleオブジェクトの入った配列の宣言」 のみが行われただけで, 「インスタンス化」 はまだされていない,ということです。

配列を用いないOOPの場合

Circle a; で宣言

a = new Circle() でインスタンス化

a.size へアクセスが可能

配列を用いたOOPの場合

Circle[] a = new Circle[n]; で宣言

a[i] = new Circle() でインスタンス化(iには0以上の整数が入ります。)

a[i].size へアクセスが可能

というふうに区別できます。

最終的に全てのインスタンス化をするにはこのようになりますね。

//オブジェクトの配列の定義と中身のインスタンス化
int n = 20;
Circle[] c = new Circle[n];
for(int i=0; i<n; i++){
c[i] = new Circle();
}

6.プログラム1-3

オブジェクトの配列を用いて,この記事の最初に行った,プログラム1-1と同じ物を作成してみましょう。ここまで来たならもう大丈夫です!

同じデータ(座標,速度,色,,,)を持った円をオブジェクトとしてクラス書くと,以下のようになりますね。

//クラスの定義
class Circle{
float x;
float y;
float vy;
float size;
float gray;
}

setup()とdraw()は以下のようになります。

//メイン部分
int n = 100;
Circle[] c = new Circle[n];
void setup(){
noStroke();
size(1000,600);
for(int i=0; i<n; i++){
c[i] = new Circle();
c[i].x = random(width);
c[i].y = random(height);
c[i].vy = random(1, 3);
c[i].size = random(5, 40);
c[i].gray = random(255);
}
}
void draw(){
background(255);
for(int i=0; i<n; i++){
fill(c[i].gray);
ellipse(c[i].x, c[i].y, c[i].size, c[i].size);
c[i].y -= c[i].vy;
}
}

7.おわりに

モノ(オブジェクト)を自分でクラスで作り,それをベースにプログラミングをしていくオブジェクト指向プログラミングについて,少しはつかめたでしょうか?

まだプログラム1-1の内容なら配列だけで実装した方が楽に思えるかもしれませんね?

第二回では,よりOOPの便利な機能を紹介しつつ,コードを書いていこうと思います。

変数,関数,条件分岐,繰り返し文の理解は重要です。 そして,OOPを理解することはプログラミングを理解する最後のピースです。

OOPは実践を繰り返さないとわからないアイデアではあります。 頑張って取得しましょう!

8.参考

技術評論社, 著:うえはら, 監修:竹林暁, 初心者でも「コード」が書ける! ゲーム作りで学ぶ はじめてのプログラミング, https://gihyo.jp/book/2019/978-4-297-10579-2