無限増殖(可変長配列)

2019-10-16

本章のあらすじ

  • 配列を復習する
  • 要素数を自由に変更できる配列「可変長配列」
  • 無限に湧き出すものを作る

配列の復習

まずは配列の復習として、100体のエージェントがランダムウォークするスケッチを作ってみましょう。

(→array_agents

配列の限界

いま作ってもらったとおり、配列は宣言時に個数を指定する必要があります。つまり、あらかじめ個数は決まっています。配列の個数=長さが固定なので「固定長配列」と呼ぶこともあります。

固定長の配列では、例えば「水源から無限に湧き出る水」のような、どんどんと数が増えていくようなものが表現できません。

要素の数を自由に増やしていければいいのに…そんな願いを叶えてくれるのが「可変長配列」です。

可変長配列

可変長配列とは、その名のとおり要素数が可変の配列です。要素を自由に足したり除いたりできます。

Processingにおける可変長配列

Processingでは、変数の種類に応じていくつか型が用意されています。

変数の種類 単体の型 対応する可変長配列の型
小数 float FloatList
整数 int IntList

これ以外にも、より複雑な型を扱う ArrayList なるものも存在しますが、これについては後ほど必要になったときに触れることにしましょう。

ここからは、もっともよくお世話になるであろう FloatList を例に説明していきます。

宣言と初期化

可変長配列も変数の一種です。宣言してはじめて使えるようになります。

// float型の可変長配列を宣言
FloatList values;

固定長配列と同様、可変長配列も宣言したままでは空っぽです。

可変長配列の実体は new キーワードで作成します。

// 可変長配列を作成
values = new FloatList();

これで可変長配列を使う準備が整いましたが、初期状態では要素がありません。(整地して準備はできたけどまだ家が建っていない…ようなものでしょうか)

ちなみに、固定長配列は初期化の際にあらかじめ要素数を決めておく必要がありましたが、可変長配列はあとで自由に変更できるので不要です。

要素の追加

可変長配列には、いつでも要素を追加できます。 配列の名前.append( 追加する要素 ) と書くことで、配列の末尾に要素を追加できます。

// 末尾に 1.4141 を追加
values.append(1.4141);
// 末尾に 3.1416 を追加
values.append(3.1416);

要素の個数

可変長配列の要素の個数は 配列の名前.size() で取得できます。

// 可変長配列の要素数を出力
println( values.size() );

要素の取得

可変長配列の要素を取得するには、先頭から何番目か=インデックスを使って 配列の名前.get( インデックス) と書きます。

// 0番目の要素を取得するので「1.4141」が出力される
println( values.get(0) );

// 1番目の要素を取得するので「3.1416」が出力される
println( values.get(1) );

要素の更新

可変長配列の要素を更新するには、 配列の名前.set( インデックス , 更新後の数値 ) と書きます。

// 0番目の要素を「7.7777」に更新する
values.set(0, 7.77);

もし要素の数値に四則演算をしたいときは、 .get().set() を組み合わせてもよいですが…。

// 0番目の要素に 123 を足す:
// 0番目の要素を get(…) で取得してから 123 を足して、 set(…) で更新する
values.set(0, values.get(0) + 123);

このようにちょっと込み入ってしまうので、簡単に書ける方法が用意されています。

// 0番目の要素に 123 を足す
values.set(0, values.get(0) + 123);

// 0番目の要素に 123 を足す(上の文と同じ)
values.add(0, 123);

四則演算をまとめておくとこんな感じ。

演算 書きかた コード例
足し算 配列.add( インデックス , 数値 ) values.add(0, 3.14)
引き算 配列.sub( インデックス , 数値 ) values.sub(0, 3.14)
掛け算 配列.mult( インデックス , 数値 ) values.mult(0, 3.14)
割り算 配列.div( インデックス , 数値 ) values.div(0, 3.14)

まとめ:固定長配列との対応表

固定長配列との操作もまとめておきます。

操作 固定長 可変長
配列の宣言 float[] ary; FloatList ary;
配列の初期化 ary = new float[100]; ary = new FloatList();
i 番目の要素の取得 ary[i] ary.get(i)
i 番目の要素の更新 ary[i] = 123; ary.set(i, 123);
i 番目の要素に 123 を足す ary[i] += 123; ary.add(i, 123);
または
ary.set(i, ary.get(i) + 123);
i 番目の要素に 0.5 をかける ary[i] *= 0.5; ary.mult(i, 0.5);
または
ary.set(i, ary.get(i) * 0.5);

練習:可変長配列に書き換えてみる

一番はじめに固定長配列で作ったエージェント100体のランダムウォークを、可変長配列を使って書き直してみましょう。

まずスケッチの先頭、変数を宣言する際の型が float[] から FloatList に変わります。

// エージェントの位置を覚えておくための配列
FloatList x;
FloatList y;

次に setup() ブロックを見ていきます。初期化の際は、要素数を指定する必要がなくなります。

// 可変長配列を初期化
x = new FloatList();
y = new FloatList();

初期設定では、エージェントの数ぶん .append() で要素を追加していきます。

// エージェントの初期位置を設定
for (int i = 0; i < 100; i++) {
  x.append(width / 2);
  y.append(height / 2);
}

for文で100回要素を追加しているので、それぞれの可変長配列には100体ぶんのX/Y座標が入っている状態になりますね。

最後に draw() ブロックを見ていきます。エージェントの位置をランダムにずらす処理は、 .add() を使って既存の要素に数値を足し算していきます。

for (int i = 0; i < 100; i++) {
  // i番目のエージェントの位置をずらす
  x.add(i, random(-5, 5));
  y.add(i, random(-5, 5));

  
}

エージェントを描くときは、可変長配列に入っている座標を .get() で取得して利用すればOKです。

for (int i = 0; i < 100; i++) {
  

  // i番目のエージェントを描く
  ellipse(x.get(i), y.get(i), 10, 10);
}

以上で書き換えが完了しました。書き換え前と同じように動作しましたか? (→vla_agents

練習:クリックでエージェントを増やしてみる

さて、ここからが可変長配列の出番です。例として、クリックしてエージェントを増やしてみましょう。

クリックしたときに実行されるブロックは mousePressed() でしたね。この中で .append() を使ってエージェントのX/Y座標をひとつずつ追加しましょう。

void mousePressed() {
  // 新たなエージェントを追加
  x.append(mouseX);
  y.append(mouseY);
}

これでエージェント1体ぶんの情報が追加されました。次に、 draw() ブロックを見ていきます。この中で、100体ぶんのエージェントをまとめて移動・描画するのにfor文を使っていましたが、0 ~ 99まで固定で繰り返しているので、せっかくクリックで配列の要素を増やしても100体ぶんしか処理されません。

このfor文の終了条件を書き換えて、配列の個数が増えてもすべての要素に対して処理されるようにしましょう。配列の要素数は .size() で取得できるので…。

for (int i = 0; i < x.size(); i++) {
  
}

このように書けますね。これでクリックするたびエージェントが増えていくようになりました! (→vla_agents_append

練習:エージェントに重力を持たせてみる

このまま、エージェントに速度・加速度の概念を導入して重力をかけてみましょう。重力を忘れてしまった人は、「物理演算」の回を振り返りましょう。

ヒント

  • 重力は加速度
  • 加速度は速度に、速度は位置に作用する
  • 速度を変数として追加すればよい

(→vla_agents_gravity

発展

  • 壁や床を設置して、エージェントがキャンバス外に出ないようにできるだろうか?

練習:自然発生させてみる

ここまではクリックでエージェントを増やしていましたが、自然発生させてみることも可能です。 mousePressed() ブロックに書いた処理を draw() ブロックに移して実行してみましょう。 (→vla_agents_emitter

パーティクル

練習:粒子に寿命を持たせてみる

粒子に寿命を持たせて、消えるようにしてみましょう。寿命用の可変長配列をもうひとつ追加します。

// 寿命の配列
FloatList life;

setup() ブロック内で、他の可変長配列と同様に初期化しておきましょう。寿命は 1 から 0 まで少しずつ減らしていくことにします。

void setup() {
  

  // 可変長配列を初期化
  life = new FloatList();

  

  // エージェントの初期位置を設定
  for (int i = 0; i < 100; i++) {
    

    // 寿命を設定
    life.append(1);
  }
}

最後に draw() ブロック内で、寿命を減らす処理と、寿命が残っているときだけ粒を描くようにします。

void draw() {
  

  for (int i = 0; i < x.size(); i++) {
    

    // i番目のエージェントの寿命を減らす
    life.sub(i, 0.05);

    // 寿命が残っているときだけ、i番目のエージェントを描く
    if (life.get(i) > 0) {
      ellipse(x.get(i), y.get(i), 10, 10);
    }
  }
}

これで、エージェントに寿命の概念が入り、途中で消えるようになりました。 (→hello

このように、粒子がたくさん集まって構成されるエフェクトを「パーティクル」と呼びます。パーティクルは、粒子の色や形状・動きを工夫することで、爆発や炎・水など、さまざまな効果を再現することができます。

ちなみに、パーティクルの発生源を「エミッター (emitter)」と呼びます。

練習:寿命を不透明度に反映してみる

先の例では寿命が尽きたとたん消えてしまうので、不自然に見えます。寿命に応じて不透明度を変化させることで、より滑らかにしてみましょう。

ellipse() で粒を描く直前に、寿命 life から不透明度を計算して、塗り色を調整します。

for (int i = 0; i < x.size(); i++) {
  

  // 寿命を塗りの不透明度に反映する
  // 寿命 life.get(i) が 1 ~ 0 で変化するとき、
  // 透明度 al を 255 ~ 0 まで変化させる
  float al = map(
    life.get(i),
    1, 0,
    255, 0
  );
  fill(0, 0, 0, al);

  
}

これで、滑らかに消えていくようになりました。 (→fade

寿命の尽きた粒子が増えていく…

先のスケッチで、要素数をコンソールに出力して見てみましょう。 draw() ブロックに以下のコードを差し込んで実行します。

// 可変長配列 x の要素数をコンソールに出力
println(x.size());

どうでしょうか?時間が経つほど増えていくのが確認できると思います。

寿命が尽きて不要になった要素を削除してあげないと、for文の繰り返し回数も負荷も増え続け、動作は次第に重くなり…最終的にはプログラムが停止してしまいます。展示など長期間実行し続ける状況では死活問題です。

発展:要素の削除

可変長配列の要素を削除するには、 配列の名前.remove( インデックス ) を使います。後続の要素がある場合は、空席が出ないように詰められる=インデックスが調整されます。

// 1番目の要素を削除
values.remove(1);

練習:画面外に消えた粒子を削除する

寿命が尽きた要素を削除するには、先ほど触れた .remove() を使って、以下のように書けば一見うまくいきそうです。

// これで不要な要素を削除できる…?
for (int i = 0; i < x.size(); i++) {
  if (life.get(i) <= 0) {
    x.remove(i);
    y.remove(i);
    vx.remove(i);
    vy.remove(i);
    life.remove(i);
  }
}

…本当でしょうか?このコードはうまくいくこともありますが、少しだけ欠陥があります。特定の条件下—例えば寿命切れの粒子が2つ並んでいる場合など—で見落としが発生してしまうのです。

これを防ぐには、for文のインデックスの巡っていく順序を逆にします。つまり、先頭から末尾ではなく、末尾から先頭にかけて不要な要素を見ていくのです。

// 末尾から要素をチェックしていくことで、漏れなく不要な要素を削除できる
for (int i = x.size() - 1; i >= 0; i--) {
  if (life.get(i) <= 0) {
    x.remove(i);
    y.remove(i);
    vx.remove(i);
    vy.remove(i);
    life.remove(i);
  }
}

(→remove

参考:さまざまなパーティクル

粒子に画像を利用する

粒子に画像を利用すると、複雑な表情を手軽に出すことができます。例えば輪郭のぼけた粒子で描くと、全体の塊感が出てまったく違った趣を受けます。

画像を描く際は、 tint() で色を被せることができます。透明な色を指定すれば、画像を半透明に描くこともできるので、これを利用しましょう。 (→img

規則的に飛ばす

規則的に飛ばすようにすると、複雑なパターンを描くことができます。発生源を動かすとまた違った表情になりますね。 (→bullet

演習:パーティクルでさまざまな効果を再現してみよう

パーティクルの外観や動きを工夫して、さまざまな効果を再現してみましょう。

爆発(→explosion)、 火花、 炎(→fire)、 水、蛍、…

本章のまとめ

  • 可変長配列は、要素を自由に追加したり削除したりできる配列
  • 要素を追加し続けることで、無限に増殖するものを表現できる
  • たくさん粒子を放出すると、いろいろなものを表現ができる(パーティクル)