[Go to “teaching” in Morimoto Lab]

Game by ball 6

ArrayList(便利な配列)について学習する。


1. 動的な配列

Static array(静的配列): これまでに習った、定数によってデータ数を指定する配列。データ数は途中で変動しない [GameByBall2でやりました]
Dynamic array(動的配列): しかし、以下のような場合どう実装しましょう?

こういう場合、データ数を柔軟に変動できる配列があると便利です。

ProcessingではArrayListを使うことでそれを実現できます。
以下のコードにて、静的配列と動的配列の違いを見てみましょう。

//↓ 定義と生成(new)
PVector [] ballsStatic = new PVector[5];
ArrayList<PVector> balls = new ArrayList<PVector>();
void setup() {
size(1000, 800);
for (int i=0; i < 5; i++) {
//↓ 2回目のnew:クラスのインスタンスの生成
ballsStatic[i] = new PVector(i*110, 300);
balls.add(new PVector(i*110, 100));
//↑★0: addの仕方について
}
noFill();
strokeWeight(5);
}
void draw() {
background(255);
//↓ 静的配列 (緑の丸)
stroke(0,128,0);
//↓★1: .lengthは静的配列のデータ数の変数
for (int i=0; i < ballsStatic.length; i++) {
ellipse(ballsStatic[i].x, ballsStatic[i].y, 100, 100);
}
//↓ 動的配列 (赤い丸)
stroke(255,0,0);
//↓★2: .size() データ数を返すメソッドへのアクセス。
for (int i=0; i < balls.size(); i++) {
//↓★3: .get() i番目のボール情報を返すメソッドへのアクセス。
PVector pv = balls.get(i);
//↓★4: メンバ変数へのアクセス
ellipse(pv.x, pv.y, 100, 100);
//↓★5: 3&4の代わりに以下のようにも書けるが、3&4の書き方の方が個人的にはわかりやすい。
//ellipse(balls.get(i).x, balls.get(i).y, 130, 130);
}
}
void mouseClicked() {
//↓★6 もし配列のデータ数が0個だったら、それ以上データを消せず、エラーが起きます
if (balls.size()>0) {
//↓★7
balls.remove(0);
}
print(balls.size()+", " );//★See the console
}

★0: add() の引数に加えたいPVectorの変数等を指定します。
サンプルコードのやり方以外に以下のやり方がストレートです

PVector a = new PVector(100,100);
balls.add(a);

が、一行増えるので、サンプルのような以下の方法が好まれます。

balls.add(new PVector(100,100));

★1: length は静的配列のデータ数を表す変数です
★2: size() は動的配列のデータ数を返すメソッドです
★3: get(i) は動的配列のi番目のデータを返すメソッドです
※注意:
get()で代入した変数の値を変えると、元のballsのデータも変更されるので注意しましょう。下記「シャローコピーとディープコピー」を参考。
★4: メンバ変数へのアクセス
★5: 3,4 or 5、どちらの方法でもやっていることは同じですが、個人的には3,4の方が(1行増えても)見やすい&書きやすいです。
★6: もし配列のデータ数が0個だったら、それ以上データを消せず、エラーが起きます
それを防ぐためにはこのような記述が必要です。
動的配列の isEmpty() のメソッドを使えば、データが空かどうか、boolean (true or false)で知ることができます

if( balls.isEmpty() == false ) balls.remove(0);//使い方の例1
if(!balls.isEmpty()) balls.remove(0);//使い方の例2

★7: remove(i) はi番目のデータを消す動的配列のメソッドです
詳細はArrayListのJavaの公式ページを見てください(ProcessingはJavaベースの言語なのでこちらが本元)

シャローコピーとディープコピー
上記の★3に関連して、動的配列のballs.get(i)で代入した変数(ここでのpv)の値を変えるとballsのi番目のデータ自体も変更されてしまいますので、これは大変注意が必要です・・!

こういったコピーをシャローコピーと言い、実態は一つしかなく、他の変数に代入しても一つの同じ実態を扱うことになります。
対して、ディープコピーは同じ値を持った変数をもう一つちゃんと作りますので、実態は二つに増えます。
【参考リンク:シャローコピー(shallow copy)とは】

PVectorではディープコピーをするためのcopy()メソッドがあるので、もしシャローコピーで不都合があるなら、
pv = balls.get(i).copy();
とすると別の実態が作られます。
自分でディープコピーするなら、
PVector pv = new PVector();
pv.x = balls.get(i).x;
pv.y = balls.get(i).y;
または
PVector pv = new PVector(balls.get(i).x, balls.get(i).y);
とするとディープコピーになります(そりゃそうだ)。
※クラスにたくさんメンバ変数があると大変ですね・・!

シャローコピーのいいところもあります。
実態が一つしかないのでメモリを食わないですし、プログラムの可読性をあげられます。
ただしちゃんと理解していないと逆効果でバグになりえます。

Ex A. ArrayListを使って、クリックしたところにボールを加えましょう

Ex B. ArrayListを使って、クリックしたところにボールがあったらそのボールを消しましょう

マウスクリックしたところにボールがあるかどうか、Game by ball (1)で行った衝突判定で計算しましょう。

  1. マウスをクリックしたら、マウスとボールの距離を計算する。まずremoveなどせずに、クリックしたら全てのボールとの距離を計算し、その数値を見て確認してみるとよいでしょう。

  2. 距離がボールの半径より小さいとき、衝突したと判定してそのボールをremoveしましょう

注意:for文で1から順に全てのボールをremoveしようとしたら変なことに?
今回、これを必ずしもこれを実装しなくても課題はOKとしますが、プログラムが変な挙動の時に気になると思うので解説します。

for文で配列の0番目から順に全てremoveしようとすると、全て削除されず、一つ飛ばしに削除されます(以下のだるま落としのイメージで言うと、配列のデータは下から順に0,1,2…となっています)。なぜかというと、0番目のボールをremoveすると、それまで1番目だったボールが0番目に詰められます。しかしfor文でi++などとしていると、i=0でボールを削除した次の処理はi=1の時になるので、元々1番目だったボールが削除されません😱。これを回避するにはいくつか方法がありますが、一つは

for( i=balls.size()-1 ; i >= 0 ; i– ){…}

などとして、後ろから順に削除を実行することです。すると「ボールを前に詰める」ことがないので、ボールの位置が変更することがなく、思った通りの挙動になります。この時はだるま落としで言うと、上から順に飛ばしていくイメージですね。(最初に頭がなくなるだるま落としって・・・)。

ちなみにですが、全て消したい場合はArrayListのclearメソッドが便利です。

また、今回の課題ではボールが複数重なっているときには上記のことが理由で、一度に複数消せないことがあります。余裕のある人はこのことに考慮して、重なったボールを一度に複数消せるように対応してもらえればと思います。
Alt text【図解:0青、1ピンク、2緑、3黄色、4赤、だとすると?】
動的配列のremoveをfor文で使うときのデータ位置の変更に注意!(image url)

Ex C. 自作のボールクラスと動的配列を使って、ボールを増やしたり消したりするプログラムを作りなさい。

これまでPVectorの配列を使っていましたが、自作のクラスの動的配列に変更してプログラムを作りましょう。

Points: マウス関係のメソッドをクラス内から呼ぶことについて

  1. mouseClicked()やdraw()などのProseccingが元から用意している特殊な関数はクラス内で使う(定義・記述する)と、同じ名前でも特殊性を失った普通のメソッドになるので注意してください。

  2. mousePressed()はクラスの中で呼ぶことはできますが、あまりクラスの中で使うのはおすすめしません。draw()からの呼び出しに限定してあげた方がわかりやすいように思います。

Point: ボールを減らしたときに追加したくない人へヒント(任意でOK)。
この場合、マウスの座標一点と一つでもボールが衝突しているかどうかを判定するbooleanの変数を一つ用意し、それを用いてaddするかどうか決めるとよいでしょう。
(booleanの使い方がまだピンとこない人は、intの変数でもいいです。衝突した際に、最初の値と違う値を与えれば、あとから衝突があったかどうかわかります。)

Ex D. 自作のクラスを使って、インタラクティブに要素の数が増減することを利用した面白いアプリを作成してください。

ExCに新たな工夫を加えて、★(ゲーム/インタラクション/ジェネラティブアート/数式/アニメーション)の要素(複数でも)を入れてください。

下記の例は、先週と同じ例ですが、クリックしたら増える、減る+目や赤い玉がアニメーションしていますね。
これを真似してみてもいいと思います。

そういえば大阪万博のロゴマーク、「いのちの輝きくん」がTwitterで色々とアレンジされていました。
今回の課題の参考に下記のURLでぜひ遊んでみてください!


☕コーヒーブレークにならない、ちょっとややこしい話

1. 静的配列でもnewすれば数が変えられるのでは?(new できるものは動的配列なのでは?)→Yes!
実はここで言う静的配列は、もう一度newすれば数が変えれるので動的配列と捉えることができます。特にc++などでは、newせずに配列を作るものが静的配列なので、newの有無で静的か動的かを分けます。

2. ArrayListはただの動的配列なのか?
今まで静的配列だと思っていたものが動的配列なら、ArrayListはなんなんでしょう?これはクラスの一種で、動的配列クラスと呼ぶことができます。動的配列に、それを便利に使うための機能がくっついたクラスです。もう少し詳しく言うと、コレクション・インターフェースの一種であるListインターフェースの配列クラスと言えます。詳しくはリンク先を見てみてください。おすすめなのはまずArrayListを使ってみて便利さがわかったら、他にも似たような少し違うクラスがあるんだなという風に理解を広げていくことです。

3. newとはなにか
配列を作るとき、クラスを生成するとき、newは動的に必要なメモリを確保しています。配列の時は、指定したデータ型の使用するサイズ(bit, byte)×要素数のメモリを確保しています。クラスの場合も、クラスで用いているデータ等のメモリを確保しています。