物体を円周や進行方向に従って動かす
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 を使った描画はほとんど基礎的なことしか行っていない。
角度や進行方向の計算、条件分岐による処理の設定
ここでは circle
が target
を中心とした円周上に入るときの角度の計算や、circle
の進行方向の計算を行う。また、if (tapping)
から始まる条件文でマウスを押している間と離してる間に行う処理を定義する。一つ一つ見ていこう。
以下の変数は、後で target
を中心とした円周を求める際に使用する。selfToTarget
で target
から 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
はなす角しか返さないので、変数 angle
は JavaScript の座標系で円周の -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 上に残らないように、clearRect
で onUpdate
関数が呼び出される度に Canvas を一度白紙に戻してる。
他は特に解説するようなことが無く、Canvas に用意されているメソッドとプロパティを使って円や線の描画を行っている。