プログラミング はじめの一歩 JavaScript + p5.js編
20:目的地への道順を伝える

この記事の詳しい内容には毎日新聞の「目的地への道順伝える」ページから有料記事に進むことで読めます。

概要

下図は、スタート地点から、野球場とプール、体育館とテニスコートへ行く道順を示した案内図です。途中の分かれ道には案内板が3つあります。

女の子は、道順を伝えるとき、最初の案内板に「1」、次の案内板に「2」という番号を付けます。たとえば野球場へ行きたい人には、「1左、2左」と伝えます。これは、「最初の案内板で左に曲がり、次の案内板で左に曲がる」ということです。

では、問題です。女の子は、テニスコートまで行きたい人に、何と伝えるでしょう?

論理を考える

女の子は人間なので、案内図と案内板を見ただけで、テニスコートへの道順を「1右、2右」と容易に伝えることができます。これが問題の答えです。しかしこれをコンピュータにやらせるとなると、相当高度なプログラミングが必要です。

スタート地点から目的地への経路をプログラミングで調べることは経路探索(Pathfinding)と呼ばれます。そのための方法(アルゴリズム)があり、A*(A-star,エースター)が有名です。A*の理解は容易ではありませんが、当サイトでは、A*よりも理解しやすい幅優先探索アルゴリズムによる経路探索の方法を解説しています。

とはいえ本稿で見ていくのは、難しい経路探索ではなく、女の子に道順を教えられた車の動きです。たとえば、「右、右」と教えられた車は、スタート地点からスタートし、最初の案内板(A)で右に曲がり、次の案内板(B)で右に曲がってテニスコートに到着するというものです。車は教えられた通りに進み、左右どちらに曲がるかを決めることはできません。

概念的なグリッドを考える

まず考えるのは、車が移動する地面です。これは、上図のように、正方形が横に9個、縦に6個並ぶグリッド(格子模様)として考えます。上図はキャンバスの描画結果ですが、ここで考えるのは、その前段階の概念的なグリッドです。

概念的なグリッドはJavaScriptの2次元配列で表せますが、もっと分かりやすいように、エクセルの表にしたのが下図です。ここではエクセルのセルに、0,0、0,1という言わば”地番”を付けています。そして上図のスタート地点や分岐、道、行先に当たるセルに名前をつけています。

プログラムではセルの地番を元にキャンバスに”転写”、つまり矩形やイメージを描画します。たとえば、スタート地点は(4, 4)なので、キャンバスで(4, 4)に当たる位置に黄色の矩形を描きます。テニスコートは(1, 7)なので、キャンバスで(1, 7)に当たる位置にテニスコートのイメージを描画します。

したがって、今最も重要なのは、この表をコードで表すことです。表は縦と横の2方向に変化するので、JavaScriptの2次元配列で表すことができます。

グリッドの作成
グリッドは「格子ブロックのひな型コード」で作成できます。

const rows = 9;
const columns = 6;
// 論理の元になる2次元配列
let grid = [];

function setup() {
    createCanvas(550, 400);
    background(220);
    for (let c = 0; c < columns; c++) {
        grid[c] = [];
        for (let r = 0; r < rows; r++) {
            grid[c][r] = 'empty';
        }
    }
    print(grid);
}

grid配列の出力からは下図の結果が得られます。これは、外側の配列の中に、内側の配列が6個が含まれていて、内側の配列はそれぞれ9個の要素を持つという2次元配列の構造になっています。たとえば下図左上隅の’empty’には、grid[0][0]でアクセスできます。grid[0]は6個ある内側の配列の最初の配列で、つづく[0]は、その最初の配列の最初の要素を指します。

2次元配列のgridが作成できたので、エクセルの表を見ながら、grid配列の内側の配列要素に具体的な値を設定します。上記コードでは全部の要素を’empty’にしているので、これを上書きする形になります。値を入れてくれる便利な配列のメソッドがあればよいのですが、残念ながらないので、1つずつ丁寧に指定していきます。当然ながら地番の指定を誤ると、プログラムは思ったように動作しないので、この作業には丁寧さが求められます。

grid[1][1] = 'ballpark';
grid[1][3] = 'pool';
grid[1][5] = 'gym';
grid[1][7] = 'tenniscourt';

grid[4][4] = 'start';

grid[3][4] = 'cross1';
grid[1][2] = 'cross2';
grid[1][6] = 'cross2';

grid[3][2] = 'turnToRight';
grid[3][3] = 'toLeft';
grid[3][5] = 'toRight';
grid[3][6] = 'turnToLeft';
grid[2][2] = 'toUp';
grid[2][6] = 'toUp';
print(grid);

ここで指定している値はそのマスの意味付けを行います。たとえば’ballpark'(野球場)は目的地です。’turnToRight’は右折でtoLeft’は左へ進むことを意味します。

grid配列を出力すると、下図の結果が得られます。これで前のエクセルの表をJavaScriptの2次元配列に置き換えることができました。

論理で車を進める

2次元配列のgridという、概念的なグリッドが作成できたので、次はそこを概念的に移動する車を考えていきましょう。

車は最初、スタート地点にあります。これはgrid[4][4]、エクセルの表で言う(4, 4)です。ここからは上にしか進めないので、車は1マス上に進みます。そこは(3, 4)です。つまり車は(4, 4)から(3, 4)へと、上に1マス進んだわけです。車の移動にはこの地番の変化を利用します。

Carクラス

車はCarクラスで表すことにします。車の位置は地番で決めるので、これをプロパティとして定義します(gridAとgridB)。上へ1マス移動するメソッドでは、gridAの今の値から1だけ引けばよいことになります。

// Caeクラス
class Car {
    constructor(gridA, gridB) {
            this.gridA = gridA; // grid配列の地番に相当する
            this.gridB = gridB;
    }
    // 上に進む
    goUp() {
        // grid配列の地番に相当する値を操作
        this.gridA = this.gridA - 1;
    }

右に進むメソッドと左に進むメソッドも同様に定義できます。車は下に進む場合はないので、下に進むメソッドは要りません。

// 右に進む
    goRight() {
            this.gridB = this.gridB + 1;
    }
    // 左に進む
    goLeft() {
        this.gridB = this.gridB - 1;
    }
}

これで、たとえば、let car = new Car(4, 4); によって、スタート地点からスタートするcarインスタンスが作成できるようになりました。

今いる位置を調べ移動するメソッド

前に示したエクセルの表は、車にとっては”指示書”のようなものと考えることができます。車が、今自分のいる場所の地番を調べられるようになると、grid配列の要素の値(‘start’や’toRight’など)が分かります。すると、たとえばそれが’start’ならgoUp()を実行し、’toRight’ならgoRight()を実行すればよいわけです。

// 今いる位置を調べ移動する
checkAndMove() {
        // 今いる場所の地番の値を調べる
	const currentGrid = grid[this.gridA][this.gridB];
        // 値によって向きを変える
        // 'start'なら上に行くしかない
        if (currentGrid === 'start') {
            this.goUp();
        }
	...

ここで1つ、考えなくてはいけない事柄があります。それは地番の値が’cross1’と’cross2’のときです。そのとき車は案内板のある場所にいるので、女の子が教えてくれた道順にしたがうことになります。

女の子が教えてくれた道順を、グローバル変数の配列directions = [‘right’, ‘right’] とすると、currentGridの値を調べる一連のif文とは別のif文で、directions配列を調べ、その値によって向きを決めます。

else if (currentGrid === 'cross1') {
    print('1つめの案内板')
    // 案内の道順の1つめにしたがって向きを変える
    const direction = directions[0];
    // 案内が右なら、
    if (direction === 'right') {
        // 右へ進む
        this.goRight();
        // 案内が左なら
    }
    else if (direction === 'left') {
        // 左に進む
        this.goLeft();
    }
}

また、今いる場所が目的地なら移動は終了です。

// 4つの目的地のどれかに着いたら終わり
}
else if (currentGrid === 'ballpark' || currentGrid === 'pool' || currentGrid === 'gym' || currentGrid === 'tenniscourt') {
    print(currentGrid);
    print('終了');
}

今いる位置を調べ移動するこのcheckAndMove()メソッドは、currentGrid変数が取り得るすべての値を網羅した無骨な方法ですが、車を確実に進むべき次のマスげ進めてくれます。

論理のまとめ

ここまでをまとめると、次のようになります。車のcheckAndMove()メソッドは、1回の呼び出して車を1マス進めるので、目的地に着くまで何度も呼び出す必要があります。次のコードではこれをsetInterval()関数で行っています。

const rows = 9;
const columns = 6;
// 論理の元になる2次元配列
let grid = [];

let car;
// 案内の道順
let directions = ['right', 'right'];
// 最後にクリアするタイマー
let timerID;

function setup() {
    createCanvas(550, 400);
    background(220);

    for (let c = 0; c < columns; c++) {
        grid[c] = [];
        for (let r = 0; r < rows; r++) {
            grid[c][r] = 'empty';
        }
    }

    // grid配列に"地番"を指定して役割を決める
    grid[1][1] = 'ballpark';
    grid[1][3] = 'pool';
    grid[1][5] = 'gym';
    grid[1][7] = 'tenniscourt';

    grid[4][4] = 'start';

    grid[3][4] = 'cross1';
    grid[1][2] = 'cross2';
    grid[1][6] = 'cross2';

    grid[3][2] = 'turnToRight';
    grid[3][3] = 'toLeft';
    grid[3][5] = 'toRight';
    grid[3][6] = 'turnToLeft';
    grid[2][2] = 'toUp';
    grid[2][6] = 'toUp';

    // Carインスタンスをスタート位置で作成
    car = new Car(4, 4);
    // タイマーを使って、車を進める
    timerID = window.setInterval(() => {
        car.checkAndMove();
    }, 1000);
}

// Caeクラス
class Car {
    constructor(gridA, gridB) {
            this.gridA = gridA; // grid配列の地番に相当する
            this.gridB = gridB;
        }
        // 今いる位置を調べ、移動する
    checkAndMove() {
        // 今いる場所の地番の値を調べる
        const currentGrid = grid[this.gridA][this.gridB];
        /// 値によって向きを変える
        // startなら上に行くしかない
        if (currentGrid === 'start') {
            print(currentGrid);
            this.goUp();
            // cross1なら左右の選択肢がある
        }
        else if (currentGrid === 'cross1') {
            print(currentGrid);
            print('1つめの案内板')
                // 案内の道順の1つめにしたがって向きを変える
            const direction = directions[0];
            // 案内が右なら、
            if (direction === 'right') {
                // 右へ進む
                this.goRight();
                // 案内が左なら
            }
            else if (direction === 'left') {
                // 左に進む
                this.goLeft();
            }
            // toRightなら右に進むしかない
        }
        else if (currentGrid === 'toRight') {
            print(currentGrid);
            this.goRight();
            // turnToLeftなら左折するしかない
        }
        else if (currentGrid === 'turnToLeft') {
            print(currentGrid);
            this.goUp(); // 左折は上に進むこと
            // toUpなら上に進むしかない
        }
        else if (currentGrid === 'toUp') {
            print(currentGrid);
            this.goUp();
            // cross2なら左右の選択肢がある
        }
        else if (currentGrid === 'cross2') {
            print(currentGrid);
            print('2つめの案内板');
            // 案内の道順の2つめにしたがって向きを変える
            const direction = directions[1];
            if (direction === 'right') {
                this.goRight();
            }
            else if (direction === 'left') {
                this.goLeft();
            }
            // toLeftなら左に進むしかない
        }
        else if (currentGrid === 'toLeft') {
            print(currentGrid);
            this.goLeft();
            // turnToRightなら右折するしかない
        }
        else if (currentGrid === 'turnToRight') {
            print(currentGrid);
            this.goUp(); // 右折は上に進むこと
            // 4つの目的地のどれかに着いたら終わり
        }
        else if (currentGrid === 'ballpark' || currentGrid === 'pool' || currentGrid === 'gym' || currentGrid === 'tenniscourt') {
            print(currentGrid);
            print('終了');
            // タイマーを切って呼び出しを止める
            window.clearInterval(timerID);
        }
    }

    // 上に進む
    goUp() {
            // grid配列の地番に相当する値を操作
            this.gridA = this.gridA - 1;
        }
        // 右に進む
    goRight() {
            this.gridB = this.gridB + 1;
        }
        // 左に進む
    goLeft() {
        this.gridB = this.gridB - 1;
    }
}

このコードを実行すると、[コンソール]にcurrentGrid変数の値が出力されます。下図左はdirections配列に[‘right’, ‘right’]を指定したときの結果で、テニスコートに着くのが分かります。ほかの場合も同様です。このdirections配列への要素の設定は、女の子が教える道順です。

基本的な論理はこれで完成です。次はこの論理を使って可視化を行っていきましょう。

可視化

まずは格子模様からです。前に述べているように、キャンバスに描く主要な要素は全部、grid配列から”転写”して描画します。

格子模様の描画

格子模様(グリッド)は、前述したように「格子ブロックのひな型コード」で作成できます。

まずはそのために必要な変数をグローバル変数で作成します。

// 矩形の格子模様の描画に必要
const gutter = 0;
const w = 50; // 矩形の幅 
const h = 50; // 矩形の高さ
const offsetX = 50; // キャンバス左端からのオフセット量
const offsetY = 50; // キャンバス上端からのオフセット量

そして、grid配列の要素(‘start’や’cross1’など)から格子模様を描く関数を定義します。関数では、grid配列で使った同じcolumnsとrowsを使った2重のforループを用い、内側のforループでカウンタ変数のcとrを使ってgrid配列の要素にアクセスしその値によって、描画する矩形の色を変えます。これが’転写’と言っている作業です。grid配列と同じ構造の格子模様を描き、grid配列の相当する要素を参照して矩形を写し取るように描くので、このように呼んでいるわけです。

// 矩形をgrid配列の要素の値に応じて色分けして描画する
function fillSquare() {
    // grid配列と同じcolumns数とrows数を使って、
    for (let c = 0; c < columns; c++) {
        for (let r = 0; r < rows; r++) {
            // grid配列の要素を参照
            const t = grid[c][r];
            // スタートの矩形の色
            if (t === 'start') {
                fill(255, 255, 0);
                // 道の矩形の色
            }
            else if (t === 'toRight' || t === 'toLeft' || t === 'toUp' || t === 'turnToRight' || t === 'turnToLeft') {
                fill(250);
                // 案内板のある矩形の色
            }
            else if (t === 'cross1' || t === 'cross2') {
                fill(195, 157, 81);
                // そのほか
            }
            else {
                fill(208, 227, 187);
            }
            // rect()で矩形の格子模様を描く
            rect(offsetX + r * (gutter + w), offsetY + c * (gutter + h), w, h);
        }
    }
}

このfillSquare()関数をsetup()関数内で呼び出すと、下図が描画されます。

イメージの読み込みと描画

次にイメージの読み込みと描画を行います。車のイメージには次の4つの画像ファイルを使用します。車は、向いている方向によって描画するイメージを切り替えます。たとえば上を向いているスタートのときはtoUp.pngのイメージを使います。読み込んだ車の4つのイメージはcarsImage配列に右、下、左、上の順で入れます。

// イメージ
let carsImage;
let ballparkImage, poolImage, gymImage, tenniscourtImage;

// 使用するイメージを読み込む
function preload() {
    const toRight = loadImage('images/toRight.png');
    const toDown = loadImage('images/toDown.png');
    const toLeft = loadImage('images/toLeft.png');
    const toUp = loadImage('images/toUp.png');
    carsImage = [toRight, toDown, toLeft, toUp];

    ballparkImage = loadImage('images/ballpark.png');
    poolImage = loadImage('images/pool.png');
    gymImage = loadImage('images/gym.png');
    tenniscourtImage = loadImage('images/tenniscourt.png');
}

目的地を描画
setup()関数に次のコードを記述すると、目的地のイメージが描画できます(車の描画はまだです)。

// setup()内
// イメージをセンターで描画する
imageMode(CENTER);

// イメージを描画
image(ballparkImage, offsetX + 1 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
image(poolImage, offsetX + 3 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
image(gymImage, offsetX + 5 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
image(tenniscourtImage, offsetX + 7 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);

imageMode(CENTER)の呼び出しによって、image()関数の引数x,yでイメージのセンターが指定できるので、車が向きを変えたときのイメージの描画が簡単になります。

また目的地を描画するimage()関数の引数はずいぶん長いので、関数にした方がすっきりします。しかし引数の指定で行っていることが格子模様の描画と同じであることを示すために、ここでは関数にまとめず長いままにしています。

rect(offsetX + r * (gutter + w), offsetY + c * (gutter + h), w, h);

ポイントは、rect()の引数で指定しているrとc(forループのカウンタ変数)を、イメージを描画したい地番に置き換えていることです。野球場は(1,1)なので、
offsetX + 1 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2 とし、
プールは(1,3)なので、
offsetX + 3 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2 で描画しています。ほかも同様です。これもgrid配列を元にした”転写”です。

下図は4つの目的地を描画した結果です。

車を描画
最後は車の描画です。まずCarクラスでイメージを扱えるように変更します。

コンストラクタでは、パラメータにイメージの配列(imgs)を追加し、描画位置を決めるx,yとイメージの配列を参照するimages、現在のイメージを参照するimageプロパティを追加し初期値を設定します。

xとyプロパティの値もgrid配列を元にした”転写”です。imgsには車のイメージを入れたcarsImageを割り当てるので、imageプロパティの初期値this.images[3]は上向きのtoUpになります。

// Carクラス
class Car {
    constructor(gridA, gridB, imgs) {
            ...
            // grid配列の地番から描画できるように数値を調整
            this.x = this.gridB * w + offsetX + w / 2;
            this.y = this.gridA * h + offsetY + h / 2;
            // イメージ変更にはthis.images配列を使用する
            this.images = imgs;
            this.image = this.images[3];
        }
        // 描画
    display() {
        // grid配列の地番から描画できるように数値を調整
        this.x = this.gridB * w + offsetX + w / 2;
        this.y = this.gridA * h + offsetY + h / 2;
        image(this.image, this.x, this.y);
    }

またgoUp()、goRight()、goLeft()メソッドにも、イメージを設定します。たとえば上向きのときはimagesプロパティにimages配列の上向きのイメージを割り当てます。Carクラスの変更は以上です。

// 上に進む
goUp() {
        ...
        this.image = this.images[3];
    }
// 右に進む
goRight() {
        ,,,
        this.image = this.images[0];
    }
// 左に進む
goLeft() {
    ...
    this.image = this.images[2];
}

Carクラスのコンストラクタを変更したので、setup()関数内のCarインスタンスを作成するnew Car()の3つめの引数にcarsImageを指定します。

// Carインスタンスをスタート位置で作成
car = new Car(4, 4, carsImage);

最後はdraw()関数を記述します。毎フレーム呼び出されるdraw()関数で、まず矩形の格子模様を描画し、その後目的地を描き、最後に車を描画すると、車が格子模様の上を移動して、目的地に到着するように表現できます。

function draw() {
    // 矩形を役割り別に色分けして描画
    fillSquare();
    // イメージを描画
    image(ballparkImage, offsetX + 1 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(poolImage, offsetX + 3 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(gymImage, offsetX + 5 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(tenniscourtImage, offsetX + 7 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    // 車を描画
    car.display();
}

ここまで記述すると、論理はもう出来上がっているので、車は自動的にスタートし、directions配列で指定された道順にしたがって目的地まで移動します。

最終的なプログラムコード

以下に全コードを示します。ここでは画面のクリックでスタートするように変更しています。また女の子のイメージや、キャンバス横に車の現在位置を表示するリストを追加しています。

const rows = 9;
const columns = 6;
// 論理の元になる2次元配列
let grid = [];

let car;
// 案内の道順
let directions = ['right', 'right'];
// 最後にクリアするタイマー
let timerID;

// 矩形の格子模様の描画に必要
const gutter = 0;
const w = 50; // 矩形の幅 
const h = 50; // 矩形の高さ
const offsetX = 50; // キャンバス左端からのオフセット量
const offsetY = 50; // キャンバス上端からのオフセット量

// イメージ
let carsImage;
let ballparkImage, poolImage, gymImage, tenniscourtImage, girlImage;

// 画面のクリックでスタート
let isStart = false;
let list;


// 使用するイメージを読み込む
function preload() {
    const toRight = loadImage('images/toRight.png');
    const toDown = loadImage('images/toDown.png');
    const toLeft = loadImage('images/toLeft.png');
    const toUp = loadImage('images/toUp.png');
    carsImage = [toRight, toDown, toLeft, toUp];

    ballparkImage = loadImage('images/ballpark.png');
    poolImage = loadImage('images/pool.png');
    gymImage = loadImage('images/gym.png');
    tenniscourtImage = loadImage('images/tenniscourt.png');
    girlImage = loadImage('images/girl.png');
}


function setup() {
    createCanvas(550, 400);
    background(220);

    // イメージをセンターで描画する
    imageMode(CENTER);

    for (let c = 0; c < columns; c++) {
        grid[c] = [];
        for (let r = 0; r < rows; r++) {
            grid[c][r] = 'empty';
        }
    }

    // grid配列に"地番"を指定して役割を決める
    grid[1][1] = 'ballpark';
    grid[1][3] = 'pool';
    grid[1][5] = 'gym';
    grid[1][7] = 'tenniscourt';

    grid[4][4] = 'start';

    grid[3][4] = 'cross1';
    grid[1][2] = 'cross2';
    grid[1][6] = 'cross2';

    grid[3][2] = 'turnToRight';
    grid[3][3] = 'toLeft';
    grid[3][5] = 'toRight';
    grid[3][6] = 'turnToLeft';
    grid[2][2] = 'toUp';
    grid[2][6] = 'toUp';

    // 矩形を特性別に色分けして描画
    fillSquare();

    // イメージを描画
    image(ballparkImage, offsetX + 1 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(poolImage, offsetX + 3 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(gymImage, offsetX + 5 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(tenniscourtImage, offsetX + 7 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(girlImage, 50, 300);

    // Carインスタンスをスタート位置で作成
    car = new Car(4, 4, carsImage);

    list = createElement('ol');
    list.position(550, 50);
}

function draw() {
    // 矩形を役割り別に色分けして描画
    fillSquare();
    // イメージを描画
    image(ballparkImage, offsetX + 1 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(poolImage, offsetX + 3 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(gymImage, offsetX + 5 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(tenniscourtImage, offsetX + 7 * (gutter + w) + w / 2, offsetY + 1 * (gutter + h) + h / 2);
    image(girlImage, 50, 300);
    // 車を描画
    car.display();
}

// 画面のマウスプレスでスタート
function mousePressed() {
    if (!isStart) {
        timerID = window.setInterval(() => {
            car.checkAndMove();
        }, 1000);
        isStart = true;
    }
}

// 矩形をgrid配列の要素の値に応じて色分けして描画する
function fillSquare() {
    // grid配列と同じcolumns数とrows数を使って、
    for (let c = 0; c < columns; c++) {
        for (let r = 0; r < rows; r++) {
            // grid配列の要素を参照
            const t = grid[c][r];
            // スタートの矩形の色
            if (t === 'start') {
                fill(255, 255, 0);
                // 道の矩形の色
            }
            else if (t === 'toRight' || t === 'toLeft' || t === 'toUp' || t === 'turnToRight' || t === 'turnToLeft') {
                fill(250);
                // 案内板のある矩形の色
            }
            else if (t === 'cross1' || t === 'cross2') {
                fill(195, 157, 81);
                // そのほか
            }
            else {
                fill(208, 227, 187);
            }
            // rect()で矩形の格子模様を描く
            rect(offsetX + r * (gutter + w), offsetY + c * (gutter + h), w, h);
        }
    }
}

// Carクラス
class Car {
    constructor(gridA, gridB, imgs) {
            this.gridA = gridA; // grid配列の地番に相当する
            this.gridB = gridB;
            // grid配列の地番から描画できるように数値を調整
            this.x = this.gridB * w + offsetX + w / 2;
            this.y = this.gridA * h + offsetY + h / 2;
            // イメージ変更にはthis.images配列を使用する
            this.images = imgs;
            this.image = this.images[3];
        }
        // 描画
    display() {
            // grid配列の地番から描画できるように数値を調整
            this.x = this.gridB * w + offsetX + w / 2;
            this.y = this.gridA * h + offsetY + h / 2;
            image(this.image, this.x, this.y);
        }
        // 今いる位置を調べ、移動する
    checkAndMove() {
        // 今いる場所の地番の値を調べる
        const currentGrid = grid[this.gridA][this.gridB];
        // キャンバス右横の<ol>要素に子<li>要素を追加し、移動先を表示
        const li = createElement('li', currentGrid);
        li.parent(list);
        /// 値によって向きを変える
        // startなら上に行くしかない
        if (currentGrid === 'start') {
            this.goUp();
            // cross1なら左右の選択肢がある
        }
        else if (currentGrid === 'cross1') {
            // 案内の道順の1つめにしたがって向きを変える
            const direction = directions[0];
            // 案内が右なら、
            if (direction === 'right') {
                // 右へ進む
                this.goRight();
                // 案内が左なら
            }
            else if (direction === 'left') {
                // 左に進む
                this.goLeft();
            }
            // toRightなら右に進むしかない
        }
        else if (currentGrid === 'toRight') {
            this.goRight();
            // turnToLeftなら左折するしかない
        }
        else if (currentGrid === 'turnToLeft') {
            this.goUp(); // 左折は上に進むこと
            // toUpなら上に進むしかない
        }
        else if (currentGrid === 'toUp') {
            this.goUp();
            // cross2なら左右の選択肢がある
        }
        else if (currentGrid === 'cross2') {
            // 案内の道順の2つめにしたがって向きを変える
            const direction = directions[1];
            if (direction === 'right') {
                this.goRight();
            }
            else if (direction === 'left') {
                this.goLeft();
            }
            // toLeftなら左に進むしかない
        }
        else if (currentGrid === 'toLeft') {
            this.goLeft();
            // turnToRightなら右折するしかない
        }
        else if (currentGrid === 'turnToRight') {
            this.goUp(); // 右折は上に進むこと
            // 4つの目的地のどれかに着いたら終わり
        }
        else if (currentGrid === 'ballpark' || currentGrid === 'pool' || currentGrid === 'gym' || currentGrid === 'tenniscourt') {
            const li = createElement('li', '到着');
            li.parent(list);
            // タイマーを切って呼び出しを止める
            window.clearInterval(timerID);
        }
    }

    // 上に進む
    goUp() {
            // grid配列の地番に相当する値を操作
            this.gridA = this.gridA - 1;
            this.image = this.images[3];
        }
        // 右に進む
    goRight() {
            this.gridB = this.gridB + 1;
            this.image = this.images[0];
        }
        // 左に進む
    goLeft() {
        this.gridB = this.gridB - 1;
        this.image = this.images[2];
    }
}

下図をクリックすると、実行画面が開きます。

コメントを残す

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

CAPTCHA