プログラミング はじめの一歩 JavaScript + p5.js編
21:指定歩数分だけ進む

この記事の詳しい内容には四国新聞 WEB朝刊の「「歩く数」の分だけ進む」ページから有料記事に進むことで読めます。

概要

このロボットは、指定された歩数だけ歩いて右に進みます。たとえば次の命令の場合、

  1. 歩く数2
  2. 進む (歩く数)
  3. 歩く数1
  4. 進む (歩く数)

ロボットは、(1)の歩数2を受け取り、自分の歩行機能を使って2歩歩いた後、(3)の歩数1を受け取って1歩歩きます。ロボットは合計で3歩歩いたことになります。

ここで問題です。次の命令を受けたロボットはどのマスに止まるでしょうか?

  1. 歩く数2
  2. 進む (歩く数)
  3. 歩く数5
  4. 進む (歩く数)
論理を考える

まず、ロボットの身になってみます。ロボットには歩数2が与えられます。これは、ロボットが歩数を受け取る入口を持っているということです。ロボットはその後、歩数分歩きます。これはロボットが、伝えられた歩数分だけ自分で歩く能力を持っているということです。

ロボットのクラス

ロボットはロボットのクラスで作成できます。ロボットが自分で歩く能力はそのクラスの歩くメソッドで作成できます。歩数を受け取る入口はそのメソッドのパラメータで作成できます。

class ロボット {
    ...
    歩く(歩数) {
        ...
    }
}
歩くwalk()メソッド

ロボットは画面の1マスを1歩で歩くので、受け取った歩数は進むマスの数でもあります。walk(5)なら5マス進むわけです。では歩数分だけ進むとは、どういうことでしょうか。それは、今いるマスの位置(地番)に歩数を加えるということです。

今いるマスの位置に歩数を加えるには、ロボットは今いるマスの位置を知っている必要があります。また歩き命令は何回も受ける可能性があるので、そのたびに自分の新しい位置をメモしておく必要があります。

この自分が今いるマスの位置は、クラスのプロパティで追跡できます。次のWalkingRobotクラスはその例で、自分の位置をgridXプロパティで設定しています。スタートするマスの位置を0とすると、gridXの最初の値は0です。walk()メソッドに歩数を表すnumパラメータを設定すると、ロボットがnumマス歩いた後の位置は、this.gridX = this.gridX + num、つまりthis.gridX += num で計算できます。

class WalkingRobot {
    constructor() {
            // gridXは今いるマスの地番(0)
            this.gridX = 0;
        }
        // 与えられたnum歩分だけ進む
    walk(num) {
        // 今いるマスの地番にnumを足して、今いるマスの地番を更新
        this.gridX += num;
    }
}

WalkingRobotクラスのインスタンスは次のように作成します。walk()メソッドを呼び出すときには歩数を引数として渡します。

function setup() {
    noCanvas();
    // WalkingRobotクラスのインスタンスを作成
    const robot = new WalkingRobot();
    // 2歩歩く
    robot.walk(2);
    // 1歩歩く
    robot.walk(1);
    // ロボットが今いるマスの地番
    // => 2歩進んで、そこからさらに1歩進んだということ
    print(robot.gridX);
}

ここまでのコードを実行すると、[コンソール]に3が表示されます。これはロボットが今いる地番です。ロボットは最初0の地番(this.gridX = 0)にいたので、3歩歩いたことになります。

歩数を変えてrobot.walk(2)とrobot.walk(5)を実行すると、[コンソール]に7が表示されます。これが問題の答えです。

このお題を解決するための基本的な論理は以上です。

可視化

論理が完成したら、キャンバスの描画に移ることができます。

マスの並びの描画

まずは、ロボットが歩くマスを作成しましょう。マスの並びは「格子ブロックのひな型コード」で作成できます。

ただし作成したい格子模様は1 x 10なので、rowsのforループは事実上要りません。またgutterも0なので無用です。後でロボットが移動するアニメーションを作成するので、マスはdraw()関数内で描画します。

//const rows = 1; // 横向きの行数
const columns = 10; // 縦向きの列数
//const gutter = 0; // 矩形間の空き
const w = 30; // 矩形の幅
const h = 30; // 矩形の高さ
// 矩形の開始位置をずらす量 => (offsetX,offsetY)から始まる
const offsetX = 50;
const offsetY = 100;

function draw() {
    background(220);
    for (let c = 0; c < columns; c++) {
        rect(offsetX + c * w, offsetY + 1 * h, w, h);
    }
}

下図はこの実行例です。

ロボットの描画

ロボットの画像には、下図のものを使用します。この2つのファイルのイメージを交互に描画してアニメーションにします。

ロボットのイメージはrobotImages配列に入れます。

let robot, robotImages;

function preload() {
    const walk0 = loadImage('images/walk0.png');
    const walk1 = loadImage('images/walk1.png');
    robotImages = [walk0, walk1];
}

イメージの入れ替え
ロボットのイメージを読み込んだので、ロボットのWalkingRobotクラスに手を加えていきます。ロボットは自分のgridXプロパティの値にしたがって描画位置を右に移動させますが、同時に自分のイメージも入れ替えます。まずはその入れ替え方法を見ていきましょう。

コンストラクタは次のように変更します。

class WalkingRobot {
    constructor(x, y, imgs) {
        this.gridX = x; // 今いる地番
        this.gridY = y;
        // キャンバスの位置設定に応じた描画位置
        this.x = this.gridX * w + offsetX + w / 2;
        this.y = this.gridY * w + offsetY + w / 3;
        // 現在のフレーム番号(ロボットは2フレームで構成される)
        this.currentFrame = 0;
        // ロボットのイメージ
        this.images = imgs;
        this.image = this.images[1];
    }

パラメータのx,yはマスの地番で、imgsはロボットのイメージを入れた配列です。currentFrameプロパティはその時点でのフレーム番号です。これは、下図のように見なすことができます。

imagesプロパティが参照するロボットのイメージのrobotImages配列は、2コマ(フレーム)だけのフィルムの切れ端のようなものです。currentFrameを0,1,0,1,…と循環して変化させると、robotImages[currentFrame]は、上図のフレーム0、フレーム1を交互に参照します。これをアニメーションに利用します。

描画を実行するのはdisplay()メソッドです。currentFrameは0,1,0,1,…と循環して変化するので、ロボットのイメージは、display()メソッドが呼び出されるたびに、images[0]とimages[1]の間で切り替わることになります。

// 描画
display() {
    // 現在のイメージは、現在のフレーム番号に対応するimages配列のイメージ
    this.image = this.images[this.currentFrame];
    image(this.image, this.x, this.y);
}

次のanime()メソッドは、ロボットのイメージの切り替えを説明するための、実際には使わないメソッドです。ここでは呼び出されるたびにcurrentFrameを1ずつ大きくしています。currentFrameは0,1,2,3…と大きくなりますが、ここで行いたいのは、0,1,0,1,…という循環する変化なので、currentFrameがイメージの数を超えたら0に戻しています。この単純な方法が、ロボットのフレームが交互に切り替える原動力になっています。

anime() {
    // 現在のフレームも1つ大きくする
    this.currentFrame++;
    // しかしロボットは2フレームしか持たないので、制限する必要がある
    // 現在のフレーム番号がロボットのイメージ数を超えたら最初に戻る
    if (this.currentFrame >= this.images.length) {
        this.currentFrame = 0;
    }
}

* 0,1,0,1,…の循環は「5:暗号をといて箱を開ける」の「数学的な解決方法」で述べている、%演算子を使った方法でも作成できます。

ではテストで、このanime()メソッドを実行してみましょう。WalkingRobotクラスに地番とロボットのイメージを渡してインスタンスを作成し、setInterval()でロボットのanime()メソッドを呼び出すと、ロボットはその場で足の閉じ開きを繰り返します。

function setup() {
    createCanvas(150, 150);
    // イメージを描画をそのセンターから行う
    imageMode(CENTER);
    // WalkingRobotクラスのインスタンスを作成
    robot = new WalkingRobot(0, -1, robotImages);
    window.setInterval(() => {
        robot.anime();
    }, 1000);
}

下はこの実行画面です。anime()メソッドはsetInterval()関数によって1秒に1回呼び出されているので、ロボットの足の閉じ開きも同じ頻度で行われます。

7歩あるくロボットのコード

では、ここまでのまとめとして、画面のマウスプレスで7歩歩くロボットのコードを見てみましょう。

const columns = 10; // 縦向きの列数
const w = 30; // 矩形の幅
const h = 30; // 矩形の高さ
// 矩形の開始位置をずらす量
const offsetX = 50;
const offsetY = 100;

let robot, robotImages;

function preload() {
    const walk0 = loadImage('images/walk0.png');
    const walk1 = loadImage('images/walk1.png');
    robotImages = [walk0, walk1];
}

function setup() {
    createCanvas(400, 300);
    // イメージを描画をそのセンターから行う
    imageMode(CENTER);
    // WalkingRobotクラスのインスタンスを作成
    robot = new WalkingRobot(0, 0, robotImages);
}

WalkingRobotクラスのコンストラクタには、0,0を渡します。これはロボットのスタート位置です。

function draw() {
    background(220);
    // マスの並びを描画
    for (let c = 0; c < columns; c++) {
        rect(offsetX + c * w, offsetY + 1 * h, w, h);
    }
    // ロボットを描画
    robot.display();
}

// 画面のマウスプレスで歩きをスタート
function mousePressed() {
    const timerID = window.setInterval(() => {
        // 7マス歩く
        const res = robot.walk(7);
        // 歩きが終わったら'end'が返される
        if (res === 'end') {
            // タイマーをクリア
            window.clearInterval(timerID);
        }
    }, 1000);
}

ロボットのwalk()メソッドには7を渡します。ロボットはこの命令によって7歩歩きます。

// 歩くロボットのクラス
class WalkingRobot {
    constructor(x, y, imgs) {
            this.gridX = x;
            this.gridY = y;
            // メインのsketch.jsの変数に依存している
            this.x = this.gridX * w + offsetX + w / 2;
            this.y = this.gridY * h + offsetY + h / 3;
            // 現在のフレーム番号(ロボットは2フレームで構成される)
            this.currentFrame = 0;
            // 歩いた歩数
            this.steps = 0;
            this.images = imgs;
            this.image = this.images[2];
        }
        // 描画
    display() {
            // メインのsketch.jsの変数に依存している
            this.x = this.gridX * w + offsetX + w / 2;
            // イメージを現在のフレーム数から描画
            this.image = this.images[this.currentFrame];
            image(this.image, this.x, this.y);
        }
        // 指定歩数だけ1歩ずつ歩く
    walk(num) {
        // 歩数が指定歩数に達していない間は
        if (this.steps < num) {
            // 歩数をカウントアップ
            this.steps++;
            // 地番を1つ右に移す
            this.gridX++;
            // 現在のフレームも1つ大きくする
            this.currentFrame++;
            // しかしロボットは2フレームしか持たないので、制限する必要がある
            // 現在のフレーム数がロボットのイメージ数を超えたら最初に戻る
            if (this.currentFrame >= this.images.length) {
                this.currentFrame = 0;
            }
            // 歩数が指定歩数に達した終了
        }
        else {
            this.currentFrame = 0;
            return 'end';
        }
    }
}

ロボットがマスの地番に描画されるのは、ロボットのxとyプロパティを次のコードで調整しているからです。wもhもメインのsketch.jsの変数なので、このWalkingRobotクラスはsketch.jsに依存していると言えます。

this.x = this.gridX * w + offsetX + w / 2;
this.y = this.gridY * h + offsetY + h / 3;

新しいstepsプロパティはロボットが歩いた歩数を数えるプロパティです。これが、walk()メソッドで指定された歩数を超えたら、その歩数を歩いたということなので、呼び出し元に終わりを意味する’end’を返しています。これにより、呼び出し元では、タイマーをクリアすることができます。

下図をクリックすると、実行画面が開きます。画面をクリックすると、ロボットが7歩歩きます。

2歩歩いて、5歩歩くロボット

最後に、お題の「2歩歩いて、5歩歩くロボット」を見ておきましょう。

WalkingRobotクラスのロボットは、walk()メソッド1回の呼び出しで1歩だけ歩きます。ロボットがつづけて歩くのは、タイマーを使って繰り返しwalk()メソッドを呼び出しているからです。したがって、ロボットを2歩歩かせ、その後5歩歩かせるには、タイマーを2つ使います。

次の例では、1つめのタイマーで2歩歩かせ、その歩きが終わったタイミングで1つめのタイマーをクリアし、2つめのタイマーを作成して、5歩歩かせています。

const columns = 10;
const w = 30;
const h = 30;
const offsetX = 50;
const offsetY = 100;

let robot, robotImages;

// 歩数
const mStep = 2;
const nStep = 5;

function preload() {
    const walk0 = loadImage('images/walk0.png');
    const walk1 = loadImage('images/walk1.png');
    robotImages = [walk0, walk1];
}

function setup() {
    createCanvas(400, 300);
    imageMode(CENTER);
    robot = new WalkingRobot(0, 0, robotImages);
}

// 画面のマウスプレスで歩きをスタート
function mousePressed() {
    // タイマーを2つ使う
    timerID = window.setInterval(() => {
        // ロボットをmStep歩かせる
        let res = robot.walk(mStep);
        // mStep歩の歩きが終わったら
        if (res === 'end') {
            window.clearInterval(timerID);
            // 2つめのタイマーを作成して
            timerID = window.setInterval(() => {
                // ロボットをnStep歩歩かせる
                res = robot.walk(nStep);
                // nStep歩の歩きが終わった
                if (res === 'end') {
                    window.clearInterval(timerID);
                }
            }, 1000);
        }
    }, 1000)
}

function draw() {
    background(220);
    for (let c = 0; c < columns; c++) {
        rect(offsetX + c * w, offsetY + 1 * h, w, h);
    }
    robot.display();
}

// ロボットのクラス
class WalkingRobot {
    constructor(x, y, imgs) {
        this.gridX = x;
        this.gridY = y;
        this.x = this.gridX * w + offsetX + w / 2;
        this.y = this.gridY * h + offsetY + h / 3;

        this.currentFrame = 0;
        this.steps = 0;
        this.images = imgs;
        this.image = this.images[2];
    }
    display() {
        this.x = this.gridX * w + offsetX + w / 2;
        this.image = this.images[this.currentFrame];
        image(this.image, this.x, this.y);
    }

    walk(num) {
        if (this.steps < num) {
            this.steps++;
            this.gridX++;
            this.currentFrame++;
            if (this.currentFrame >= this.images.length) {
                this.currentFrame = 0;
            }
        }
        else {
            this.currentFrame = 0;
            // 1回の歩きが終わったので、歩数を0に戻し、次回の呼び出しに備える
            this.steps = 0;
            return 'end';
        }
    }
}

walk()メソッドでは、1回の歩きが終わったタイミングで、stepsプロパティを0に戻しておく必要があります。

このロボットが2歩歩いて5歩歩くことと、7歩歩くことの違いは、ロボットが2歩歩いた後、少し間を空けることです。2歩歩いた後、新しいタイマーを作成しているので、最初の遅延がこの間となっています。下図をクリックすると、実行画面が開きます。画面をクリックすると、ロボットが2歩歩いて5歩歩きます。

コメントを残す

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

CAPTCHA