[Go to “teaching” in Morimoto Lab]
ArrayList(便利な配列)について学習する。
Static array(静的配列): これまでに習った、定数によってデータ数を指定する配列。データ数は途中で変動しない [GameByBall2でやりました]
Dynamic array(動的配列): しかし、以下のような場合どう実装しましょう?
クリックしたときにその場所にボールが現れる。 [公式の動的配列の例]
たくさんの敵がいて、あなたが敵を撃ったら、敵が消える。
こういう場合、データ数を柔軟に変動できる配列があると便利です。
ProcessingではArrayListを使うことでそれを実現できます。
以下のコードにて、静的配列と動的配列の違いを見てみましょう。
ballsと言う名前の動的配列を作っています(以下はイメージ図)
balls
├─ [0] → PVector(100,100)
├─ [1] → PVector(200,100)
├─ [2] → PVector(300,100)
//↓ 定義と生成(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ベースの言語なのでこちらが本元)
remove(1)のイメージ
[0][1][2][3]
↓
[0][2][3]
(まとめ)動的配列と静的配列の比較
配列は「数が固定」のときシンプルで扱いやすいです。
ArrayListは「増減するデータ」を扱うとき便利です。
上記の★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);
とするとディープコピーになります(そりゃそうだ)。 シャローコピーのいいところもあります。
実態が一つしかないのでメモリを食わないですし、プログラムの可読性をあげられます。
ただしちゃんと理解していないと、変えたつもりがないのに値が変わっているなどのバグになりえます。
【シャローコピーのイメージ】
PVector a ----┐
├→ (100,100)
PVector b ----┘
実態は一つしかないが、同じ実態を複数の変数から「参照」している(同じデータを指し示す)。
【ディープコピーのイメージ】
PVector a → (100,100)
PVector b → (100,100)
同じ値でも、別々に実態がある。
for文で0から順に全てのボールをremoveしようとしたら変なことに?
for( int i=0 ; i < balls.size() ; i++ ){
balls.remove(i);
}//全て削除されず、一つ飛ばしに削除されます
上記のように、for文で配列の0番目から順に全てremoveしようとすると、全て削除されず、一つ飛ばしに削除されます
(以下のだるま落としのイメージで言うと、配列のデータは下から順に0,1,2…となっています)。
なぜこうなるか、詳細を見てみましょう。
0番目のボールをremoveすると、それまで1番目だったボールが0番目に詰められます。
for文でi++しているので、次の処理はi=1の時になるので、元々1番目だったボールが削除されません😱(以下のリンクを参照)。
【図解:0青、1ピンク、2緑、3黄色、4赤、だとすると?】
(image url)
これを回避するにはいくつか方法がありますが、一つは
for( int i=balls.size()-1 ; i >= 0 ; i-- ){
balls.remove(i);
}//後ろからなら問題なく全て削除できる
などとして、後ろから順に削除することです。
すると「ボールを前に詰める」ことがないので、ボールの位置が変更することがなく、思った通りの挙動になります。
この時はだるま落としで言うと、上(頭)から順に飛ばしていくイメージですね(最初に頭がなくなるだるま落としって・・・)。
このように、for文で走査している途中にArrayListの要素数を変更すると、思わぬバグの原因になりえることを覚えておきましょう。
ちなみに、全て消したい場合はArrayListのclearメソッドが便利です。
マウスクリックしたところにボールがあるかどうか、Game by ball (1)で行った衝突判定で計算しましょう。
マウスをクリックしたら、マウスとボールの距離を計算する。まずremoveなどせずに、クリックしたら全てのボールとの距離を計算し、その数値を見て確認してみるとよいでしょう。
距離がボールの半径より小さいとき、衝突したと判定してそのボールをremoveしましょう
最初に見つかった1個だけを消したい場合は break を使う方法もあります。
これまでPVectorの配列を使っていましたが、自作のクラスの動的配列に変更してプログラムを作りましょう。
Note: マウス関係のメソッドをクラス内から呼ぶことについて
mouseClicked()やdraw()などのProcessingが元から用意している特殊な関数はクラス内で使う(定義・記述する)と、同じ名前でもProcessingが自動で呼び出す特殊な関数ではなくなり、普通のメソッドになります。
mousePressed()はクラスの中で呼ぶことはできますが、入力処理を複数個所に分散させると、どこで入力が処理されているかわかりづらくなるためおすすめしません。draw()からの呼び出しに限定してあげた方がわかりやすいです。
Note: ボールを減らしたときに追加したくない人へヒント(任意でOK)。
この場合、まずマウスと一つでもボールが衝突しているかどうかを判定し、booleanの変数などに判定結果を代入しておきます。 ボールが衝突しているときは、addしない、とするとよいでしょう。Note: Ballクラスのupdate()とdisplay()を分ける理由
プログラムが大きくなってきたときに、描画とそれ以外の処理がクラスごとに分けられていると、理解しやすいコードになります。また、「動きだけ停めたい」「描画だけ確認したい」など、問題の切り分けもしやすくなるため、デバッグにも便利です。
自作のクラスとArrayListによる要素数の増減を用い、
丸いキャラクター(または丸で表現できるオブジェクト)を使った、
ゲーム性・インタラクション・アニメーションのあるアーティスティックなアプリケーションを作成してください。
また、生き物や現象を参考に、複雑性を取り入れてください。
「増える」「減る」「分裂する」「集まる」「消える」など、
ArrayListならではの“数が変化する表現”を活かしてみましょう。
例:
粒子、泡、細胞、群れ、スライム、増殖、生物、発光、感染、融合 など
これまでに扱った
ボールの重力による動き、
Boidsのようなルールに従った動き、
樹木のような枝分かれ、
などは複雑性の一例になります。
キャラクター自体にも、生物らしさがあるとよいです。
参考:大阪万博のロゴマーク、「いのちの輝きくん」のアレンジ
今回の課題の参考に下記のURLでぜひ遊んでみてください!
いのちの輝きくんと一緒に遊べるようになった。
— シャポコ🌵 (@shapoco) August 26, 2020
👉️ https://t.co/889MuzSwI4
. pic.twitter.com/2mlINAHMYl参考:粒子による生き物のプログラム
こちらは2026年度受講生が教えてくれた、粒子による生き物の表現!YouTube動画解説もあります(英語)
余裕のある人は、
・particleクラスを自力で実装してみる(前回紹介したparticleのofficial siteはぜひ参考にしましょう)
・Boidsをクラスで実装する
・Boidsをクラスで実装した上で、炎や光のようなエフェクトにアレンジする
などに挑戦してみてください!
※前回のテキストの最後にparticleクラスの関連リンクがあります!(今度こそArrayListまで学んだので読めます!)
※かっこいいエフェクト
を見つけて、どうやったらプログラムで再現できるか、ぜひ考えてみてください。
Note: 炎や煙のシミュレーションもパーティクルでできる
パーティクルに、透明度を持った小さな炎や煙の画像(テクスチャ)を描画することで、炎・煙・爆発などを表現できます。
多数のパーティクルを少しずつ異なる動きで動かすことで、複雑で自然な見た目になります。
動き方の制御には様々な方法があります。例えば:
ランダムな動き、ノイズ、セルオートマトン、 流体シミュレーション
などです。
特に流体シミュレーションは、煙や炎の流れをよりリアルに表現できる方法です。
少し難易度は高いですが、夏学期の講義で扱います!
Smoke Particle System (Processing Official)
テクスチャとパーティクルシステムを使った煙の表現になります。
テクスチャや動きを工夫して、もっとリアルな表現や、別の表現に応用できるか試してみましょう。
Processing Tutorial: How to Create Smoke Particles
こちらは同様のSmoke Particlesの解説付きサイトになります。
Stable Fluids
夏学期で扱う流体シミュレーション手法の元になった重要な論文です。
CGにおけるリアルタイム流体シミュレーションの代表的研究として知られています。
A. 静的配列でもnewすれば数が変えられるのでは?(new できるものは動的配列なのでは?)→Yes!
実はここで言う静的配列は、もう一度newすれば数が変えれるので動的配列と捉えることができます。特にc++などでは、newせずに配列を作るものが静的配列なので、newの有無で静的か動的かを分けます。
B. ArrayListはただの動的配列なのか?
今まで静的配列だと思っていたものが動的配列なら、ArrayListはなんなんでしょう?実はArrayListはクラスの一種で、動的配列クラスと呼ぶことができます。動的配列に、それを便利に使うための機能がくっついたクラスです。もう少し詳しく言うと、コレクション・インターフェースの一種であるListインターフェースの配列クラスと言えます。詳しくはリンク先を見てみてください。おすすめなのはまずArrayListを使ってみて便利さがわかったら、他にも似たような少し違うクラスがあるんだなという風に理解を広げていくことです。
C. newとはなにか
配列を作るとき、クラスを生成するとき、newは動的に必要なメモリを確保しています。配列の時は、指定したデータ型の使用するサイズ(bit, byte)×要素数のメモリを確保しています。クラスの場合も、クラスで用いているデータ等のメモリを確保しています。
※Processing(Java)の場合、厳密にこのメモリ数を確保しているわけではないのですがここでは割愛します。