第11回(まとめ)

一次元配列

これまで,

  • 数値と変数
  • 条件文
  • 繰り返し
  • 配列

を学んできました.ここまでの授業や課題で,質問が多かったポイント・勘違いしやすいポイントを,もう一度立ち返って整理します.

説明部

setup()draw() の役割

関数 いつ呼ばれる? 何を書く?
setup() プログラム開始時に 1回だけ 初期設定 (ウィンドウサイズ,初期値)
draw() 毎フレーム (通常 1秒に60回) 毎フレーム描画する内容,状態の更新

変数のスコープ:グローバル変数とローカル変数

スコープ

変数には 使える範囲 (=スコープ)があります.

中括弧 { }中で宣言された変数 は, その中括弧の 中だけ で使えます.

void setup(){
  int a = 5;       // ここで宣言
  println(a);      // OK
}

void draw(){
  println(a);      // エラー!aはここでは存在しない
}

これは asetup() の中で宣言された ローカル変数 だからです.

setup() が終わった瞬間に a は消えてしまいます.

グローバル変数

逆に,どの関数からでも使いたい変数は 関数の外側 に宣言します.

int a = 5;  // グローバル変数

void setup(){
  println(a);  // OK
}

void draw(){
  println(a);  // これもOK
}

このように関数の外で宣言された変数を グローバル変数 と言います.

setup() でも draw() でも使えます.

配列でも一緒

普通の変数ならこう書く人が多いです.

int x;  // 関数の外でちゃんと宣言

void setup(){
  x=100;
  println(x);
}

ところが配列になると,こう書いてしまう人がいます.

int[] arr;   // 宣言だけ.これはOK!
arr = new int[10];
arr[0] = 100;

void setup(){
    println(arr); // エラー
}

以下のようにしてください.

int[] arr;   // 宣言だけ.これはOK!

void setup(){
    arr = new int[10];
    arr[0] = 100;
    println(arr);
}
int[] arr = new int[10];   // 宣言+生成.これも宣言が入っているからOK!

void setup(){
    arr[0] = 100;
    println(arr);
}

ループ

for 文を書くとき,次のように書くこともできます.

int i;
for(i=0; i<10; i++){
    // 処理
}

この書き方では, ifor 文の外で宣言しています.そのため, for 文が終わったあとも, i は使える状態のまま残ります.

ただし,多くの場合, for 文の i は,「何回の繰り返しか」を数えるためだけに使います.つまり, i そのものに特別な意味があるというより,繰り返しの回数を管理・カウントするための変数です.

そのため,特別な意図がない限り,iは以下のようにローカル変数とするのが良いです.

for(int i=0; i<10; i++){
    // 処理
}

二重ループ

for文やwhile文などで繰り返し処理をする場合の考え方についてです.

特に二重ループについて,イメージが湧きにくいとは思いますが,変数が2つ出てきたから二重の繰り返し処理,というものではありません.

二重の繰り返しが本当に必要なのは, すべての組合わせ (直積) を扱いたい場合です.

右はすべての組合せについて考えているので二重ループが必要です.左は必要ありません.

変数を使う意義:型・名前・可読性

変数には必ず型と名前がある

int x = 5;
  • int : 型
  • x : 名前

型は「どんな種類の値を入れる入れ物か」を表します. int は整数, float は小数(浮動小数点), String は文字列, color は色,など.

いつ変数を使うべきか

考え方の目安:

  1. 同じ値が 2〜3回以上 出てくるなら変数にする
  2. 1回しか出てこなくても,値の 意味 がパッと見でわからないなら変数にする

たとえば次のコードは「何の30か」がパッと見でわかりません.

ellipse(x[i], y, 30, 30);
if(x[i] < 15 || x[i] > width-15){ ... }

これを次のように書き直すと他の人が読みやすくなります.

int diameter = 30;
int radius = diameter / 2;

ellipse(x[i], y, diameter, diameter);
if(x[i] < radius || x[i] > width-radius){ ... }

「30」や「15」のように,制作者以外に意味のわからない数値を マジックナンバー と言います.マジックナンバーを減らす ことは,読みやすいプログラムの基本です.

また,値を変更したくなったときも,変数を使っておけば 1箇所直すだけ で済みます.直接 30 と書いてあると,プログラム中に何度も出てくる 30 をすべて手で直さなくてはなりません.

名前のつけ方

良い名前は, 値の意味 がわかる名前です.

良くない例 良い例
int a; int score;
int n; int playerCount;
float x; float circleRadius;

特にプログラムが長くなってくると,昔の自分が書いた ab が何だったのか自分でもわからなくなります...名前をつけることは,未来の自分の手助けと思ってください.

===

=== は,記号は似ていますが まったく違うもの です.

記号 名前 意味
= 代入演算子 右の値を左に入れる
== 比較演算子 右と左が等しいかを調べる

本日の課題

取り組める順に挑戦してください。

数値と変数-基礎9

3桁の整数をランダムに5つ生成してください.これらを

  • 2の倍数かつ3の倍数でない
  • 3の倍数かつ2の倍数でない
  • 2の倍数かつ3の倍数(6の倍数)
  • 上記のどれでもない

に分類し,末尾に"はXの倍数です"とつけて出力してください.

ただし、2の倍数でも3の倍数でもないときは、数字だけを表示してください.

958は3の倍数です
749
480は2の倍数です
168は2の倍数です
666は6の倍数です
void setup() {

  for(int i=0; i<5; i++){
      int n = int(random(100, 1000)); // 100以上999以下の整数

  if (n % 2 == 0 && n % 3 == 0) {
    println(n + "は6の倍数です");
  } else if (n % 2 == 0) {
    println(n + "は2の倍数です");
  } else if (n % 3 == 0) {
    println(n + "は3の倍数です");
  } else {
    println(n);
  }
  }
}
解説

本課題の場合, setup() はなくても問題ありません.もし setup() を書く or 書かないの違いが知りたい人は, Processing/EnvironmentProgramming Styles の項目を参照してください.

倍数の判定には % (剰余演算子) を使用します.これは「割った 余り 」を返す演算子です.

たとえば, 10 % 3 は「10を3で割ったあまり」で 1 を返します. 12%3 のように出力が 0 であれば割り切れる = その数の倍数 ということになります.

倍数の関係を整理してみます.

6の倍数は必ず2や3の倍数ですが, 2や3の倍数は必ずしも6の倍数とは言えません.

このような「大きな条件が小さな条件の一部を含んでいる」場合, if ~ else if ~ else 文を使うときはより厳しい(狭い)条件から書く のがコツです.

本課題では6の倍数から始めると条件式の中身を簡潔に書くことができます.

あるいは,以下のように書くこともできます.

void setup() {

  for(int i=0; i<5; i++){
      int n = int(random(100, 1000)); // 100以上999以下の整数

  if (n % 2 == 0 && n % 3 != 0) {
    println(n + "は2の倍数です");
  } else if (n % 3 == 0 && n % 2 != 0) {
    println(n + "は2の倍数です");
  } else if (n % 3 == 0 && n % 2 == 0) {
    println(n + "は3の倍数です");
  } else {
    println(n);
  }
  }
}

繰り返し-基礎7

幅500,高さ100のウィンドウに,左から右へ 10個 の円を等間隔に並べてください.

色は, 左端は赤(255,0,0),右端は青(0,0,255) で,間の円はその間を 滑らかに変化 させてください.

  • ウィンドウサイズ: 500×100
  • 円の数: 10個
  • 直径: 20
  • y座標: 中央(50)
  • 色は左端 (255,0,0) から右端 (0,0,255) へ線形に変化

回答


int en_haba = 25;
void setup(){
  size(500,100);
  background(255,255,255);

  for(int i=0; i<10; i++){
    int r = 255 - i*255/9;
    int b = 0 + i*255/9;
    noStroke();
    fill(r,0,b);
    ellipse(50*i+en_haba, height/2, en_haba, en_haba);
  }
}
解説
int r = 255 - i255/9;
int b = 0 + i255/9;

i=0 で赤 (255, 0, 0)i=9 で青 (0, 0, 255) になるように,赤成分を減らしつつ青成分を増やしていきます.

円は10個ですが, 左端から右端までを何等分するかを考えると, 10個の円の間隔は9つしかありません.そのため分母は 9 となります.

繰り返し-応用5

幅400,高さ400のウィンドウに対し, 8×8 のマス目状に黒と白の四角形を交互に描いてください.

回答

void setup(){
  size(400,400);
  for(int i=0; i<8; i++){
    for(int j=0; j<8; j++){
      if((i+j) % 2==0){
        fill(0,0,0);
      } else{
        fill(255,255,255);
      }
      rect(50*j, 50*i, 50,50);
    }
  }
}
解説

二重ループが必要な典型例です. 64個のマス全てを1つずつ描くので, 単純にfor文を64回回すこともできますが,「縦8個 × 横8個」という 組み合わせ として捉えるほうが自然です.

色を決めているのは次の1行です.

if((i+j) % 2==0)

なぜこれで市松模様になるか を考えてみてください.

ij が1ずつ増えると, (i+j) の偶奇は毎回入れ替わります.つまり隣り合うマスは必ず違う色になり,これが市松模様の正体です.

i j i+j
0 0 0
0 1 1
0 2 2
1 0 1
1 1 2

通常の慣習では i を「行(y座標側)」, j を「列(x座標側)」として扱います.このため, rect(50*j, 50*i, 50, 50) としています.

8×8の市松模様は縦横対称なので今回はどちらでも同じ絵が出ますが,非対称な模様(たとえば横長のマス)を扱うようになると違いが表面化するので, 意識しておくと良いです.

以下で色の決め方の他の書き方についても触れておきます.

  • 別解1:素朴な場合分け

    「行が偶数か奇数か」「列が偶数か奇数か」の4通りで場合分けする方法もあります.

    for(int i=0; i<8; i++){
        for(int j=0; j<8; j++){
            if(i%2 == 0 && j%2 == 0){
                fill(0);        // 偶数行・偶数列 → 黒
            } else if(i%2 == 0 && j%2 == 1){
                fill(255);      // 偶数行・奇数列 → 白
            } else if(i%2 == 1 && j%2 == 0){
                fill(255);      // 奇数行・偶数列 → 白
            } else {
                fill(0);        // 奇数行・奇数列 → 黒
            }
            rect(50j, 50i, 50, 50);
        }
    }
    

    一番愚直で,場合分けをすべて書き出した形です.プログラミングを始めたばかりだとこれが一番自然な発想かもしれません.

    動きますが, 条件がゴチャゴチャして読みにくいです.

  • 別解2:行ごとに色を切り替える

    「偶数列は黒・白・黒・白…」「奇数列は白・黒・白・黒…」と分けて考える方法もあります.

    for(int i=0; i<8; i++){
        for(int j=0; j<8; j++){
            if(i % 2 == 0){
                // 偶数行: j が偶数なら黒
                if(j % 2 == 0) fill(0);
                else           fill(255);
            } else {
                // 奇数行: j が偶数なら白
                if(j % 2 == 0) fill(255);
                else           fill(0);
            }
            rect(50j, 50i, 50, 50);
        }
    }
    

    こちらは if文のネスト(入れ子) を使っています.考え方としては素直ですが, 別解1と同じく分岐が多くて読みにくいです.

  • 別解3:「両方偶数」または「両方奇数」で判定

    「隣り合うマスは色が違う」ことを別の角度から書くと,「iとjの偶奇が同じなら同じ色」ということでもあります.

    if(i % 2 == j % 2){
        fill(0);       // 偶奇が一致 → 黒
    } else {
        fill(255);     // 偶奇が違う → 白
    }
    

    これも回答と同じ結果になります. (i+j) % 2 == 0= と ~i % 2 == j % 2 は数学的に等価です.

繰り返し-応用6

1 から 100 までのランダムな整数を生成し続け,合計が500を超えた時点で停止するプログラムを書いてください.

  • 1回1回の加算を「 n 回目: + x → 合計 y 」と表示する
  • 停止後, 「 ~ 回足して,合計は ~ 」と表示する
1回目: +70 → 合計 70
2回目: +99 → 合計 169
3回目: +84 → 合計 253
4回目: +87 → 合計 340
5回目: +47 → 合計 387
6回目: +28 → 合計 415
7回目: +92 → 合計 507
---
7回足して,合計は 507

回答

int sum = 0;
int count = 0;
while(sum < 500){
    int x = int(random(1, 101));
    sum += x;
    count++;
    println(count + "回目: +" + x + " → 合計 " + sum);
}
println("---");
println(count + "回足して,合計は " + sum);
解説

この課題の一番のポイントは, ループの回数が事前にわからないことです.例えば運が良ければ7回で合計500を超えるかもしれませんし,運が悪ければ20回以上かかることもあります.

for 文は「何回繰り返すか」を最初に書くものでした.

for(int i=0; i<10; i++){ ... }   // 「10回繰り返す」と最初に決めている

しかし今回は繰り返し回数が乱数次第で決まるので,「何回?」の部分がそもそも書けません.こういうときに使うのが while 文です.

while(条件){
// 条件が成り立っている間,繰り返す
}

while 文は「条件が成り立っている 間だけ 繰り返す」というシンプルな作りです.条件が成り立たなくなった時点でループを抜けます.今回は「合計が500未満の間,加算を続ける」=「 sum < 500 の間ループする」なので,以下のように自然に書けます.

while(sum < 500){ ... }

while 文と for 文の使い分けについては以下を参考にしてください.

状況 おすすめ
繰り返し回数が最初からわかっている for
ある条件になるまで繰り返したい while

厳密に言えば for でも書けなくはないですが, for で書くと ループの終了判定を無理やり中に埋め込む ことになって不自然です.「条件で止まる」動きなら while が自然な選択です.

int sum = 0;
int count = 0;

sum : これまでに足した数値の合計 count : 何回加算したかを数えるカウンタ

どちらも ループが始まる前 に 0 で初期化しておく必要があります.特に sum は「合計が500を超えたか」の判定に使うので,最初に 0 にしておかないと正しく比較できません.

while(sum < 500){
    int x = int(random(1, 101));  // 新しい数を作る
    sum += x;                      // 合計に足す
    count++;                       // 回数を1増やす
    println(count + "回目: +" + x + " → 合計 " + sum);
}

このループの中で, 毎回 sum が増えている ことに注目してください.これが 終了条件に近づく処理 です.

count++count = count + 1 の省略形で,「今の count に1を足したものを count に入れ直す」という意味です.「1ずつ増やしていく」ときの定番の書き方です.

繰り返し-応用7

幅500×高さ500のウィンドウに, 30個の雨粒が上から下に降るアニメーションを作ってください.

  • ウィンドウサイズ: 500×500
  • 雨は短い縦線として描く(長さ10ピクセル)
  • 雨が画面下に到達したら, また上から降ってくる
  • 各雨のx座標と落ちる速さはランダム

int N = 30;
float[] x = new float[N];
float[] y = new float[N];
float[] speed = new float[N];
void setup(){
  size(500, 500);
  for(int i=0; i<N; i++){
      x[i] = random(width);
      y[i] = random(height);
      speed[i] = random(3, 8);
  }
}

void draw(){
    background(200, 220, 240);
    stroke(50, 80, 150);
    strokeWeight(2);
    for(int i=0; i<N; i++){

// 描画
        line(x[i], y[i], x[i], y[i]+10);    // 落下
        y[i] += speed[i];    // 画面下に出たら上に戻す
        if(y[i] > height){
            y[i] = 0;
            x[i] = random(width);  // x も新しい位置に
        }
    }
}
解説

30本の雨がそれぞれ 違うx座標 , 違う落下速度 を持って動きます.「雨1本ずつ」の情報を配列で持ちます.

int N = 30;
float[] x = new float[N];
float[] y = new float[N];
float[] speed = new float[N];

雨1本1本が独立に動くので, 次の3つの情報を配列で管理します.

  • x[i] : i 番目の雨のx座標
  • y[i] : i 番目の雨のy座標
  • speed[i] : i 番目の雨が1フレームあたりに落ちるピクセル数

配列を並列に使うときの約束として, x[0]y[0]speed[0] は同じ雨(0番目の雨) を指します.なお N を定数として宣言しているのは, 「雨の数を変えたい」ときに 1箇所書き換えるだけで済むようにするためです.配列を作るときと for 文の両方で使うので, 変数にする意義が大きいです.

void setup(){
    size(500, 500);
    for(int i=0; i<N; i++){
        x[i] = random(width);
        y[i] = random(height);
        speed[i] = random(3, 8);
    }
}

各雨に バラバラ の初期値を与えています. x座標だけでなく y座標も乱数 にしています.

もし y[i] = 0 から始めると, すべての雨が最初は同じ高さで一直線に並ぶことになり起動直後に不自然な絵になります.初期状態から全画面にすでに雨が散らばっている状態を作っておくことで,起動した瞬間から自然な雨の風景に見えます.

速さも random(3, 8) で1本ごとに変わります.これで, 雨によって 速い雨と遅い雨 があるように見え,より自然な見た目になります.

speed[i] = random(3, 8)setup() の中で設定しています.もしこれを draw() に書いてしまうと, 毎フレーム速さが変わる ので,雨の動きがガクガクして「雨」に見えなくなります.「変わってほしくないものは setup() で, 変化するものは draw() で」という基本的な考え方に従っています.

for(int i=0; i<N; i++){
    line(x[i], y[i], x[i], y[i]+10);   // 雨を描く
    y[i] += speed[i];                    // y座標を増やす → 下に動く
    ...
}

line(x[i], y[i], x[i], y[i]+10) は縦方向に長さ10の線分を描きます.上端が (x[i], y[i]) , 下端が (x[i], y[i]+10) です.その後 y[i] += speed[i] で, y座標を増やす ことで下方向に移動します. Processingの座標系では y軸が下向き なので, y座標を増やす = 下に動くです.

if(y[i] > height){
y[i] = 0;
x[i] = random(width);
}

雨が画面下端を超えたらy座標を0に戻し, x座標もランダムに新しく決め直します.

配列-発展8

幅600×高さ600のウィンドウに,

15個の円が連なって マウスを追いかけるアニメーションを作ってください.

イメージとしては,しっぽのある彗星や蛇のような動きです.

  • ウィンドウサイズ: 600×600
  • 円の数: 15個
  • 先頭(0番目) の円はマウスを滑らかに追う
  • 2番目以降 の円は, 自分の 1個前の円 を滑らかに追う
  • 後ろの円ほど少し 小さく 描く

回答

int N = 15;
float[] x = new float[N];
float[] y = new float[N];

void setup(){
    size(600, 600);
    noStroke();
}

void draw(){
    background(255);  // 先頭(0番目)はマウスを追う
    x[0] += (mouseX - x[0]) * 0.2;
    y[0] += (mouseY - y[0]) * 0.2;  // 1番目以降は,1個前を追う

    for(int i=1; i<N; i++){
        x[i] += (x[i-1] - x[i]) * 0.3;
        y[i] += (y[i-1] - y[i]) * 0.3;
    }  // 描画(後ろから前へ → 先頭が一番上に来る)

    for(int i=N-1; i>=0; i--){
        float d = 30 - i * 1.5;        // 後ろほど小さく
        fill(50 + i*10, 100, 200);     //       後ろほど少し色を変化
        ellipse(x[i], y[i], d, d);
    }
}
解説
int N = 15;
float[] x = new float[N];
float[] y = new float[N];

15個の円それぞれについて, 位置 (x, y) を配列で持ちます.

x[0] += (mouseX - x[0]) * 0.2;
y[0] += (mouseY - y[0]) * 0.2;

先頭の円(0番目)だけはマウスを直接追いかけます.式の考え方は次のとおりです.

新しい位置 = 今の位置 + (目標 - 今の位置) × 0.2

「目標との距離の20%だけ進む」という意味で,距離が大きい(遠い)ほど大きく動く距離が小さい(近い)ほどゆっくりになるというふうにだんだん近づく動きになります.

この「だんだん近づく」動きを イージング(easing) と言います.もし単に x[0] = mouseX; y[0] = mouseY; と書くと,円はマウスに 瞬間移動 してしまいます.イージングを使うことで,「マウスをふわっと追いかける」動きが表現できます.

for(int i=1; i<N; i++){
x[i] += (x[i-1] - x[i]) * 0.3;
y[i] += (y[i-1] - y[i]) * 0.3;
}

ここが今回のキモです. i 番目の円は, 自分の1個前(i-1 番目)の円 を目標に追いかけます.

1番目の円 → 0番目(先頭)を追う 2番目の円 → 1番目を追う 3番目の円 → 2番目を追う …

こうすることで, 先頭がマウスを追い, 2番目が先頭を追い, 3番目が2番目を追い… とバケツリレーのように動きがつながっていきます.これがしっぽや蛇のように連なる動きの正体です.

このforループは i=1 から始まっていることに注意してください.もし i=0 から始めると, x[i-1] = x[-1] を参照することになり,配列の範囲外 でエラーになります. 0番目の円は「マウスを追う」という別ルールで先に処理してあるので,連鎖のループは1番目からで問題ありません.このように「先頭だけ別ルール」というのは,配列を数珠つなぎに扱うときによく出てくる書き方です.

for(int i=N-1; i>=0; i--){
    float d = 30 - i * 1.5;
    fill(50 + i*10, 100, 200);
    ellipse(x[i], y[i], d, d);
}

描画のループが 逆向き になっています. iN-1 から始まって, i>=0 の間, i-- で1ずつ減っていきます.つまり i = 14, 13, 12, ..., 1, 0 の順に処理されます.これは Processingでは 後から描いたものが上に重なる からです.先頭の円(0番目)を一番手前に見せたいので,最後に描きます.

float d = 30 - i * 1.5;    // 後ろほど小さく (30, 28.5, 27, ..., 8.25)
fill(50 + i*10, 100, 200); // 後ろほど赤成分が強く

インデックス i を使って, 後ろの円ほど小さく・色も変化する演出を入れています.

  • よくある別解: 前フレームの位置を引き継ぐ

    もう一つ, 自然に思いつきやすい書き方があります. 「 i 番目の円は, 前フレームの i-1 番目の円の位置にワープする」という発想です.

    int enN = 15;
    int[] enX = new int[enN];
    int[] enY = new int[enN];
    int[] tempX = new int[enN];
    int[] tempY = new int[enN];
    void setup(){
        size(600,600);
    }
    void draw(){
        background(255);
        // 現在の位置を一時保存
        for(int i=0; i<enN; i++){
            tempX[i] = enX[i];
            tempY[i] = enY[i];
    }
    
    // 位置を1個ずつずらす
    for(int i=0; i<enN; i++){
        if(i==0){
            enX[i] = mouseX;
            enY[i] = mouseY;
        } else{
            enX[i] = tempX[i-1];
            enY[i] = tempY[i-1];
    }
    }
    // 描画
    for(int i=enN-1; i>=0; i--){
        noStroke();
        fill(100+10*i,100,255);
        ellipse(enX[i], enY[i], 30-i, 30-i);
            }
    }
    

    これは1フレーム前の情報を tempX , tempY に保存しておき, 次のフレームでは i 番目に i-1 番目の1フレーム前の位置を代入するという考え方です. なぜ tempX, tempY が必要かというと, 一時保存なしに enX[i] = enX[i-1] としてしまうと

    i=1: enX[1] = enX[0]  → 全部 enX[0] の値になる
    i=2: enX[2] = enX[1]  → これも enX[0] の値
    i=3: enX[3] = enX[2]  → これも enX[0] の値
    ...
    

    というふうに 全部が先頭と同じ位置に潰れてしまうからです. これは第10回・課題6(配列の逆順並び替え)で tmp 変数が必要だった理由と同じです. 考え方はまっとうで, プログラムとしても正しく動きます. ただし, 動きの見た目は模範解答とかなり違います.

    どちらが良いというより, どういう表現をしたいかによってプログラムの書き方を選んでください.

    「動くコードが書けた」あとで, 自分が意図した動きになっているかを確かめ, 違うなら別のアプローチを考える, というのはプログラミングの重要なプロセスです. 自分の書いたコードの動きを言葉で説明できるようになると, 次のステップに進みやすくなります.