これは10月3日の信濃毎日新聞に掲載されていた記事です。詳しい内容には毎日新聞の「おかしに当たり券を入れる」ページから有料記事に進むことで読めます。
目次
概要
舞台はとあるおかし工場です。そこでは、飴、せんべい、クッキー、ポテトチップスを作ってます。おかしを作る順番に決まりはなく、どれが作られるかは分かりません。作られたおかしはベルトコンベアを流れます。このときおかしに当たり券を入れるのですが、それには「飴のふたつ後にせんべいが来たら、その次のおかしに当たり券を入れる」という決まりがあります。
たとえば、せんべい、クッキー、飴、クッキー、せんべい、ポテトチップスと流れて来た場合、3つめが飴で、その2つ後がせんべいなので、その1つ後のポテトチップスに当たり券を入れます。
これをJavaScript + p5.jsでプログラミングしてみよう、というのが本稿です。
論理を組み立てる
プログラミングで最も重要なのは論理の組み立てです。論理とは、人間が見てもコンピュータが見ても、誰がどう見ても、こうなるしかないと思われる理屈です。完成後に目に見えているのは論理の視覚表現にすぎず、プログラム本体の重要な部分は舞台裏で働く論理です。
物事をどう表すか?
このプログラムでは、実際におかしを作るわけではなく、おかしを別のもので表す必要があります。また、どのおかしが作られるかは分からないので、無作為性をプログラムに持ち込む必要があります。実際の世界に普通に存在するこういった物事をプログラミングの仮想世界でどうやって表せばよいのでしょう?
プログラムは何の問題もなくすんなり完成することはまずありません。もしあったら、それはまだ改善の余地のある可能性があります。おかしを表す場合でも、いきなり画像を用意してそれの読み込みから始めるのではなく、もっともっと単純なものでおかしを表すことから始めます。するとどこかで行き詰まるかもしれません。そのときには文字以外の別のもので表すことを考えればよいのです。
おかしは文字で表す
おかしはokashiなので、”okashi”という文字(文字列)にしようとしても、これでは役不足です。おかしは4種類あるので、違いを表す必要があります。最も分かりやすく一般的なのは、”ame”、”senbei”などアルファベット小文字で表す方法です。”Ame”でも問題はありませんが、JavaScriptは大文字小文字を区別するので、プログラムのどこかで記述を間違えたとき、うまく動作しなくなります。日本語文字の”飴”でも構いませんが、舞台裏で働く論理の中で積極的に日本語文字を使う理由はありません。
おかしを作るとは?
今の場合、おかしを作るとは、「おかしを作れ」という命令に対し、”ame”や”senbei”などの文字列を無作為に返すことです。ロボットに例えるなら、「おかしを作れ!」という命令に対し、’ame’か’senbei’か’cookie’か ‘potatechips’のどれかを無作為に決めてそれを渡してくれるロボットです。ロボットは作るおかしのリストを持っていて、そこから無作為におかしを選びます。
このロボットの機能は関数で作成できます。たとえばcreateOkashiという名前で関数を作成し、そこから’ame’を返すと、関数の呼び出し元に’ame’が返されるので、呼び出し元では、おかしとして’ame’が作られたのだな、と分かります。
おかしのリストはあらかじめ配列で作っておきます。
// 作るおかしのリスト
let okashiList = ['ame', 'senbei', 'cookie', 'potatechips'];
// おかしを作る関数
function createOkashi() {
// 'ame'を返す
return 'ame';
}
無作為とは?
無作為とは好き嫌いなく、不公平なく、おかしのリストからおかしを1つ選ぶことです。これはサイコロの出目と同じで、どのおかしも同じ確率で選ばれます。このランダム性はp5.jsのrandom()関数で作成できます。
random()関数を、おかしを作って返すcreateOkashi()関数の中で、おかしのリストokashiListと合わせて使うと、おかしの文字列をランダムに返すことができます。
// okashiListのおかしをランダムに作成して返す
function createOkashi() {
// 0-3をランダムに得る
const rndNum = int(random(0, 4));
// okashiListからおかしをランダムに選ぶ
const newOkashi = okashiList[rndNum];
// 作ったおかしを呼び出し元に返す
return newOkashi;
}
random(0,4)からは、0から4未満の小数値が返されます。その値をp5.jsのint()関数に渡すと、0か1か2か3の整数になります。これを配列okashiListのインデックス位置に適用すると、’ame’か’senbei’か’cookie’か’potatechips’のどれかが決まります。たとえばrndNumが0の場合には変数newOkashiには’ame’が割り当てられます。
次々におかしを作るとは?
おかしをランダムに作るにはcreateOkashi()関数を呼び出します。「次々におかしを作る」とは、「次々に」createOkashi()関数を呼び出すことだと考えられます。では「次々に」はどうしたら表現できるでしょう?
おかしは一定の距離をおいてベルトコンベアを流れてくると想像できるので、「次々に」はcreateOkashi()関数を一定の時間間隔(インターバル)で呼び出すことで表せます。それにはJavaScriptのsetInterval()関数が利用できます。setInterval()には、第1引数として実行したい関数を、第2引数としてインターバルの時間をミリ秒単位で指定します。下記コードの場合、1000を渡しているので1秒おきに、第1引数に指定した関数(名前のない関数)内のcreateOkashi()が呼び出されます。これにより、新しいおかしが1秒おきに「次々と」作成されるようになります。
let intervalID = window.setInterval(() => {
// createOkashi()で新しいおかしをランダムに作る
const newOkashi = createOkashi();
}, 1000);
作ったおかしの管理
createOkashi()関数は作ったおかしを表す文字列を返すので、上のコードではその文字列を変数newOkashiに代入しています。これは”次々と作られるおかし”なので、createOkashi()が呼び出されるたびに値が変わります。
また冒頭で示した図のように、作ったおかしをベルトコンベアに流すようなアニメーションを作成する場合には、作ったおかしを覚えておく必要があります。
そしてさらに、このプログラムの課題である「飴のふたつ後にせんべいが来たら、その次のおかしに当たり券を入れる」を実現するには、どのおかしをどういう順番で作っているのかを覚えておく必要があります。
具体的に言うと、「飴のふたつ後にせんべいが来たら」を知るには、”ame”が作られたときを知る必要があり、その”ame”の2つ後に作られたおかしが何かを調べる必要があります。”ame”が作られたときその2つ後のおかしはまだ作られていません。タイミングで言うと未来です。”ame”が作られたときにそれを覚えておき、その次の次のおかしが作られたタイミングで、そのおかしが”senbei”であるかを確かめることになるので、作られたおかしとその順番を覚えておく必要があるのです。
そのためには、作ったおかし用の配列を用意します。そして、おかしを作ったときにそれに追加していきます。
// 作ったおかしを保持しつづける配列
let okashiArray;
...
// createOkashi()で新しいおかしをランダムに作る
const newOkashi = createOkashi();
// 新しいおかしを配列の末尾に追加する
okashiArray.push(newOkashi);
おかしをランダムに作成して配列に追加する論理
では、ここまで見てきたことをp5.jsのプログラムにしてみましょう。
次のサンプルでは、p5.jsのボタンを使って、おかしの作成開始と停止を制御しています。
// 作るおかしの種類
let okashiList = ['ame', 'senbei', 'cookie', 'potatechips'];
// 作ったおかしを保持しつづける配列
let okashiArray;
function setup() {
createCanvas(400, 300);
// スタート時は空
okashiArray = [];
// setInterval()の関数の呼び出しを止めるための番号
let intervalID;
const startButton = setButton('START', {
x: 20,
y: 320
});
// [START]ボタンのクリックでおかしの作成を開始する
startButton.mousePressed(() => {
// setInterval()の関数を呼び出す時間間隔
let ms = 2000; // => おかしを2秒おきに作る
intervalID = window.setInterval(() => {
// createOkashi()で新しいおかしをランダムに作る
const newOkashi = createOkashi();
// 新しいおかしを配列の末尾に追加する
okashiArray.push(newOkashi);
// ベルトコンベアに流れるおかし(左のものほど古い)
print(okashiArray);
}, ms);
});
// [STOP]ボタンのクリックでおかしの作成をやめる
const stopButton = setButton('STOP', {
x: 150,
y: 320
});
stopButton.mousePressed(() => {
window.clearInterval(intervalID);
});
}
function draw() {
background(220);
}
// okashiListのおかしをランダムに作成して返す
function createOkashi() {
// 0-3をランダムに得る
const min = 0;
const max = okashiList.length;
const rndNum = int(random(min, max));
// okashiListからおかしをランダムに選ぶ
const newOkashi = okashiList[rndNum];
// 作ったおかしを呼び出し元に返す
return newOkashi;
}
function setButton(label, pos) {
const button = createButton(label);
button.size(100, 30);
button.position(pos.x, pos.y);
return button;
}
ブラウザのデベロッパーツールの[コンソール]には、おかしを表す文字列を持つ配列が表示されます。何回か繰り返しておかしの文字列を見ていると、確かにランダムに作成されていることが分かります。
ここには留意しておくべきことがあります。[コンソール]には、同じ文字列、たとえば’ame’と’ame’が表示されますが、これらは文字列としては同じであるものの、おかしとしては別のものです。
飴のふたつ後にせんべいが来たら、その次のおかしを特定する論理
ここまでで、2秒おきに「おかしを作れ!」という命令を出し、その命令を受けた関数が、おかしのリストからランダムにおかしの文字列を選んで、それをおかしの配列に作った順番に追加していく、というプログラムができました。
次はいよいよ、このプログラムの核心、「飴のふたつ後にせんべいが来たら、その次のおかしに当たり券を入れる」です。
飴のふたつ後
この課題をじっとながめていると、実はけっこう複雑であることが分かります。「飴のふたつ後にせんべいが来たら」と簡単に言っていますが、そのためにはまず、作られたおかしが’ame’であるかないかを調べる必要があります。そしてそれが’ame’なら、2つ後のおかしが’senbei’であるかどうかを調べるのですが、待ってください! 今作られたのは’ame’で、次のおかしはまだ作られていません! 当然その次のおかしもまだです。作られていないものをどうやって調べるのでしょう?
とは言え、冷静になって考えると、’ame’が作られ、その2つ後におかしが作られた後なら、それが何かを調べることはできます。
ともかくコードを書いてみましょう。書き始める場所は、setInterval()関数に渡す第1引数の関数内、おかしの配列に新しいおかしを追加した後です。
intervalID = window.setInterval(() => {
// createOkashi()で新しいおかしをランダムに作る
const newOkashi = createOkashi();
// 新しいおかしを配列の末尾に追加する
okashiArray.push(newOkashi);
// ベルトコンベアに流れるおかし(左のものほど古い)
print(okashiArray);
// 条件:飴のふたつ後にせんべいが来たら、次のおかしに当たり券を入れる。
// おかしの配列を全部調べる
for (let i = 0; i < okashiArray.length; i++) {
// 調べている対象のおかしが飴なら
if (okashiArray[i] === 'ame') {
// その2つ後のおかしを特定する
const after2 = okashiArray[i + 2];
print(after2);
}
}
}, ms);
作ったおかしは全部okashiArrayに入れているので、作ったおかしはokashiArrayの走査で調べられます。
for (let i = 0; i < okashiArray.length; i++) {
変数iはokashiArrayのインデックス位置に使用できるので、i位置にあるのが’ame’かどうかが調べられます。
if (okashiArray[i] === 'ame') {
okashiArrayのi位置にあるのが’ame’なら、okashiArray内でその’ame’の2つ後にあるおかしを特定します。2つ後のおかしは、i位置にある’ame’の2つ右にあるので、okashiArray[i + 2]で参照できるはずです。
// その2つ後のおかしを特定する
const after2 = okashiArray[i + 2];
print(after2);
前のサンプルに上記コードを加えてプログラムを実行すると、[コンソール]にたとえば次のような結果が表示されます。[]はprint(okashiArray);の、その下のundefinedやpotateshipsはprint(after2);の出力結果です。
このときの上記コードの動作内容を上から順に見ていくと、
- 1回めのcreateOkashi()の呼び出しで’potatechips’が作られる。okashiArrayは今[‘potatechips’]なので、forループ内のif (okashiArray[i] === ‘ame’)にはひっかからず、after2は出力されない。
- 2回めのcreateOkashi()の呼び出しで’ame’が作られる。okashiArrayは今[‘potatechips’,’ame’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。しかしそこに要素はないので、undefined(未定義)が出力される。
- 3回めのcreateOkashi()の呼び出しで’senbei’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。しかしそこに要素はないので、undefinedが出力される。
- 4回めのcreateOkashi()の呼び出しで’potatechips’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’,’potatechips’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。今度はここに’potatechips’があるので、after2は’potatechips’として出力される。
- 5回めのcreateOkashi()の呼び出しで’potatechips’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’,’potatechips’,’potatechips’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。ここには前回同様’potatechips’があるので、after2は’potatechips’として出力される。
- 6回めのcreateOkashi()の呼び出しで’ame’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’,’potatechips’,’potatechips’,’ame’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。ここには前回同様’potatechips’があるので、after2は’potatechips’として出力される。またiが5のときにもif (okashiArray[i] === ‘ame’)が成立するので、okashiArray[5 + 2]であるafter2が探される。しかしそこに要素はないので、undefinedが出力される。結果、’potatechips’とundefinedが出力される。
- 7回めのcreateOkashi()の呼び出しで’cookie’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’,’potatechips’,’potatechips’,’ame’,’cookie’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。ここには’potatechips’があるので、after2は’potatechips’として出力される。またiが5のときにもif (okashiArray[i] === ‘ame’)が成立するので、okashiArray[5 + 2]であるafter2が探される。しかしそこに要素はないので、undefinedが出力される。結果、’potatechips’とundefinedが出力される。
- 8回めのcreateOkashi()の呼び出しで’senbei’が作られる。okashiArrayは今[‘potatechips’,’ame’,’senbei’,’potatechips’,’potatechips’,’ame’,’cookie’,’senbei’]なので、iが1のときif (okashiArray[i] === ‘ame’)が成立し、okashiArray[1 + 2]であるafter2が探される。ここには’potatechips’があるので、after2は’potatechips’として出力される。またiが5のときにもif (okashiArray[i] === ‘ame’)が成立するので、okashiArray[5 + 2]であるafter2が探される。今度はここに’senbei’があるので、after2は’senbei’として出力される。結果、’potatechips’と’senbei’が出力される。
となります。眠くなるような長い文章ですが、腰をすえて読んでみてください。
undefinedとはまだ定義されていないことを意味するJavaScriptの予約語です。MDN web docの「undefined」ページには、
まだ値が代入されていない変数は undefined 型となります。評価しようとしている変数に値が代入されていない場合、メソッドや文も undefined を返します。
と説明されています。
今の場合には、const after2 = okashiArray[i + 2];で、配列内に存在しない要素を参照しようとしたとき、after2はundefinedとなります。つまり、近いうちに存在するようになるのだけれど今はまだない配列要素をundefinedとして処理できるのです。ないものを参照しようとするとエラーが発生してプログラムが停止するように思えますが、JavaScriptの配列では存在しないインデックス位置にアクセスすると、undefinedが返されます。
飴のふたつ後にせんべいが来たら、その次のおかしを特定する
いよいよプログラムの核心に近づいてきました。
「飴のふたつ後にせんべいが来たら」は、JavaScriptの立場で言うと「after2が’senbei’なら」ということで、「その次のおかしを特定する」は、「okashiArrayの(i+3)番めの要素を取得する」ということです。ただしafter2のときと同様、この要素(after3)もまだそこにない可能性があります。特定したいのはこのafter3です。
以上を踏まえてコードを書くと、次のようになります。
for (let i = 0; i < okashiArray.length; i++) {
// 調べている対象のおかしが飴なら
if (okashiArray[i] === 'ame') {
// その2つ後のおかしを特定する
const after2 = okashiArray[i + 2];
// after2はまだない場合がある
if (after2 !== undefined) {
// そのおかしがせんべいなら
if (after2 === 'senbei') {
// その1つ後のおかしを特定する
const after3 = okashiArray[i + 3];
// after3はまだない場合がある
if (after3 !== undefined) {
// 当たり券を入れるおかしのインデックス位置
print(i + 3);
// 当たり券を入れるおかし
print(after3);
}
}
}
}
}
after2とafter3はundefinedである場合があるので、それを除外したい場合には、「undefinedでないなら」を意味する式!== undefined が使用できます。
前のコードに上記コードを追加し、プログラムを実行すると、たとえば下図のような結果が得られます。3回めの呼び出しで’ame’が来て、5回めの呼び出しで’senbei’が来ているので、当たり券を入れるのはその次の’cookie’だとプログラムは言っています。これで当たり券を入れるおかしを特定することができました!
当たり券を入れるには?
実際のおかしの場合には、おかしの箱の中に当たり券を入れればよいのでしょうが、プログラムの場合はどうすればよいのでしょう?
上記の場合で言うと、okashiArray配列の5番めにある’cookie’という文字列を当たりにしたいわけです。この後も同じ文字列である’cookie’が追加される可能性は大いにあります。したがってokashiArray配列の5番めにあるこの’cookie’を、ほかの’cookie’と区別する必要があります。
実を言うと、ただの文字列では役不足です。たとえば当たりはずれ専用の配列を作っておき、okashiArrayにおかしを追加するように、はずれの場合は’hazure’、当たりの場合は’atari’と、okashiArrayの要素と対応させていく方法もないではありません。しかしいささか面倒です。
プログラミングには属性(プロパティ)と呼ばれる概念があり、属性はオブジェクトにつけることができます。オブジェクトとはJavaScriptの最も基本を成すもので(「JavaScript オブジェクトの基本」を参照)、今の場合には、文字列に代えておかしを表すものとして使用できます。
オブジェクト(Objectオブジェクト)はJavaScriptから簡単に作成でき、「名前(属性名、プロパティ名):値」の形式でいくつでもその属性、つまりほかと異なる特性が追加できます。
今の場合なら、例えばname属性に値’ame’を設定すると、そのオブジェクトで飴が表せます。またname属性に’cookie’を持つ同じクッキーでも、atari属性をtrue/falseに設定することで、当たりのクッキーとはずれのクッキーを表すことができます。
おかしの文字列をオブジェクトに置き換える
おかしにnameやatari属性を付加するには、これまで文字列で表してきたおかしをObjectオブジェクトに置き換える必要があります。
そのためにはcreateOkashi()関数に手を加えます。
// okashiListのおかしをランダムに作成して返す
function createOkashi() {
// 0-3をランダムに得る
const min = 0;
const max = okashiList.length;
const rndNum = int(random(min, max));
// okashiListからおかしをランダムに選ぶ
const newOkashi = okashiList[rndNum];
// おかしを表すオブジェクト
// nameとatariプロパティを持つ
const okashiObject = {
name: newOkashi,
atari: false
};
// 作ったおかしを呼び出し元に返す
return okashiObject;
}
書き換えるのは、okashiListからランダムにおかしの文字列を選んだ後です。前は選んだ文字列を返していましたが、ここでおかしを表すオブジェクトを作成し、それを返すようにします。
Objectオブジェクトは、変数名 = {属性名:値}の形式で簡単に作成できます。複数の属性を与えるときにはコンマで区切ります。上記コードでは、name属性にnewOkashiという値を、atari属性にfalseを設定しています。newOkashiは文字列なので、name属性の値は’ame’や’senbei’などになります。atari属性はどのオブジェクトでもfalseに設定されます。これは、おかしが作られたときには当たり券はどれも入っていない、ということを意味します。
オブジェクトの属性値を得るには、そのオブジェクトを参照する変数名と属性名をドット(.)をつなぎます。
okashiObject.name; // ‘ame’や’senbei’
okashiObject.atari; // false
作成したおかしのオブジェクトは、前と同様に呼び出し元に返され、okashiArray配列に追加されます。前は文字列が追加されましたが、変更後はおかしのオブジェクトが追加されます。
また、前のサンプルではおかしが飴やせんべいであるかどうかを、文字列を参照する変数を使っていたので、変数.nameに変更する必要があります。たとえばforループ内で対象が’ame’であるかを調べるif (okashiArray[i] === ‘ame’) { や after2が’senbei’であるかを調べるif (after2 === ‘senbei’) { です。
おかしに当たり券を入れるということは、おかしのオブジェクトのatari属性の値をtrueに設定するということです。当たりのおかしを参照するのは変数after3なので、after3のatari属性をtrueに設定します。
intervalID = window.setInterval(() => {
// createOkashi()で新しいおかしをランダムに作る
const newOkashi = createOkashi();
// 新しいおかしを配列の末尾に追加する
okashiArray.push(newOkashi);
// ベルトコンベアに流れるおかし(左のものほど古い)
print(okashiArray);
// 条件:飴のふたつ後にせんべいが来たら、次のおかしに当たり券を入れる。
// 4つ未満のときには調べる必要がない
if (okashiArray.length >= 4) {
// おかしの配列を全部調べる => 前に調べたものも重複して調べることになる
for (let i = 0; i < okashiArray.length; i++) {
// 調べている対象のおかしが飴なら
if (okashiArray[i].name === 'ame') {
// その2つ後のおかしを特定する
const after2 = okashiArray[i + 2];
// after2はまだない場合がある
if (after2 !== undefined) {
// そのおかしがせんべいなら
if (after2.name === 'senbei') {
// その1つ後のおかしを特定する
const after3 = okashiArray[i + 3];
// after3はまだない場合がある
if (after3 !== undefined) {
// 当たり券を入れるおかしのインデックス位置
print(i + 3);
// 当たり券を入れるおかし
print(after3);
// 該当するおかしに当たりの印を付ける(atariプロパティをtrueする)
after3.atari = true;
}
}
}
}
}
}
}, ms);
修正したプログラムを実行すると、[コンソール]には、配列の各括弧([])の中に{…}が入った結果が表示されます。{…}はObjectオブジェクトを表します。[の左の三角マークをクリックすると、オブジェクトを展開して表示することができます。
下図は[コンソール]表示の例で、(5)の三角マークをクリックしています。
0は配列内の0番め(最初)の要素です。name属性値が’potatechips’、atari属性値がfalseであることが分かります。この場合、2つめのオブジェクトのname値が’ame’で、その2つ後のオブジェクトのname値が’senbei’なので、4番めの要素であるオブジェクト(‘senbei’)のatari属性がtrueになっています。
無作為に作られる飴、せんべい、クッキー、ポテトチップスのおかしについて、「飴のふたつ後にせんべいが来たら、その次のおかしに当たり券を入れる」という論理をこれで組み立てることができました。
非常に面白いのが、このプログラムはおかしでなくてもほかのものにも転用できることです。変わらないのはランダム性や、「飴のふたつ後にせんべいが来たら、その次のおかしに当たり券を入れる」という決まりで、”ame”は飴でなくても、たとえば青い靴でも、プログラムは’ame’を青い靴だと見なすので、論理は破綻しません(もちろん、’ame’が青い靴だと人間の方が混乱するので、’ame’ではなく’blueshoes’などの方が分かりやすくはなります)。
とは言え、次の視覚化を行うと、’ame’は飴でしかなくなります。次はここまで構築してきた論理を、目で見て分かるように表舞台に反映させていくます。
視覚化に取り組む
論理は舞台裏で働くもので、それを誰かに見せようとすると、目に見えるもので表現することになります。ここまでのプログラムは、結果をブラウザの[コンソール]に表示するだけでしたが、視覚化を行うと、ブラウザ画面に結果が表示できるようになります。p5.jsは特に視覚化が得意です。
視覚化に必要なもの
視覚化に必要なものと言えば、一般的には画像ファイルです。飴やせんべいを表す画像を用意します。下図はその例です。ベルトコンベアを流れるようなアニメーションにする場合には、画像のサイズを揃えるようにします。また下図では「おかしに当たりを入れる」ということを、おかしの上に赤い星の画像を置くことで表現しています。
作られたおかしの出口やベルトコンベアはp5.jsの矩形で表現しています。
上図のようにおかしのイメージの下端を揃えるには、透明の背景も含む画像ファイルのサイズをほぼ同じにし、おかしのイメージの下端を画像ファイルの下端に揃える方法が簡単です。p5.jsのimage()関数はイメージをその左上隅を基点に描画します。
画像の読み込み
画像ファイルはp5.jsのloadImage()関数で読み込みます。するとp5.jsのp5.Imageオブジェクトとして使用できるようになります。画像ファイルの読み込みはp5.jsのpreload()関数で行います。
下記コードでは、画像ファイルのイメージ用変数をそれぞれ用意し、各イメージの対応する変数に割り当てています。またおかしのイメージを入れておく配列も変数で用意しています。
// おかしのイメージ
let ameImage, senbeiImage, cookieImage, potatechipsImage;
// イメージを保持する変数(配列)
let imageList;
// 当たりを表す赤い星のイメージ
let atariImage;
function preload() {
// おかしのイメージを読み込んでおく
ameImage = loadImage('img/ame.png');
senbeiImage = loadImage('img/senbei.png');
cookieImage = loadImage('img/cookie.png');
potatechipsImage = loadImage('img/potatechips.png');
atariImage = loadImage('img/atari.png');
}
読み込んだイメージは、setup()関数内でイメージ用の配列に入れます。このときイメージの順番を、okashiListの順番と同じに、つまり[飴、せんべい、クッキー、ポテトチップス]にします。順場を同じすることで、おかしのオブジェクトを作成するcreateOkashi()関数内のrndNum変数がimageListにもそのまま適用できるようになります。詳しくは後述します。
function setup() {
createCanvas(1000, 150);
// 線はなし
noStroke();
...
// イメージ配列に、おかしのイメージを入れる。
// 順番をokashiListと同じにするところがミソ
imageList = [ameImage, senbeiImage, cookieImage, potatechipsImage];
...
おかしのオブジェクトのパワーアップ
非常に重要なことなので、最初に言います。論理の構築で作成したおかしのオブジェクトには、属性に加え、アクションを付けることができます。具体的に言うと、たとえばそのオブジェクトのx座標の位置を表す属性をxとし、値0を設定します。そしてそのx属性の値を呼び出しごとに1ずつ追加するアクションをオブジェクトに加え、オブジェクトに対し頻繁にそのアクションを実行するよう命令すると、そのオブジェクトは自分のx位置を1ずつ大きくするので、結果として移動するアニメーションになります。このアクションはオブジェクトのメソッドと呼ばれます。簡単に言うとそのオブジェクトが自分で実行する関数です。
おかしのオブジェクトにイメージを加える
現在、おかしのオブジェクトはnameとatari属性しか持っていないので、これにおかしのイメージの属性を加えましょう。name属性に’ame’を持つオブジェクトには、飴のイメージのameImageを設定するということです。
それにはcreateOkashi()関数内のokashiObjectオブジェクトを作成するコードでimg属性を追加します。このとき、1つ前の値(今の場合ならfalse)の後にコンマ(,)を入れます。オブジェクトを作成する{と}の中では、属性名:値 のペアはコンマで区切る必要があります。名前のimgはほかのものでも構いません。イメージであることが分かる名前にします。
ではその値はどうやって決めればよいでしょう? オブジェクトのイメージを全部飴にしたいならameImageを指定すればよいのですが、今の場合には、せんべいにはsenbeiImageを、クッキーにはcookieImageを設定したいわけです。
ここで出て来るのがimageListとrndNumです。rndNumには0から3までの整数がランダムに入ります。rndNumが0の場合、okashiList[rndNum]は’ame’なので、newOkashiは’ame’です。そしておかしのイメージを入れたimageListには、okashiListのおかしと同じ順番でおかしのイメージを入れているので、imageList[rndNum]は飴のameImageを指すことになります。
function createOkashi() {
// 0-3をランダムに得る
const min = 0;
const max = okashiList.length;
const rndNum = int(random(min, max));
// okashiListからおかしをランダムに選ぶ
const newOkashi = okashiList[rndNum];
// おかしを表すオブジェクト
// nameとatariプロパティを持つ
// imgプロパティを追加
const okashiObject = {
name: newOkashi,
atari: false,
// おかしのイメージ
img: imageList[rndNum]
...
おかしのオブジェクトにアクションを加える
設定したイメージを早速描画してみたいところですが、それにはもう少し手順が要ります。img: imageList[rndNum]を追加しただけなので、このokashiObjectはimg属性をどう扱っていいかまったく知りません。このimg属性はおまえのイメージとして描画するのだよ、と教える必要があるのです。
これを教えるのがアクション、つまりメソッドの定義です。メソッドも属性と同じようにして、名前:値の形で追加します。メソッドの値は関数なので、function(){…}の形式で指定します。
メソッドで実行する具体的な内容は、値のfunction(){…}の[と}の間に記述します。今の場合には、img属性が参照するイメージを描画したいので、p5.jsのimage()関数が利用できます。
image()関数には、描画するイメージと描画する位置(キャンバス上のxy座標)を指定する必要があります。位置はオブジェクトの外から与えることもできますが、今の場合、おかしのオブジェクトは多数作成するので、それぞれのおかしにいちいち位置を指定するのは大変です。そこで少し面倒でも、このオブジェクトの属性として追加することにします(xとy座標)。
メソッドの名前も自由に決められますが、アクションの内容が分かる名前にします。下記はその例です。
const okashiObject = {
name: newOkashi,
atari: false,
// おかしのイメージ
img: imageList[rndNum],
// 位置変更用
x: 10,
y: 50,
// イメージを描画する
display: function() {
image(this.img, this.x, this.y);
}
}
オブジェクトの{と}の間に入れる属性名:値のペアはコンマで区切ることを忘れないようにします。JavaScriptコードの書き方(構文)にミスがあると、[コンソール]にエラーが表示され、プログラムは動作しません。
上記では、x属性に10、y属性に50を指定しています。これはキャンバスの左上隅から右に10ピクセル、下に50ピクセル進んだ位置に相当します。メソッド名はdisplayです。メソッドも属性と考えられ、その値はJavaScriptの関数オブジェクトです。実行内容は、image()関数を使ってimg属性が参照するイメージを、位置(x,y)に描画することです。
上記コードのimage()関数の引数には、this.xとthis.yを指定しています。このthisというのはJavaScriptのキーワードで、今の場合には、ここで作成されるおかしのオブジェクトを指します。したがってthis.xはこのオブジェクトのx属性を、this.yはこのオブジェクトのy属性を指し、値はそれぞれ10と50になります。
おかしのオブジェクトのメソッドを実行する
オブジェクトに定義したメソッドには、nameやatari属性のときと同じように、そのオブジェクトを参照する変数の後にドットをつづけ、その後にメソッド名をつづけることでアクセスできます。ただしそのメソッドを実行するので、名前の後にかっこ()を付けます。
okashiObject.display();
今の場合、おかしのオブジェクトはokashiArray配列に追加しているので、オブジェクトにはこの配列を経由してアクセスします。おかしのイメージを描画するコードは、p5.jsのdraw()関数内に記述します。draw()関数はデフォルトで毎秒60回呼び出されるので、アニメーションに利用できます。
okashiArray配列におかしのオブジェクトが含まれているときに、okashiArray[0].display()を実行すると、okashiArrayの最初に含まれているおかしのイメージが描画できます。
function draw() {
background(220);
// おかしの配列に要素が含まれているなら
if (okashiArray.length >= 1) {
// おかしのオブジェクトのdisplay()メソッドを呼び出す
okashiArray[0].display();
}
}
ここまでのコードを実行すると、ランダムに選ばれたおかしのイメージが下図のように、p6.jsのキャンバスに描画されます。また[コンソール]にはokashiArray配列の要素が表示されます。三角マークをクリックして配列を展開すると、オブジェクトの中身が表示されます。nameやatariに加え、新たに追加したxやy、img属性がその値とともに表示されます。またdisplay()メソッドも表示されます。値のf()は関数オブジェクトであることを示しています。
ブラウザのリロードボタンを押して、プログラムを何回か再実行すると、描画されるおかしが変わります。これについて、本稿をここまで読んで来られた方なら、当然だと思われるでしょう。
命令を与えているのは、どの場合もokashiArray[0].display()です。okashiArrayの最初にはランダムに異なるおかしのオブジェクトが入る可能性が高いので、その場合には異なるおかしのイメージが描画されます。呼び出されるのはそのオブジェクトのdisplay()メソッドです。オブジェクトはdisplay()が呼び出されたら、自分のimgプロパティの値を探し、image()関数を使って、自分のxとyプロパティの値の位置に、自分のimgプロパティの値であるイメージを描画するのです。
このようにおかしのオブジェクトは、ほかのおかしのオブジェクトとは異なる、それ自体が独立したものとして見ることができます。
おかしのアニメーションを実行する
今のところ、おかしのオブジェクトのxとyプロパティの値に変化はないので、おかしのオブジェクトはどれも同じ位置に描画されます。しかしこの値を少しずつ変化させると、作成した複数のおかしが移動するアニメーションが作成できます。
* オブジェクトの位置のプロパティを変更するとなぜアニメーションになるのかは、「1_2 アニメーションが動いて見える原理」で述べています。
アニメーションにするには、おかしのオブジェクトに、自分のxプロパティの値を変更するメソッドを定義します(ベルトコンベア上を移動するアニメーションにするので、水平方向のx値だけを変更します)。
次のコードでは、updateという名前のメソッドを定義し、this.xに0.5を足しています。
}, // 前の名前:値のペアの後にコンマを忘れずにつける
// 位置を更新する => 毎フレーム、右へ0.5ずつ進む
update: function() {
this.x += .5;
}
そして、draw()関数内で、okashiArrayを走査し、配列に含まれる全おかしオブジェクトのupdate()とdisplay()メソッドを呼び出します。
function draw() {
background(220);
if (okashiArray.length >= 1) {
for (let i = 0; i < okashiArray.length; i++) {
// おかしのオブジェクトのupdate()メソッドを呼び出す
okashiArray[i].update();
// おかしのオブジェクトのdisplay()メソッドを呼び出す
okashiArray[i].display();
}
}
}
ここまでの変更を保存し、プログラムを実行すると、おかしがランダムに作成され、右に移動するアニメーションが描画されます。
アニメーションを作成するときには、まず位置など、アニメーションしたいプロパティを変更し、その後実際の描画を行います。この順序が逆になると、描画してからプロパティを変更することになるので、その位置での描画が1フレーム遅れることになります。オブジェクト自身が自分で自分を描画する場合には、上記コードのように、プロパティを更新するupdateメソッドと描画を行うdisplayメソッドを分けて定義し、updateを呼び出してからdisplayを呼び出すようにします。
当たりの赤い星を追加する
このプログラムでは、当たりのおかしの少し上に赤い星を描くことで、そのおかしが当たりである(当たり券を入れる)ことを表します。赤い星のイメージはpreload()関数内で変数atariImageとして読み込み済みです。
当たりの赤い星を追加するにはどうすればよいか、論理で考えてみましょう。
当たりのおかしは、setInterval()関数に渡す名前のない関数内に長々と記述したatari3です。このオブジェクトはatari属性がtrueになっています。
if (after3 !== undefined) {
// 当たり券を入れるおかしのインデックス位置
print(i + 3);
// 当たり券を入れるおかし
print(after3);
// 該当するおかしに当たりの印を付ける(atariプロパティをtrueする)
after3.atari = true;
}
ということは、おかしのイメージを描画するときに、そのオブジェクトのatari属性の値を調べればよいのではないでしょうか? おかしのイメージを描画するのは、おかしのオブジェクトのdisplay()メソッドです。
// イメージを描画する
display: function() {
image(this.img, this.x, this.y);
// atariプロパティがtrueなら、おかしのイメージの少し上に赤い星を描く
if (this.atari) {
// 30は画像サイズの半分、10は当たりの星画像の半分
// 30を足し10を引くことで、おかし画像のほぼ真上に描画できる
image(atariImage, this.x + 30 - 10, this.y);
}
},
後は、赤い星がうまくおかしの上に来るように、image()関数に渡す値を調整します。このプログラムで使用しているおかしや赤い星のイメージのサイズでは、上記のthis.x + 30 – 10とthis.yが適当なようです。
おかしの画像ファイルはどれもサイズがほぼ同じ(60 x 60)なので、イメージの左上隅(this.x)からイメージの幅の半分を足し、そこから星のイメージの幅の半分を引くと、星のイメージはおかしのイメージのほぼ真ん中に来る計算になります。
ベルトコンベアとおかしの出口を描く
では最後に、おかしを流すベルトコンベアとおかしの出口をp5.jsの矩形で描きましょう。
実際にはおかしは自力で右に移動しており、おかしの出口から出てくるわけではありません。しかし位置や描画順を工夫することで、おかしが次々と出口から出てきて、ベルトコンベアの上を流れるように見えます。
コードを書くのはdraw()関数内です。draw()では、先に描画したものが後から描画したものに上書きされる(絵の具で上塗りするのと同じです)ので、おかしのアニメーションより先にベルトコンベアを適切な位置に描画すると、おかしがベルトコンベアの上にのっているように見えます。
おかしの出口は、同じ理屈で、おかしより後に描画します。矩形でおかしを隠すと、おかしがそこから出て来るように見えます。
function draw() {
background(220);
// 横長のベルトコンベア
// おかしのイメージより先に描画することで、
// おかしのイメージがベルトコンベアに乗っているように見える
fill(100);
rect(0, 105, width, 12);
// おかしの配列を走査し、
// おかしのオブジェクトのupdate()とdislay()メソッドを呼び出す
if (okashiArray.length >= 1) {
for (let i = 0; i < okashiArray.length; i++) {
// おかしのオブジェクトのupdate()メソッドを呼び出す
okashiArray[i].update();
// おかしのオブジェクトのdisplay()メソッドを呼び出す
okashiArray[i].display();
}
}
// おかしの出口
fill(0, 200, 0);
rect(0, 40, 50, 80);
}