アリソンパリッシュによる
このチュートリアルでは、スケッチ内のユーザーアクションに関する情報を保持する方法を見ていきます。これを実現するには、配列とオブジェクトという新しいプログラミング概念について知る必要があります。
このチュートリアルのゴールは次の振る舞いを持つスケッチの作成です。”ユーザーがスケッチ上でクリックすると矩形が作成され、矩形はその後下方向に移動し、画面下端を過ぎたら見えなくなる。”
目次
矩形だらけ
ずっと存在しつづける矩形の作成方法はすでに述べました。Processingに対して、毎フレーム、背景を上書きしないよう伝えるだけです。
また1つの矩形を移動する方法も述べました。少し頭をひねると、ユーザーがクリックした位置から移動を開始する矩形も作成できます。
これはこれでよくできていますが、ユーザー入力に応答して、複数の矩形が画面上を移動する、というわたしたちのゴールはまだまだ先です。もちろん、前もって複数の矩形をセットしておけば、ゴールを実現する方法は容易に概念化できます。
これは、”同じことの繰り返しを避ける”というプログラミングルールに反するものの、動作はします。
座標の配列
上記サンプルの繰り返しを”くくり出す”方法と、スケッチに簡単に矩形を追加できるようにする方法について、少し考えてみましょう(これは、ユーザーが制御する矩形というゴールに近づく小さな一歩でもあります)。
前のサンプルでは、繰り返しのくくり出しにforループを使いました。一見、今の課題でもforループが次のように使えそうに思えます。
// これは実際のJavaScriptコードではない
for (var i = 0; i < 3; i++) { // iは0から2まで変化する
rect(100, rectY_ < i > , 50, 25); // rectY_ < i >で、rectY_0、rectY_1、
rectY_ < i > += 1 // rectY_2を参照したい
}
JavaScriptには、まさにこのような事柄を容易にするために設計された、配列と呼ばれるデータ構造の一種があります。
配列は、値のリストを保持する値の一種です。配列内の値には、リスト内のその位置でアクセスできます。また配列への項目(要素)の追加や、配列からの項目の削除、配列の項目の変更なども行えます。1つの値が数値などを保持する箱のようなものだとすると、配列は複数の箱を置く棚のようなものです。
次の例は、上記サンプルを配列を使って書き直したものです。
この例にはたくさんの新しいことが含まれているので、1つずつ順番に見ていきましょう。まず、配列の宣言があります。
const rectY = [0, 15, 30];
JavaScriptで配列の値を作成するときには、配列に入れたい値をプログラム内に直接入力できます(これは配列リテラルと呼ばれます。配列をプログラムに取り入れるほかの方法については後述します)。上記ステートメントは、配列を作成し、rectYという名前の変数にそれを保持しています。
配列はすべて、配列の中に項目がいくつ含まれているかを教えてくれる.length属性を持っています。forループの1行めでは、これを使ってループの上限を設定しています。
for (let i = 0; i < rectY.length; i++) {
ループ内では、次の式が配列のそのインデックスに保持されている値に評価されます(インデックスとは、配列の”スロット”を表す番号で、値はその”スロット”にアクセスして取得します)。
rectY[i]
したがって次のステートメントは、リストの個々のインデックス(0,1,2)の位置に保持されている値(0, 15, 30)を使って、矩形を描画します。
rect((i + 1) * 100, rectY[i], 50, 25);
配列が保持している値も変更できます。それは次の行で起こっていることです。
rectY[i] += 1;
配列をもっと詳しく
上記で述べた使い方は、Processingでの配列のほとんどの使用事例を示していますが、シンタックスについてもっと詳しく見ておくのは十分に価値のあることです。では、空のp5.jsスケッチにステートメントと式を記述していきましょう。
まず、配列を作成するために、角かっこ([])を書き、中に値をカンマで区切って記述します。
const stuff = [5, 10, 15, 20];
配列にはいくつでも値を入れることができます。また空の状態でスタートすることもできます
const nothingHereMoveAlong = [];
配列は多くの要素(項目)を持つことができます。配列がいくつの要素を持っているかを調べるには、.length属性を使います。
console.log(stuff.length); // 表示 4
console.log(nothingHereMoveAlong.length); // 表示 0
配列内の特定の項目を取得するには、配列値を保持する変数の直後に角かっこ([])を置き、かっこの中に数値(インデックス番号、インデックス位置)を記述します。
console.log(stuff[2]); // 表示 15 ...なせ10でないのか? 詳細は以下
配列の範囲を超える数値(つまり配列の長さよりも大きな数値)を与えると、式はJavaScriptの特別な値であるundefinedに評価されます。
console.log(stuff[152]); // displays "undefined"
console.log()関数はパラメータとして配列を取ることもできます。その場合console.log()は、配列の中身を表示します。これは、デバッグ目的に役立ちます。
console.log(stuff); // diplays [5,10,15,20]
配列の特定のインデックスにある値は、配列の変数名につづけて角かっこ、かっこ内にインデックスの数値を入れ、代入演算子を書いて、代入演算子の右に新しい値を置くことで、上書きできます。
stuff[2] = 999;
console.log(stuff); // 表示 [5,10,999,20]
const stuff = [5, 10, 15, 20];
const nothingHereMoveAlong = [];
// 配列の長さ(要素数)
console.log(stuff.length); // 4
console.log(nothingHereMoveAlong.length); // 0
// インデックス番号で値にアクセス
console.log(stuff[2]); // 15
// 範囲外にアクセスすると、結果はundefined(未定義)
console.log(stuff[152]); // undefined
// 配列の中身
console.log(stuff); // [5,10,15,20]
// 値の上書き
stuff[2] = 999;
console.log(stuff); // [5,10,999,20]
インデックス0
では、stuff[2]がどうして15に評価されるのでしょう? 15は配列stuff([5,10,15,20])の3番めの項目であり、2番めではないはずです。「わたしは幻覚を見ているのか?」あなたは自問します。「判断がそうとう鈍くなっているのか?」。いいえ、あなたは少なくともこのトピックに関しては健全です。JavaScriptやほとんどのプログラミング言語では、配列のインデックス付けは、慣れない当初は少し直感に反する方法で機能します。配列のインデックス付けはゼロベースなのです。
ゼロベースとは、外からは配列の最初の要素に見えるものを、JavaScriptはインデックス0として見る、という意味です。これは、stuffという名前の配列の最初の要素を取得するには、式stuff[0]
を記述する必要があるということで、要素を4つ持つ配列の最後の要素を取得するには、stuff[3](stuff[4]ではなく)を記述する、ということです。
こんな妙なことになった理由は直感的でなく、ほとんどがコンピュータプログラミングの歴史に関係しています。Wikipediaでは、ゼロインデックスの歴史に関する説明を読むことができます。
わたしは、配列のインデックスは、項目番号を数えるものではなく、項目がリストの最初からどれだけ離れているかを数えるものだ、という考え方が好きです。ある項目から次の項目へ移動するのに1ステップかかるとすると、リストの最初の項目は、リストの始まりからゼロステップの距離にあることになり、2つめの項目はリストの始まりから1ステップの距離にあることになります。以降も同様です。
配列に項目を追加する
配列値は、リストに項目を追加する.push()というメソッドを持っています。次のサンプルでは、このメソッドを使って、ユーザーがクリックしたら落下する矩形をスケッチに追加する、という当初のゴールにかなり近づいたものにしています。
複数の属性を追跡する
わたしたちはゴールに大いに近づきました。スケッチをクリックすると、確かに画面を下に移動する矩形が追加されます。しかしまだ、最初に思い描いたものとは違っています。そこでゴールを次のように変更しましょう。画面をクリックすると、矩形がマウスクリックした座標に現れ、そこから下に移動する、というゴールです。
そのためには、各矩形の2つの値を追跡する必要があります。これは少しやっかいで、方法は複数あり、それぞれに長所と短所があります。以下ではそれらを順番に実装していきます。
複数の配列
おそらくこの問題を解決する一番容易な方法は、各矩形のX座標を保持する配列と、Y座標を保持する配列を用意することです。1つめの矩形のX座標はrectX配列のインデックス0に、同じ1つめの矩形のY座標はrectY配列のインデックス0に来ることになります。以下はそのスケッチです。
実に見事です。しかし、先のことを考えるといくつかの欠点が見えてきます。
- 保持している各矩形のデータに別の属性を加えるには、3つめの配列が必要になる(後も同様で、属性の数だけ配列が必要になる)
- データから1つの”矩形”を削除するには、全部の配列からその”矩形が”相当する項目を削除する必要がある
- rectXとrectYの関係性がプログラムのシンタックスに反映されていない。ほかのプログラマーがこのコードを見ても、2つの変数(rectXとrectY)がつねにいっしょに使用されることになっているいるとは、直感的に判断できない
配列の中の配列
別の解決方法は、配列自体は値なので、配列は別の配列の中に保持できるという事実に依るものです。次のコードを空のスケッチに入力して出力結果を確認してみましょう。
const stuff = [];
stuff.push([24, 25]);
stuff.push([26, 27]);
console.log(stuff); // 表j [[24,25],[26,27]]
このコードを実行すると、変数stuffは要素を2つ持つ配列であり、その要素は2つとも配列であることが分かります。インデックス0の値を調べると、配列が出力されます。
console.log(stuff[0]); // 表示 [24,25]
この配列(stuff[0])から値の1つを取得するには、外側の配列に評価される式(stuff)に角かっこシンタックスを使って、内側の値にアクセスします。
console.log(stuff[0][1]); // 表示 25
この方法を使用すると、全部の矩形の情報を1つの配列が保持するバージョンのスケッチが作成できます。配列の要素自体もそれぞれ配列です。
この方法の優れた点は、クリックごとにrectXY配列に追加する配列に3つめの要素を追加することで、3つめの属性が簡単に各矩形に追加できることです。次の例では、クリックされるたびにインデックス2にランダムな値を保持し、draw()内でその数値を使って各矩形にランダムなカラーを与えています。
演習
配列に4つめの要素を追加して、矩形が落下するスピードを制御してみましょう。
オブジェクト
配列内配列の方法の欠点は、内側の配列の各要素が何を意味するものかをすぐ忘れてしまう、ということです。数値そのものに意味はありません(たとえば、X座標はなぜインデックス0に保持しているのか?)。JavaScriptに配列のようなデータ構造でデータが保持できる方法、つまり、複数の値が保持でき、配列よりも簡単で、何を意味するものか想起しやすい方法で、保持する各値が参照できる方法があれば非常に便利です。
実際そのようなデータ構造は存在します。それはオブジェクトと呼ばれ、配列のように複数の値が保持でき、オブジェクト内の個々の値には、数値のインデックスではなく、キーを使ってアクセスします。オブジェクトはJavaScriptのほかの値と同様に、変数に保持したり、配列に入れたり、ほかのオブジェクトの中に値として保持することまでできます。
オブジェクトの値を作成する基本的なシンタックスは次の通りです。
const asteroid = {radius: 100, mass: 460, population: 17};
このステートメントは、オブジェクトを作成して、それをasteroid(小惑星)という名前の変数に代入します。このオブジェクトは、radius(半径)、mass(質量)、population(人口)という3つの”キー”を持ちます(キーには名前をつけることができます。ここでは空想的な宇宙探査をテーマにしたオブジェクトを作成しているわけです)。特定のキーに保持した値にアクセスするには、次のシンタックスを使用します。
console.log(asteroid["radius"]);
// または
console.log(asteroid.radius);
オブジェクトを作成した後でも、キー/値のペアを追加することができます。そのためには、代入演算子の左辺に上記シンタックスを用い、保持したい値を右辺に置きます。
asteroid.albedo = 7;
// or:
asteroid["albedo"] = 7;
console.log()命令は、パラメータとしてオブジェクトを渡すと、そのオブジェクトのキー/値のペアをすべて出力します。
console.log(asteroid);
オブジェクトの配列(要素としてオブジェクトを持つ配列)の作成も簡単です。オブジェクトの配列は、コンピュータプログラマーとしてのキャリアの中で、オブジェクトを使用する主要な方法の1つです。ほとんどの場合、オブジェクトの配列は外部ソース(APIなど)から読み込んだり、プログラムの処理の中で構築します(たとえば、ユーザー入力からのデータを使うなどして)。しかし、配列リテラルとオブジェクトリテラルを次のように使用すると、オブジェクトの配列を記述することができます。
var kitties = [
{age: 14, weight: 12.2},
{age: 3, weight: 8.9},
{age: 8, weight: 11.0}
];
kittiesはオブジェクトの配列なので、次の式を使って個々の値が取得できます。
console.log(kitties[0]); // 表示 {"age":14,"weight":12.2}
リスト内のオブジェクトが持つ個々のキーの値を取得するには、次のようにします。
console.log(kitties[0]["age"]);
// または
console.log(kitties[0].age);
次のコードは、オブジェクトのリストを走査する例です。このループはリスト内のすべてのネコの体重の合計を出力します。
let weightSum = 0;
for (var i = 0; i < kitties.length; i++) {
weightSum += kitties[i].weight;
}
// 表示 32.1
console.log(weightSum);
オブジェクトとしての矩形
次のスケッチはここまでを全部まとめたものです。クリックするたびに、新しいオブジェクトを作成し、それをrectObjs配列に追加します。draw()では、適切なキーを使って値にアクセスして、矩形を描画します。
演習
上記サンプルを、rectObjsがXとY座標についての個々のスピードを保持するよう修正し、矩形が下方向以外の方向に移動するよう、draw()のループを変更します。ヒント:矩形がスケッチの境界に達すると”跳ね返る”コードを記述します。
他人のデータを使う(CSV形式)
世界のデータのすべてが、1つのスケッチの実行中にユーザーが取る実際のアクションの結果として発生するわけではありません。p5.jsを使用すると、別のデータソースのデータにアクセスすることができます。このセクションでは、CSV形式のデータを扱う方法を見ていきます。
データは次のサイトでも見つかります。
以降で扱うのは、NBAのフランチャイズとして名高いUTAH JAZZの2014/15シーズンの結果のCSVです。このデータはBasketball Referenceからのものです。
”CSV”は、カンマ区切りの値(comma separated values)を表す、構造化されたデータを含むプレーンなテキストファイルです。CSVファイルは基本的に、スプレッドシートと見なすことができ、ファイルの最初の行に列(column)の名前があります。データの"行"(row)は以降にあり、個々のセルはカンマで区切られます。CSV形式は、多くの異なるプログラミング言語やツールが理解でき(たとえば、ExcelやGoogle SheetsからCSVを書き出すことができます)、データを公開するための”データ交換形式”としてCSVを使用する組織も多くあります。
CSVファイルは概念的には、行の配列のようなもので、個々の行は。それ自体がキーを列名とし値をその列のセルとする、オブジェクトです。
p5.jsライブラリは、CSVファイルを操作するための関数群を提供します。loadTable()関数は、CSVファイルを特殊な表の値(これははp5.js固有です)としてメモリに読み込みます。loadTable()はpreload()で呼び出すべきで、そのシンタックスは次の通りです。
tableVar = loadTable('your_file.csv', 'csv', 'header');
変数tableVar には好きな名前を付けることができます。'csv'と'header'パラメータは、loadTable()に、これはCSVファイルでありヘッダ値(各行の列に名前を与えるために使用されます)を持っている、ということを伝えます。
この変数tableVarは、CSV内のデータを扱うために呼び出せるメソッドを持っています。
- .getRowCount():ファイル内の行の数に評価される
- .getNum(i, "foo"):列名"foo"を持つ行iのセルの値に評価される
次のスケッチは、Utah JazzのCSVファイルを使って、2014/15シーズンにJazzがプレーしたゲームのスコアを表示します。
let season;
function preload() {
season = loadTable(
'teams_UTA_2015_games_teams_games.csv',
'csv',
'header');
}
function setup() {
createCanvas(400, 400);
noLoop();
}
function draw() {
background(50);
for (let i = 1; i < season.getRowCount(); i++) {
fill(255);
ellipse(i * 5, 100 + season.getNum(i, "Tm"), 5, 5);
fill(0);
ellipse(i * 5, 100 + season.getNum(i, "Opp"), 5, 5);
}
}
メモ
下図はteams_UTA_2015_games_teams_games.csvファイルをExcelで開いたところを示しています。列は図の赤枠で示す、縦向きの並びを言い、行は青枠で示す、横向きの並びを言います。表の一番上の行がヘッダ(見出し)で、縦向きの列が何のデータかを示すものです。
上記サンプルのloadTable()の呼び出しでは、このCSVファイルにはヘッダが含まれているので、それをp5.jsに教えています。
.getRowCount()メソッドはヘッダ分を除いた行の数を返します。.getNum()メソッドは、指定された行と列にあるセルの値を返します。行は0から数えます。列にはヘッダ値が指定できます。次のコードの、Tmは得点数、Oppは相手の得点数の列の名前です。
// ヘッダ分を除いた行数
console.log(season.getRowCount()); // 82
// ヘッダを除いたTm列の3つめのセルの値
console.log(season.getNum(2, 'Tm')); // 118
// ヘッダを除いたOpp列の3つめのセルの値
console.log(season.getNum(2, 'Opp')); // 91