CHROMA

世の中の "当たり前" を確認する

物体を円周や進行方向に従って動かす

f:id:thleap:20141224171843p:plain

JavaScript Advent Calendar 2014 の24日目。

このデモでは、マウスを押している間は障害物を中心とした円周上を操作対象が回り、マウスを押していない間はその時の進行方向に向けて操作対象が直線的に移動する。

One More Line というゲームの断片を JavaScript で作ろうとしたが、そう呼ぶにはこれはあまりに乏しい。

とは言っても、数学的な知識が中学(もしくはそれ以前)で止まってしまっている僕にとってはベクトルや三角関数といったものは脳を熱くさせるのに十分なレベルだったし、周りに助けてもらわなければここまでは到底作れなかっただろう。

プログラミングは言語の書き方と、筋道を立ててモノを作る方法を理解しないといけないということを知った。特に動きのあるモノを作るのは難しい... 。

コードの解説

まず、{x: 0, y: 0} みたいなオブジェクトを二次元のベクトルとして扱うため、便利な関数を以下のように用意する。

function vector2(x, y) {
  return {
    x: x,
    y: y
  };
}

function addVector2(a, b) {
  return {
    x: a.x + b.x,
    y: a.y + b.y
  };
}

function subVector2(a, b) {
  return {
    x: a.x - b.x,
    y: a.y - b.y
  };
}

function magnitudeVector2(a) {
  return Math.sqrt(a.x * a.x + a.y * a.y);
}

function normalizeVector2(a) {
  var m = magnitudeVector2(a);

  return {
    x: a.x / m,
    y: a.y / m
  };
}

circle (白線の円)を操作対象、target (赤線の円)を操作対象がつかむ障害物としてそれぞれ変数を用意しておく。変数 tapping はマウスの押し離しの結果を判定するのに用いる。

start 関数内で Canvas の描画を宣言し、操作対象の開始位置、マウスの押し離しのイベント、繰り返し処理を定義しておく。stop 関数では繰り返しを止める処理を定義しておく。

onUpdate 関数

さて、ここからがこのコードのメインになるが、繰り返し処理の際に使う onUpdate 関数について解説していこう。

この関数では大きく 2つのことをしている。

  • 角度や進行方向の計算、条件分岐による処理の設定
  • Canvas への描画

重要なのは 1つ目のもので、Canvas を使った描画はほとんど基礎的なことしか行っていない。

角度や進行方向の計算、条件分岐による処理の設定

ここでは circletarget を中心とした円周上に入るときの角度の計算や、circle の進行方向の計算を行う。また、if (tapping) から始まる条件文でマウスを押している間と離してる間に行う処理を定義する。一つ一つ見ていこう。

以下の変数は、後で target を中心とした円周を求める際に使用する。selfToTargettarget から circle までのベクトルを求め、これを使い変数 r で円の半径を算出する。

var selfToTarget = subVector2(target, circle);
var r = magnitudeVector2(selfToTarget);

変数 radianAngle では Math.atan メソッドを使って target の円周に対する selfToTargetラジアン値を求める。今後の計算を度数で行うため、変数 angleラジアン値を度数に変換する。

var radianAngle = Math.atan(selfToTarget.y / selfToTarget.x);
var angle = radianAngle * (180 / Math.PI);

最後の変数 newPosition は繰り返し処理の際、circle が次に移動する位置を示す。

そして、これらの変数を用意した後は、target の円周上で circle を上手く回すために次の 2つの 条件文を用意しておく。

Math.atan はなす角しか返さないので、変数 angleJavaScript の座標系で円周の -90度から 90度の範囲しか返さない。円の残り半分の角度も取るために、circle の X座標が target の X座標よりも左に来たときに角度を 180度加算する。これにより angle は -91度から -180度、91度から 180度も返すようにする。

if (circle.x < target.x) {
  angle += 180;
}

あと、angle がマイナスの値を返すと円周 0 から 360度の角度は取れないので、この際には angle に 360度を足し合わせる。

if (angle < 0) {
  angle += 360;
}

if (tapping) から始まる条件文ではマウスの押している間、離している間の circle の位置移動を定義する。

マウスを押している間は target を中心とした円周上に newPosition を取るようにし、この処理が呼び出される度に円周上の角度を 1度足していく。

マウスから指が離れている間は circle の進行方向に従って直線的に進んでいくようにする。

if (tapping) {
  var newAngle = (angle + 1) % 360;
  newPosition = addVector2(target, {
    x: r * Math.cos(newAngle * Math.PI / 180),
    y: r * Math.sin(newAngle * Math.PI / 180)
  });
} else {
  newPosition = addVector2(circle, {
    x: circleDir.x * circleVelocity,
    y: circleDir.y * circleVelocity
  });
}

Canvas による描画処理に入る前に、circle の進行方向( circleDir )と座標を上記のコードで得られた値に書き換える。

circleDir = normalizeVector2(subVector2(newPosition, circle));
circle = newPosition;

onUpdate 関数の最後で、circle の Y座標が 0 を越えたときに繰り返し処理を止める。

if (circle.y <= 0) {
  stop();
}

Canvas への描画

一つ前の描画が Canvas 上に残らないように、clearRectonUpdate 関数が呼び出される度に Canvas を一度白紙に戻してる。

他は特に解説するようなことが無く、Canvas に用意されているメソッドとプロパティを使って円や線の描画を行っている。

参考