プログラミング はじめの一歩 JavaScript + p5.js編
7:ロボットが次々ポーズ

この記事の詳しい内容には毎日新聞の「ロボットが次々ポーズ」ページから有料記事に進むことで読めます。

概要

ポーズを取るロボットが何体かいます。ロボットは自分の左にいるロボットのポーズを見て、下図のルールにしたがって、自分のポーズを決めます。

たとえばロボットが3体並んでいるとき、向かって左端のロボット(1)が右手を上げると、その右のロボットは左手を上げ、その右のロボットは右足を上げます。

ここで問題です。ロボットが5体並んでいて、最初のロボットが右手を上げると、5番めのロボットはどんなポーズを取るでしょうか? 答えは下図です。ルールをなぞって結果を紙に書くなどして進んでいくと、自ずと答えは出ます。

では、これをプログラミングで表現するにはどのようにすればよいでしょう?

論理を考える

これは、何々ならばこうこうする、という条件で表すことができます。隣りのロボットのポーズが’右手’ならば、そのロボットのポーズは’左手’になる、といった具合です。

また最初のロボット、次のロボット、右隣りのロボットというように、順番や位置が関係するので、配列が利用できそうです。

プログラムのスタート時、ロボットはおそらく基本のポーズを取っていると思われるので、配列は下図のようなイメージです。向かって左端のロボットが最初のロボットで、配列のインデックス番号は0です。インデックス番号1のロボットにとって、右隣りのロボットはインデックス番号0のロボットです。

また、各ロボットの’基本’ポーズは’右手’や’左手’のポーズに変化するので、ロボットは状態を持っていると考えられます。最初の基本状態が、自分の右隣りのロボットの状態を調べることで、’右手’や’左手’の状態に変わるのです。

プログラムのスタート時、最初のロボット(インデックス番号0)は、外部からその状態を’基本’から’右手’に変えられます。すると、インデックス番号1のロボットはその状態変化を知り、一番上の図で示したルールにしたがって、自分の新しい状態を’左手’にするのです。

これにより、インデックス番号2のロボットはインデックス番号1のロボットの状態変化を知るので、インデックス番号1のロボットの新しい状態’左手’から、ルールにしたがって、自分の新しい状態を知ります。この状態変化の連鎖が最後のロボットまでつづきます。

どうでしょう? イメージが湧いたでしょうか?

状態を変化させる

まずは、このお題の最も根幹となる「状態を変化させる」ということから見ていきましょう。状態を変化させると言っても、基本ポーズから右手を上げるポーズに変える、ということではありません。もっとコンピュータに分かりやすい、プログラミングの立場から考えます。

今の場合、「状態を変化させる」とは、文字列’基本’を要素として5つ持つ配列の要素を’右手’や’左手’に変えることです。次のコードのように配列を作成し、最初の要素の値を’右手’に変えると、下図の結果が[コンソール]に表示されます。

// 5体のロボットの状態を保持する配列
let robotStatusArray = ['基本', '基本', '基本', '基本', '基本'];
print(robotStatusArray);
// 最初のロボットの状態を'右手'に変える
robotStatusArray[0] = '右手';
print(robotStatusArray);

もちろんこれは、ただインデックス番号0の要素の値’基本’を’右手’で上書きしただけです。しかし配列要素の文字列を’ロボット’と見なしたとき、インデックス番号0のロボットの状態は’基本’から’右手’に変化した、と考えられます。配列のこの並びは、外部から何らかの操作があって、インデックス番号0のロボットの状態が変化したタイミングを表しています。

とすると、次に必要なのは、インデックス番号1のロボットがインデックス番号0のロボットの状態変化を知り、ルールにしたがって、自分の状態を変化させる論理ということになります。しかしロボットと呼んでいるものは今はただの文字列で、自分の状態を変化させる能力は持っていません。そしてそもそもルールがコード化できていません。

そこで以降では、ロボットをいったん忘れ、配列の文字列を変化させるルールのコード化に取り組むことにします。

ルールのコード化

ルールを一度、プログラミング風の日本語で、今の文字列の配列に置き換えてみましょう。

1つめの「右隣りが右手を上げたときは左手を上げる」は、「対象とする要素の1つ前の要素の値が’右手’なら、対象とする要素の値を’左手’にする」ということです。

2つめの「右隣りが右足を上げたときは右手を上げる」は「対象とする要素の1つ前の要素の値が’右足’なら、対象とする要素の値を’右手’にする」ということです。

そして「1つ前の要素の値」が同時に別の値になることはないので、この2つの間には「そうでなく」が入ります。

if…else if風の書式で全部を表すと、

もし「要素の1つ前の要素の値」が'右手'なら
    要素の値は'左手'
そうでなくもし'右足'なら
     要素の値は'右手'
そうでなくもし'左足'なら
     要素の値は'右足'
そうでなくもし'左手'なら
     要素の値は'右足'

となります。

対象とする要素を基点に前の要素の値を調べるので、前述の配列robotStatusArrayが必要になります。そして、最初の要素をのぞく各要素に対して行うので、関数にすると便利です。

次はその例で、配列robotStatusArrayのインデックス番号をパラメータとして受け取るcheckPreviousAndSetStatus()関数です。

// 5体のロボットの状態を保持する配列
let robotStatusArray = ['基本', '基本', '基本', '基本', '基本'];

// 配列のインデックス番号を受け取り、1つ前の要素の状態から、当該要素の状態を決める
// ex: 2つめの要素の場合、1つ前の要素は'右手'なので、'左手'になる
function checkPreviousAndSetStatus(index) {
    // indexの1つ前の要素の状態を得る
    const previous = robotStatusArray[index - 1];
    let newStatus = '';
    // 1つ前の要素の状態によって場合分け(お題のルールの適用)
    if (previous === '右手') {
        newStatus = '左手';
    }
    else if (previous === '右足') {
        newStatus = '右手';
    }
    else if (previous === '左足') {
        newStatus = '右足';
    }
    else if (previous === '左手') {
        newStatus = '右足';
    }
    // 状態の配列の当該要素の値を新しい状態の値にする ex:基本 -> 左手
    robotStatusArray[index] = newStatus;
}

変数previousは前の要素の値を参照します。前の要素のインデックス番号は、indexパラメータから1引くことで計算できます。そしてif…else if文でpreviousを場合分けして調べ、変数newStatusに新しい値を代入します。これをrobotStatusArray[index]に割り当てると、それまでrobotStatusArray[index]にあった値(‘基本’)が上書きされます。

checkPreviousAndSetStatus()関数は次のように使用します。

// 最初のロボットは右手を上げる
robotStatusArray[0] = '右手';

// 確認
print(robotStatusArray);
// インデックス0の状態から、インデックス1の要素の状態を決める
checkPreviousAndSetStatus(1);
print(robotStatusArray);
// インデックス1の状態から、インデックス2の要素の状態を決める
checkPreviousAndSetStatus(2);
print(robotStatusArray);
// インデックス2の状態から、インデックス3の要素の状態を決める
checkPreviousAndSetStatus(3);
print(robotStatusArray);
// インデックス3の状態から、インデックス4の要素の状態を決める
checkPreviousAndSetStatus(4);
print(robotStatusArray);

 

robotStatusArray配列の最初の要素は、checkPreviousAndSetStatus()関数を呼び出す前に、強制的に’右手’で上書きしておく必要があります(外部からの操作)。コードを実行すると、[コンソール]に下図の結果が表示されます。これは、robotStatusArray配列の2つめの要素以降が、checkPreviousAndSetStatus()関数が呼び出されるたびに、1つずつ変更されたことを示しています。

状態変化の監視

論理を考える」の冒頭では、「ロボットは右隣りのロボットの状態を調べ、それを元にルールにしたがって自分の新しい状態を決める」といった旨を述べています。前のコードでは、checkPreviousAndSetStatus()関数を小間切れに呼び出すことでこれを行っていたわけです。しかし、外部から配列の最初の値を変えるだけで、後は自動的に進行するというのが理想です。

この自動的な進行はどうすれば実現できるでしょう? JavaScriptには、たとえばデータのダウンロード完了を教えてくれるイベントがあり、これを使うと、ダウンロード完了のタイミングで自動的に次の動作に移れるのですが、今の状態変化を教えてくれるような便利なイベントはありません。

ではどうするか? 「状態変化の監視」という考え方があります。状態が変化するかどうかを始終チェックしつづけ、変化が起きたらすぐにアクションを起こすのです。この高頻度でのチェックには、p5.jsのdraw()関数が利用できます。draw()はデフォルトで1秒間に60回という高い頻度で呼び出されるので、この中で監視すると、理論上変化が起こったその1/60秒後には対応できることになります。

ただしdraw()関数はp5.jsによってずっと呼び出されつづけるので、用済みのチェックはやめる方がプログラムは効率的になります。そのためにはまた別の仕組みが要ります。

次のコードでは、watchという名前の関数を新たに作成し、draw()関数から呼び出しています。また無用のチェックをなくすために、robotStatusArray配列の各要素の状態を変化させたかどうかを示すブール値を持つisChangedArray配列を作成しています。

// 5体のロボットの状態を保持する配列
let robotStatusArray = ['基本', '基本', '基本', '基本', '基本'];
// 5体のロボットの状態を変化させたかどうかを保持する配列
let isChangedArray = [false, false, false, false, false];
let isDone = false; // trueで終了
function setup() {
    createCanvas(400, 300);
}

function draw() {
    background(220);
    if (!isDone) {
        // 変化を監視
        watch();
        print(robotStatusArray);
        print(isChangedArray);
    }
}

// ページのマウスプレスでスタート
function mousePressed() {
    print('スタート');
    // 配列の0番めの状態を変化させる
    robotStatusArray[0] = '右手';
    isChangedArray[0] = true;
    print(robotStatusArray);
    print(isChangedArray);
}

// 変化を毎フレーム監視する
function watch() {
    // 1回で終わる
    // isChangeddArray[0]はボタンのクリックによってtrueなので、このif文は実行される
    if (isChangedArray[0]) {
        // インデックス0の状態から、インデックス1の要素の状態を決める
        checkPreviousAndSetStatus(1);
    }
    // isChangeddArray[1]は上のcheckPreviousAndSetStatus(1)によってtrueなので、
    // このif文は実行される
    if (isChangedArray[1]) {
        // インデックス1状態から、インデックス2の要素の状態を決める
        checkPreviousAndSetStatus(2);
    }
    if (isChangedArray[2]) {
        // インデックス2の状態から、インデックス3の要素の状態を決める
        checkPreviousAndSetStatus(3);
    }
    // インデックス3の状態から、インデックス3の要素の状態を決める
    if (isChangedArray[3]) {
        checkPreviousAndSetStatus(4);
    }
    if (isChangedArray[4]) {
        isDone = true;
        print('終了');
    }
}
// 配列のインデックス番号を受け取り、1つ前の要素の状態から、当該要素の状態を決める
// ex: 2つめの要素の場合、1つ前の要素は'右手'なので、'左手'になる
function checkPreviousAndSetStatus(index) {
    // indexの1つ前の要素の状態を得る
    const previous = robotStatusArray[index - 1];
    let newStatus = '';
    // 1つ前の要素の状態によって場合分け(お題のルールの適用)
    if (previous === '右手') {
        newStatus = '左手';
    }
    else if (previous === '右足') {
        newStatus = '右手';
    }
    else if (previous === '左足') {
        newStatus = '右足';
    }
    else if (previous === '左手') {
        newStatus = '右足';
    }
    // 状態の配列の当該要素の値を新しい状態の値にする ex:基本 -> 左手
    robotStatusArray[index] = newStatus;
    // 状態変化の配列の当該要素の値をtrueにする false -> true
    isChangedArray[index] = true;
}

上記コードを実行すると、下図の結果が得られます。出力されるのはrobotStatusArrayとisChangedArray配列の要素の値で、図の(1)はスタート前です。画面をクリックすると、robotStatusArray[0]に’右手’が、isChangedArray[0]にtrueが割り当てられます(図の(2))。そしてわずか1フレームで、(3)の結果になります。

watch()自体は次のif文をチェックする数だけ並べた単純な関数です。チェックしたいrobotStatusArray要素のインデックス番号に対応するisChangedArray要素の値がtrueなら、checkPreviousAndSetStatus()関数を呼び出すという仕組みです。画面のクリックでisChangedArray[0]をtrueに設定しているので、最初のif文のcheckPreviousAndSetStatus(1)は必ず実行されます。最後のチェックが終わるとisChangedArray[4]はtrueになるので、それを利用して、isDone変数をtrueにして、draw()からの呼び出しを停めています。

if (isChangedArray[0]) {
    // インデックス0の状態から、インデックス1の要素の状態を決める
    checkPreviousAndSetStatus(1);
}
...
if(isChangedArray[4]) {
    isDone = true;
    print('終了');
}

そしてcheckPreviousAndSetStatus()関数では、watch()関数からの1回のチェックが終わったときに、robotStatusArray配列のその要素でのチェックが終わったことを表すために、同じインデックス番号のisChangedArray配列の要素をtrueにしています。

// 状態変化の配列の当該要素の値をtrueにする false -> true
isChangedArray[index] = true;

なお、上記コードでは調べるrobotStatusArray要素が対応するisChangedArray要素をtrueにしてからcheckPreviousAndSetStatus()関数を呼び出しているので、watch()関数自体は1回の呼び出しで終了します。

robotStatusArrayとisChangedArrayの要素が順番に変化していることを確認するには、if文の並びを逆にします。

// 4回かかる
if (isChangedArray[4]) {
    isDone = true;
    print('終了');
}
if (isChangedArray[3]) {
    checkPreviousAndSetStatus(4);
}
if (isChangedArray[2]) {
    checkPreviousAndSetStatus(3);
}
if (isChangedArray[1]) {
    checkPreviousAndSetStatus(2);
}
if (isChangedArray[0]) {
    checkPreviousAndSetStatus(1);
}

すると同一フレームでisChangedArray要素がtrueにならないので、下図のように変化を出力することができます。

可視化

論理が整った、具体的には、期待する結果が[コンソール]に出力できるようになったので、ロボットのイメージを持ち込んで可視化していきましょう。

と言っても、右手を上げるロボットを作成するわけではありません。あらかじめ右手を上げたロボットの画像を作成しておき、それを元のイメージと置き換えるのです。イメージは全部で5種類(‘基本’状態に対応する正対姿勢も要る)なので、これも専用の配列に入れて管理します。

// 5体のロボットの状態を保持する配列
let robotStatusArray = ['基本', '基本', '基本', '基本', '基本'];
// 5体のロボットの状態を変化させたかどうかを保持する配列
let isChangedArray = [false, false, false, false, false];
let isDone = false; // trueで終了

// 各イメージ用変数
let baseImg, rightHandImg, leftHandImg, rightLegImg, leftLegImg;
let robotImages; // イメージを入れる配列

function preload() {
    baseImg = loadImage('images/base.png');
    rightHandImg = loadImage('images/rightHand.png');
    leftHandImg = loadImage('images/leftHand.png');
    rightLegImg = loadImage('images/rightLeg.png');
    leftLegImg = loadImage('images/leftLeg.png');
    // 最初は全部基本イメージ
    robotImages = [baseImg, baseImg, baseImg, baseImg, baseImg];
}

function setup() {
    createCanvas(800, 400);
}

// ページのマウスプレスでスタート
function mousePressed() {
    print('スタート');
    // 配列の0番めの状態を変化させる
    robotStatusArray[0] = '右手';
    isChangedArray[0] = true;
    robotImages[0] = rightHandImg;
}

function draw() {
    background(220);
    if (!isDone) {
        watch();
    }
    // キャンバスにロボットを5体描画
    image(robotImages[0], 50, 100);
    image(robotImages[1], 180, 100);
    image(robotImages[2], 310, 100);
    image(robotImages[3], 440, 100);
    image(robotImages[4], 570, 100);
}

// 変化を毎フレーム監視する
function watch() {
    // 4回かかる
    if (isChangedArray[4]) {
        isDone = true;
    }
    if (isChangedArray[3]) {
        checkPreviousAndSetStatus(4);
    }
    if (isChangedArray[2]) {
        checkPreviousAndSetStatus(3);
    }
    if (isChangedArray[1]) {
        checkPreviousAndSetStatus(2);
    }
    if (isChangedArray[0]) {
        checkPreviousAndSetStatus(1);
    }
}

// 配列のインデックス番号を受け取り、1つ前の要素の状態から、当該要素の状態を決める
function checkPreviousAndSetStatus(index) {
    // indexの1つ前の要素の状態を得る
    const previous = robotStatusArray[index - 1];
    let newStatus = '';
    let newImage;
    // 1つ前の要素の状態によって場合分け(お題のルールの適用)
    if (previous === '右手') {
        newStatus = '左手';
        // イメージも同様に処理する
        newImage = leftHandImg;
    }
    else if (previous === '右足') {
        newStatus = '右手';
        newImage = rightHandImg;
    }
    else if (previous === '左足') {
        newStatus = '右足';
        newImage = rightLegImg;
    }
    else if (previous === '左手') {
        newStatus = '右足';
        newImage = rightLegImg;
    }
    // 状態の配列の当該要素の値を新しい状態の値にする
    robotStatusArray[index] = newStatus;
    // イメージの配列も同様に変更する
    robotImages[index] = newImage;
    // 状態変化の配列の当該要素の値をtrueにする
    isChangedArray[index] = true;
}

クリックでこのプログラムの実行ページが開きます

イメージはまず、画像ファイルをpreload()関数で読み込んで作成します。そして上記コードでは、robotImages配列を作成し、robotStatusArray配列の’基本’状態に対応するbaseImgを5個入れています。

実際に描画するdraw()関数では、このrobotImagesを使って、イメージをロボットの数分だけ描画しています。

// キャンバスにロボットを5体描画
image(robotImages[0], 50, 100);
image(robotImages[1], 180, 100);
image(robotImages[2], 310, 100);
image(robotImages[3], 440, 100);
image(robotImages[4], 570, 100);

ロボットの各イメージは状態の文字列に対応するので、状態を変更している箇所に対応するイメージを設定するだけです。

// mousePressed()関数内
robotImages[0] = rightHandImg;
...
// checkPreviousAndSetStatus()関数内
let newImage;
...
// イメージも同様に処理する
newImage = leftHandImg;
...
// イメージの配列も同様に変更する
robotImages[index] = newImage;

プログラムは期待通りの動作をしますが、管理が必要な配列がいささか多くなった気がします。robotStatusArrayもisChangedArrayもrobotImagesも、同じインデックス番号の要素は同じロボットを表す値なので、たとえばObjectオブジェクトの属性としてまとめることができます。管理はこの方が容易になります。

ロボットが自分で調べ自分の状態を決める

オブジェクト指向プログラミング(OOP)と呼ばれる手法でこのお題を作成するのも面白いアプローチです。この手法で作成すると、ロボットの立場でプログラムを考えることができます。

また、前述したように、管理が必要な配列が多いので、状態やイメージなどを属性としてロボットに持たせると、配列の管理から解放されます。

OOPでは、クラスと呼ばれるものを作成して、そこにロボットの属性(位置や状態)やアクション(描画や前のロボットの状態を調べることなど)を決めていきます。次のコードはロボットを表すクラスの例です。

// PoseRobotクラス
class PoseRobot {
    constructor(x, y, index, imageArr) {
            this.x = x; // 表示する位置
            this.y = y;
            this.status = '基本'; // 状態変化を保持する。最初は'基本'
            this.isChanged = false; // 状態に変化があったかどうかを保持
            this.index = index; // 自分の番号(配列内のインデックス番号)
            this.images = imageArr; // イメージ配列
            this.image = imageArr[0]; // 自分のイメージ
        }
        // 描画
    display() {
            image(this.image, this.x, this.y);
        }
        // 状態を設定
    setStatus(newStatus) {
            this.status = newStatus;
        }
        // イメージを設定
    setImage(newImage) {
            this.image = newImage;
        }
        // 変更済みにする
    setIsChanged(isValue) {
            this.isChanged = isValue;
        }
        // 1つ前のロボットの状態から、自分の状態を決める
    checkPreviousAndSetStatus(robotArray) {
        // 自分の1つ前のロボットはthis.index - 1で分かる
        const previous = robotArray[this.index - 1].status;
        // 1つ前の状態によって場合分け(お題のルールの適用)
        if (previous === '右手') {
            this.setStatus('左手');
            this.setImage(this.images[2]);
            this.setIsChanged(true);
        }
        else if (previous === '右足') {
            this.setStatus('右手');
            this.setImage(this.images[1]);
            this.setIsChanged(true);
        }
        else if (previous === '左足') {
            this.setStatus('右足');
            this.setImage(this.images[3]);
            this.setIsChanged(true);
        }
        else if (previous === '左手') {
            this.setStatus('右足');
            this.setImage(this.images[3]);
            this.setIsChanged(true);
        }
    }
}

すると、次のコードで、前のプログラムと同様に動作するプログラムが作成できます。PoseRobotクラスから作成したr1、r2,…r5オブジェクトは初めこそ同じ状態(statusが’基本’)で、同じイメージ(imageがimageArr[0]、つまりbaseImg)ですが、r1の状態が外部から変えられることで、後は自分が調べて自分の状態を変えていきます。

let robots; // PoseRobotクラスのインスタンスを入れる配列
let robotImages; // イメージを入れる配列

function preload() {
    const baseImg = loadImage('images/base.png');
    const rightHandImg = loadImage('images/rightHand.png');
    const leftHandImg = loadImage('images/leftHand.png');
    const rightLegImg = loadImage('images/rightLeg.png');
    const leftLegImg = loadImage('images/leftLeg.png');
    // イメージを配列に入れる
    robotImages = [baseImg, rightHandImg, leftHandImg, rightLegImg, leftLegImg];
}

function setup() {
    createCanvas(800, 400);
    // PoseRobotクラスのインスタンスを5個作成
    // キャンバス左から右に作成、3つめの数値はrobots配列のインデックス番号に対応
    const r1 = new PoseRobot(50, 100, 0, robotImages);
    const r2 = new PoseRobot(180, 100, 1, robotImages);
    const r3 = new PoseRobot(310, 100, 2, robotImages);
    const r4 = new PoseRobot(440, 100, 3, robotImages);
    const r5 = new PoseRobot(570, 100, 4, robotImages);
    // 作成したインスタンスを作成順に配列に入れる
    robots = [r1, r2, r3, r4, r5];
}


// ページのマウスプレスでスタート
function mousePressed() {
    // 一番左のrobots[0]の状態を'右手'に変更
    robots[0].setStatus('右手');
    robots[0].setIsChanged(true);
    robots[0].setImage(robotImages[1]);
}

function draw() {
    background(220);
    // ロボットの配列を走査(一番左のロボットは前のロボットがないので除外)
    for (let i = 1; i < robots.length; i++) {
        // ロボットを特定
        const robot = robots[i];
        // 変更済みでなければ
        if (!robot.isChanged) {
            // 1つ前のロボットの状態から、このロボットの状態を決める
            robot.checkPreviousAndSetStatus(robots);
        }
        // ロボットを描画
        robots[0].display();
        robot.display()
    }
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA