Chapter 62

ステアリング

miku
miku
2021.11.17に更新

以前の章ではオブジェクトを近づけたり、逆に離したりすることについて扱ったが、この章ではより説得力のある形で、追いかけたり逃げたりするような動きを作りたい。

大まかなコードの流れは下記のとおりになる。

  1. 追いかける/逃げるなどの各動作に対応する力を与える
  2. 力の合計を加速度として設定
  3. 設定した加速度が上限を超えていたら上限に合わせる。
  4. 加速度をもとに速度を更新
  5. 設定した速度が上限を超えていたら上限に合わせる。
  6. 速度をもとに位置を更新
  7. 加速度を初期化する

基本的な動作は「56章 - 運動」で扱ったことと同じになるが、今までと違うのは速度と加速度の上限を決めることである。というのも動物や車などは走れる最大の速度はだいだい決まっており、速度がある状態で急に曲がることができない。ここで速度と加速度に上限を設定するとよりリアルな動きに近づけることができる。

ステアリングの基本

let vehicle;

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

  const position = createVector(100, 100);
  const velocity = createVector();
  const maxSpeed = 8;
  const maxForce = 0.1;
  vehicle = Vehicle.create(position, velocity, maxSpeed, maxForce);
}

function draw() {
  clear();
  Vehicle.run(vehicle);
  Vehicle.update(vehicle);
  Vehicle.draw(vehicle, "#aaa");
}

const Vehicle = {
  create(position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  },

  update(v) {
    v.acceleration.limit(v.maxForce);
    v.velocity.add(v.acceleration);
    v.velocity.limit(v.maxSpeed);
    v.position.add(v.velocity);
    v.acceleration.set(0);

    Vehicle.adjustEdge(v);
  },

  adjustEdge(v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  },

  draw(v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  },

  run(v) {
    v.acceleration.set(10, 10);
  },
};

コードが長いので一つ一つ解説をしていく。

const Vehicle = {
  create(position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  }
};

Vehicle は画面上を動くオブジェクトを作るためのもので create() でオブジェクトを生成する。引数にはベクトルである位置と速度、実数である速度の上限、加速度(力の合計)の上限を指定する。

const Vehicle = {
  run(v) {
    v.acceleration.set(10, 10);
  }
};

追いかけたり逃げたりする力の与え方は後で扱うので、今は適当に加速度を与える関数を作りたい。Vehicle.run() を呼び出すと適当な力を加速度に設定する。

const Vehicle = {
  update(v) {
    v.acceleration.limit(v.maxForce); // 加速度が上限を超えないようにする
    v.velocity.add(v.acceleration); // 加速度から速度を更新
    v.velocity.limit(v.maxSpeed); // 速度が上限を超えないようにする
    v.position.add(v.velocity); // 速度から位置を更新
    v.acceleration.set(0); // 加速度を0にする

    Vehicle.adjustEdge(v); // 境界処理(後述)
  }
};

Vehicle.run() のような、力を与える関数を呼び出した後、この Vehicle.update() を呼び出して、加速度から速度の更新、速度から位置の更新を行う。前述したとおり、速度と加速度は上限を設定すると説得力のある動きになるので、Vehicle.create() で指定した速度の上限と加速度の上限を超えないようにする。

const Vehicle = {
  adjustEdge(v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  }
};

Vehicle.adjustEdge() は位置が画面外に出たら画面内に戻し、速度を反転させる。

const Vehicle = {
  draw(v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  }
};

Vehicle.draw() はオブジェクトの描画を行う。やや縦長の三角形にしているが、ベクトルを矢印で可視化するように速度の向きがわかれば描画は何でもいい。

let vehicle;

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

  const position = createVector(100, 100);
  const velocity = createVector();
  const maxSpeed = 8;
  const maxForce = 0.1;
  vehicle = Vehicle.create(position, velocity, maxSpeed, maxForce);
}

function draw() {
  clear();
  Vehicle.run(vehicle);
  Vehicle.update(vehicle);
  Vehicle.draw(vehicle, "#aaa");
}

Vechileオブジェクトの作成と更新手順。

追跡行動

let vehicle, target;

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

  const position = createVector(100, 100);
  const velocity = createVector();
  const maxSpeed = 8;
  const maxForce = 0.1;
  vehicle = Vehicle.create(position, velocity, maxSpeed, maxForce);
  target = createVector(width / 2, height / 2);
}

function draw() {
  clear();

  target.x += 2;
  target.x %= width;

  push();
  noFill();
  stroke("#ff9900");
  circle(target.x, target.y, 20);
  pop();

  Vehicle.seek(vehicle, target);
  Vehicle.update(vehicle);
  Vehicle.draw(vehicle, "#aaa");
}

const Vehicle = {
  create: function (position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  },

  update: function (v) {
    v.acceleration.limit(v.maxForce);
    v.velocity.add(v.acceleration);
    v.velocity.limit(v.maxSpeed);
    v.position.add(v.velocity);
    v.acceleration.set(0);

    Vehicle.adjustEdge(v);
  },

  adjustEdge: function (v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  },

  draw: function (v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  },

  seek: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.add(force);
  },
};

Vehicle.seek(v, target) はVehicleオブジェクト vtarget ベクトルを追いかける力を与える。

まず v から target へのベクトルを求めるため、target から v を引いたものを tv とする。その tv の速度上限を設定した後、tv から v の速度を引いたものを力とする。

tv は(速度制限をした)自身からターゲットまでのベクトルなので、このまま力にしてもいいのではないかと思われるかもしれないが、たとえば上記画像のように、現在 v が上の方向に進んでいて、右の方向にターゲットがいる場合、tv をそのまま力にすると右上が目標になってしまう。なので、目標の速度ベクトルから現在の速度ベクトルを引いたものを力にする必要がある。

逃避行動

let vehicle, target;

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

  {
    const position = createVector(100, 0);
    const velocity = createVector();
    const maxSpeed = 8;
    const maxForce = 0.1;
    seeker = Vehicle.create(position, velocity, maxSpeed, maxForce);
  }

  {
    const position = createVector(200, 220);
    const velocity = createVector();
    const maxSpeed = 8;
    const maxForce = 0.1;
    fleer = Vehicle.create(position, velocity, maxSpeed, maxForce);
  }
}

function draw() {
  clear();

  Vehicle.seek(seeker, fleer.position);
  Vehicle.update(seeker);
  Vehicle.draw(seeker, "#aaa");

  Vehicle.flee(fleer, seeker.position);
  Vehicle.update(fleer);
  Vehicle.draw(fleer, "#aaa");
}

const Vehicle = {
  create: function (position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  },

  update: function (v) {
    v.acceleration.limit(v.maxForce);
    v.velocity.add(v.acceleration);
    v.velocity.limit(v.maxSpeed);
    v.position.add(v.velocity);
    v.acceleration.set(0);

    Vehicle.adjustEdge(v);
  },

  adjustEdge: function (v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  },

  draw: function (v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  },

  seek: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.add(force);
  },

  flee: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.sub(force);
  },
};

Vehicle.flee(v, target) は Vehicleオブジェクト vtarget ベクトルから逃げようとする力を与える。

seek() で計算した forcetarget に最短で近づこうとする力なので、この力の正反対のベクトルがターゲットからもっと離れようとする力になる。なので flee() が行う計算は、力を求める計算式までは seek() と同様で、最後に求めた力を現在の加速度から引けばいい。

逃避行動(50匹ver)

let vehicles;

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

  vehicles = [];
  for (let i = 0; i < 50; i++) {
    const position = createVector(random(width), random(height));
    const velocity = createVector(random(width), random(height));
    const maxSpeed = 8;
    const maxForce = 0.4;
    const vehicle = Vehicle.create(position, velocity, maxSpeed, maxForce);
    vehicles.push(vehicle);
  }
}

function draw() {
  clear();

  vehicles.forEach((vehicle, index) => {
    const prev = index === 0 ? vehicles.length - 1 : index - 1;
    const next = (index + 1) % vehicles.length;
    Vehicle.flee(vehicle, vehicles[prev].position);
    Vehicle.seek(vehicle, vehicles[next].position);
    Vehicle.update(vehicle);
    Vehicle.draw(vehicle, "#aaa");
  });
}

const Vehicle = {
  create: function (position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  },

  update: function (v) {
    v.acceleration.limit(v.maxForce);
    v.velocity.add(v.acceleration);
    v.velocity.limit(v.maxSpeed);
    v.position.add(v.velocity);
    v.acceleration.set(0);

    Vehicle.adjustEdge(v);
  },

  adjustEdge: function (v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  },

  draw: function (v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  },

  seek: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.add(force);
  },

  flee: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.sub(force);
  },
};

あるVehicleオブジェクトは別のVehicleオブジェクトを追いかけて、自身もまた別のVehicleオブジェクトに追いかけられている作例。