Chapter 21

運動の基本・境界処理

miku
miku
2021.11.12に更新

この章では、オブジェクトを動かす方法について扱う。例えば上下左右や、任意の角度の方向に直線的に動かすことについて。それに加えてオブジェクトを動かしていくと、やがて画面から出ていってしまうのでその時の対処方法である境界処理についても扱う。

円を左右に動かす

const vx = 1;
let x, y;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = 0;
  y = height / 2;
}

function draw() {
  clear();
  circle(x, y, 40);

  x += vx;
}

円を左右に動かすには、円のx座標に 0 以外の値を足し合わせばいい。足し合わせる変数 vx を用意して vx > 0 なら x が増え続けるので、円は右に移動し続ける。逆に vx < 0 なら x が減り続けるので、円は左に移動し続けることになる。

このような位置に足し合わせる値のことを速度と呼ぶ。速度については後の章で具体的に扱う。

円を上下に動かす

let x, y;
const vy = 1;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = width / 2;
  y = 0;
}

function draw() {
  clear();
  circle(x, y, 40);

  y += vy;
}

上下に動かす場合は、y に速度を足しあわせればいい。y に足し合わせる速度を vy だとすると、vy < 0 なら上に、vy > 0 なら下に移動することになる。

円を上下左右に動かす

let x, y;
const vx = 2;
const vy = 1;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = 0;
  y = 0;
}

function draw() {
  clear();
  circle(x, y, 40);

  x += vx;
  y += vy;
}

先程の作例2つを組み合わせて、毎フレーム、座標 (x, y) に速度 (vx, vy) を足し合わせて移動させる例。(vx, vy) が両方とも 0 でなければオブジェクトが移動する。(vx, vy)(0, -2) のように一方だけが 0 なら上下左右のうちの一方向に移動する。(vx, vy)(1, -2) のように両方とも 0 でないならば、斜めに移動することになる。

3時の方向に動かすなら (1, 0)、6時の方向なら (0, 1) のように90度単位の移動は手作業でなんとか指定ができるが、123.45度の方向のように任意の角度に対応した (vx, vy) を計算するには三角関数を利用する必要がある。

任意の方向にオブジェクトを移動する

let x, y, vx, vy;
const angle = 30; // 度

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES); // 度数法で扱う形式に変える

  x = width / 2;
  y = height / 2;

  vx = cos(angle);
  vy = sin(angle);
}

function draw() {
  clear();
  circle(x, y, 40);

  x += vx;
  y += vy;
}

angle 方向に 1 進むための (x, y) の変化の量は cos(angle), sin(angle) で取得できるので、それを速度にすれば、毎フレーム angle 方向に 1 進む円を描画することができる。

任意の方向にオブジェクトを移動する その2

let x, y, vx, vy;
const angle = 30; // 度
const speed = 2;

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);

  x = width / 2;
  y = height / 2;

  vx = cos(angle) * speed;
  vy = sin(angle) * speed;
}

function draw() {
  clear();
  circle(x, y, 40);

  x += vx;
  y += vy;
}

angle 方向に 1 ではなく speed だけ進めたい場合は cos(angle) * speed, sin(angle) * speed を速度にする。

境界処理

ここまでオブジェクトを運動させる方法について扱ったが、時間が経つとやがて画面からオブジェクトが消えてしまう。ここで画面から消えるときにまた画面に戻す方法について幾つか解説を行う。

ループする

画面からオブジェクトが消えたら反対方向からまたオブジェクトを出現させてループさせる方法。

let x, y;
let r;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = width / 2;
  y = height / 2;
  r = 30;
}

function draw() {
  x += 2;
  y += 2;

  if (x + r < 0) {
    x = width - 1 - r;
  } else if (x - r >= width) {
    x = r;
  }

  if (y + r < 0) {
    y = height - 1 - r;
  } else if (y - r >= height) {
    y = r;
  }

  clear();
  circle(x, y, r * 2);
}
場所 判定 ループ先
左側 x < 0 width - 1
右側 x >= width 0
上側 y < 0 height - 1
下側 y >= height 0

画面内の領域は (0, 0) ~ (width - 1, height - 1) なので、画面外に出た判定とループ先は上記のようになる。

場所 判定 ループ先
左側 x - r < 0 width - 1 - r
右側 x + r >= width r
上側 y - r < 0 height - 1 - r
下側 y + r >= height r

円の大きさを考慮した場合は上記のとおり。

跳ね返る

let x, y, vx, vy, r;

function setup() {
  createCanvas(windowWidth, windowHeight);

  x = width / 2;
  y = height / 2;
  vx = 2;
  vy = 2;
  r = 30;
}

function draw() {
  x += vx;
  y += vy;

  if (x - r <= 0) {
    x = r;
    vx *= -1;
  } else if (x + r >= width - 1) {
    x = width - 1 - r;
    vx *= -1;
  }

  if (y - r < 0) {
    y = r;
    vy *= -1;
  } else if (y + r >= height - 1) {
    y = height - 1 - r;
    vy *= -1;
  }

  clear();
  circle(x, y, r * 2);
}

画面内の一番外側あるいは画面外にオブジェクトが出たときに、画面内に戻した後、速度を反転させてボールを跳ね返す方法。

ループの場合は円が完全に画面から出たらループをするという方法だったが、こちらは円の一部分が画面外または画面内の一番外側に来たら跳ね返るので条件が少し変わっている。たとえば画面左端の判定は、ループの場合は円の右側が画面内なら出たら、だったが、こちらは円の左側が0以下になったらに変わる。

跳ね返りを利用した作例

let a, b, c;

function setup() {
  createCanvas(windowWidth, windowHeight);
  noFill();

  a = Ball.create();
  b = Ball.create();

  c = { r: random(255), g: random(255), b: random(255) };
}

function draw() {
  Ball.update(a);
  Ball.update(b);

  c.r += random([-2, 2]);
  c.g += random([-2, 2]);
  c.b += random([-2, 2]);

  c.r = constrain(c.r, 0, 255);
  c.g = constrain(c.g, 0, 255);
  c.b = constrain(c.b, 0, 255);

  stroke(c.r, c.g, c.b, 100);
  line(a.x, a.y, b.x, b.y);
}

const Ball = {
  create: () => {
    const x = random(0, width);
    const y = random(0, height);
    const vx = random(-3, 3);
    const vy = random(-3, 3);

    return { x, y, vx, vy };
  },

  update: (ball) => {
    ball.x += ball.vx;
    ball.y += ball.vy;

    if (ball.x <= 0) {
      ball.x = 0;
      ball.vx *= -1;
    } else if (ball.x >= width - 1) {
      ball.x = width - 1;
      ball.vx *= -1;
    }

    if (ball.y <= 0) {
      ball.y = 0;
      ball.vy *= -1;
    } else if (ball.y >= height - 1) {
      ball.y = height - 1;
      ball.vy *= -1;
    }
  },
};

2つのオブジェクトを用意して、運動をさせ、画面端にきたら跳ね返るようにする。そしてオブジェクト同士を線で結ぶ作例。

ループ対応した二点間の距離

点ABの2点間の距離は赤線になるが、外側がループしているなら青線も2点間の距離になる。この条件の場合、赤線と青線の短い方を二点間の距離として採用する場合がある。

その場合、計算は軸ごとに行う。下記はx軸の場合について。

内側の赤線と外側の青線の長さの小さい方をx軸の長さとして採用する。赤線の長さの計算は A と B の差の絶対値を取る。

外側の青線は2本あるので別々に計算して総和を求めてもいいが、左側にある点に画面幅を足した位置を求め、そこから右側の点までの距離が青線の長さと同じになるので、そちらを計算してもいい。

// 2点のうちどちらが左でどちらが右になるかを計算
const minX = min(x0, x1); // 左
const maxX = max(x0, x1); // 右

// 内側(赤線)の距離と、外側(青線)の距離のうち、小さい方をx軸の長さとして採用。
const dx = min(maxX - minX, minX + width - maxX);

x軸の計算例。y軸も同様に計算し、求めた xy の長さで二点間の距離を求める。

function getDistance(x0, y0, x1, y1, w, h) {
  const minX = min(x0, x1);
  const maxX = max(x0, x1);
  const minY = min(y0, y1);
  const maxY = max(y0, y1);
  const dx = min(maxX - minX, minX + w - maxX);
  const dy = min(maxY - minY, minY + h - maxY);

  return dist(0, 0, dx, dy);
}

2点 (x0, y0), (x1, y1) と 画面の幅と高さ width, height を渡して、ループを考慮した2点間距離を返す関数。