基本の角度のついた跳ね返り
水平面や垂直面に対する跳ね返りは、単に速度を反転させればよく簡単だが(【JavaScript】ボールを壁で跳ね返させる)、反射面に角度がついている場合は難しい。そこで、ボールが反射面に当たった時、システムの座標を変換して反射面を水平にし、跳ね返り処理を行った後、システムの座標を元に戻す方法を採用する。
下のスクリプトはCanvasに反射面である線を引き、上から落としたボールを跳ね返らせている。
まず、ボールに見立てた円を描画するCircleクラス。
/**
* 真円を描画(circle.js)
*/
export class Circle {
constructor(parent = undefined, x = 0, y = 0, radius = 15) {
if (parent !== undefined) {
this._parent = parent;
}
this._x = x;
this._y = y;
this._radius = radius;
this._color = "#ff0000";
this._init();
}
//////////////////////////////////
// Private and protected
//////////////////////////////////
_init() {
if (this._parent !== undefined) {
this._ctx = this._parent;
}
this.draw();
}
//////////////////////////////////
// Public
//////////////////////////////////
draw() {
this._ctx.save();
this._ctx.fillStyle = this._color;
this._ctx.beginPath();
this._ctx.arc(this._x, this._y, this._radius, 0, (Math.PI * 2), true);
this._ctx.closePath();
this._ctx.fill();
this._ctx.restore();
}
//////////////////////////////////
// Getters/Setters
//////////////////////////////////
get x() {
return this._x;
}
set x(x) {
this._x = x;
}
get y() {
return this._y;
}
set y(y) {
this._y = y;
}
get radius() {
return this._radius;
}
set radius(radius) {
this._radius = radius;
}
set color(color) {
this._color = color;
this.draw();
}
}
次に、線を描画するLineクラス。
/**
* 反射面を描画(line.js)
* @param parent 親のコンテキスト
* @param x1 描き始めのX座標
* @param y1 描き始めのY座標
* @param x2 描き終わりのX座標
* @param y2 描き終わりのY座標
*/
export class Line {
constructor(parent = undefined, x1 = 0, y1 = 0, x2 = 0, y2 = 0) {
if (parent !== undefined) {
this._parent = parent;
}
this._x = 0;
this._y = 0;
this._x1 = x1;
this._y1 = y1;
this._x2 = x2;
this._y2 = y2;
this._rotation = 0;
this._lineWidth = 1;
this._init();
}
//////////////////////////////////
// Private and protected
//////////////////////////////////
_init() {
if (this._parent !== undefined) {
this._ctx = this._parent;
}
this.draw();
}
//////////////////////////////////
// Public
//////////////////////////////////
draw() {
this._ctx.save();
this._ctx.translate(this._x, this._y);
this._ctx.rotate(this._rotation);
this._ctx.lineWidth = this._lineWidth;
this._ctx.beginPath();
this._ctx.moveTo(this._x1, this._y1);
this._ctx.lineTo(this._x2, this._y2);
this._ctx.closePath();
this._ctx.stroke();
this._ctx.restore();
}
/**
* 線の描画領域(矩形)を返す
* @returns オブジェクトでx、y、幅、高さ
*/
getBounds() {
// 線の角度が0度(水平)なら実行
if (this._rotation === 0) {
let minX = Math.min(this._x1, this._x2);
let minY = Math.min(this._y1, this._y2);
let maxX = Math.max(this._x1, this._x2);
let maxY = Math.max(this._y1, this._y2);
return {
x: this._x + minX,
y: this._y + minY,
width: maxX - minX,
height: maxY - minY
}
// 線の角度が0以外なら実行
} else {
let sin = Math.sin(this._rotation);
let cos = Math.cos(this._rotation);
let x1r = cos * this._x1 + sin * this._y1;
let x2r = cos * this._x2 + sin * this._y2;
let y1r = cos * this._y1 + sin * this._x1;
let y2r = cos * this._y2 + sin * this._x2;
return {
x: this._x + Math.min(x1r, x2r),
y: this._y + Math.min(y1r, y2r),
width: Math.max(x1r, x2r) - Math.min(x1r, x2r),
height: Math.max(y1r, y2r) - Math.min(y1r, y2r)
}
}
}
//////////////////////////////////
// Getters/Setters
//////////////////////////////////
get x() {
return this._x;
}
set x(x) {
this._x = x;
}
get y() {
return this._y;
}
set y(y) {
this._y = y;
}
get rotation() {
return this._rotation;
}
set rotation(rotation) {
this._rotation = rotation;
}
}
最後に、角度のついた跳ね返りを処理するAngleBounceクラス。
import { Line } from "./line.js";
import { Circle } from "./circle.js";
/**
* 角度のついた跳ね返り運動(anglebounce.js)
*/
export class AngleBounce {
constructor() {
this._cvs = document.getElementById('canvas');
this._ctx = this._cvs.getContext('2d');
this._line;
this._circle;
this._gravity = 0.2;
this._bounce = -0.6;
this._vx = 0;
this._vy = 0;
// タイマー関連
this._animID;
this._isAnim = 0;
this._FPS = 60;
this._frame = 0;
this._startTime;
this._nowTime;
this._init();
}
//////////////////////////////////
// Private and protected
//////////////////////////////////
_init() {
this._cvs.style.backgroundColor = "#eeeeee";
// ①角度のついた反射面を描画
this._line = new Line(this._ctx, 0, 0, 250, 0);
this._line.x = 50;
this._line.y = 200;
// ②角度をラジアンに変換して渡す
this._line.rotation = 15 * Math.PI / 180;
this._circle = new Circle(this._ctx, 100, 100, 30);
this._startTime = performance.now();
this._mainLoop();
}
_mainLoop() {
this._nowTime = performance.now();
let elapsedTime = this._nowTime - this._startTime;
let idealTime = this._frame * (1000 / this._FPS);
if (idealTime < elapsedTime) {
this._ctx.clearRect(0, 0, this._cvs.width, this._cvs.height);
// ③ボールに速度を設定.Y軸に重力値を適用
this._vy += this._gravity;
this._circle.x += this._vx;
this._circle.y += this._vy;
// ④角度のサインとコサインを取得
let cos = Math.cos(this._line.rotation);
let sin = Math.sin(this._line.rotation);
// ⑤斜線を基準にしたボールの位置の取得
let x1 = this._circle.x - this._line.x;
let y1 = this._circle.y - this._line.y;
// ⑥座標の回転
let x2 = cos * x1 + sin * y1;
let y2 = cos * y1 - sin * x1;
// ⑦速度の回転
let vx1 = cos * this._vx + sin * this._vy;
let vy1 = cos * this._vy - sin * this._vx;
// ⑧ボールが斜線に当たったか判定
if (y2 > 0 - this._circle.radius) {
// ⑨回転させた値を使った跳ね返りの実行
y2 = -this._circle.radius;
vy1 *= this._bounce;
}
// ⑩全てを回転させて元状態に戻す
x1 = cos * x2 - sin * y2;
y1 = cos * y2 + sin * x2;
this._vx = cos * vx1 - sin * vy1;
this._vy = cos * vy1 + sin * vx1;
this._circle.x = this._line.x + x1;
this._circle.y = this._line.y + y1;
this._circle.draw();
this._line.draw();
this._frame++;
if (elapsedTime >= 1000) {
this._startTime = this._nowTime;
this._frame = 0;
}
}
this._animID = requestAnimationFrame(this._mainLoop.bind(this));
}
}
requestAnimationFrameを使ったゲームループの実装は、「【JavaScript】requestAnimationFrameでゲームループを作る」を参照。
①まず反射面である線をX座標50、Y座標200の地点に描画。②角度は15度でラジアンに変換している。
③最初に重力値をボールのY軸に加算。さらに速度をボールに適用。
④線の角度を使いサインとコサインを取得。後に回転する時に使用。
⑤線の位置を基準にしたボールのX、Y座標を、ボールの位置から線の位置を引くことで求める。これが回転の中心点となる。
⑥座標の回転。回転のやり方は「【JavaScript】オブジェクトを円運動させる【JavaScript】オブジェクトを円運動させる」の後半で解説している。③で線の角度を元にサインとコサインを出しているため、同じ角度量回転して水平になる。
⑦速度も同じように回転させる。速度は角度と大きさ(長さ)を持つベクトルのため、角度が分かれば回転可能。
⑧ボールが跳ねる下の境界は0となる線自体のため、y2が0からボールの半径を引いた値よりも大きいかどうかで、当たり判定を行っている。
⑨線に上に来るようボール位置を調整し、跳ね返りの値を適用して跳ね返させる。
⑩全てを回転させて元状態に戻す。逆回転させるため下4行の+と-を逆にしている。元の座標を変数で保存しておくよりも手軽。
線から落ちるようにする
このままでは、線上を通り過ぎても転がり続けるため、ボールが線上にあるか判定を行う。判定処理を追加したのがAngleBounce2クラス。
AngleBounceクラスの③箇所の上に、
let bounds = this._line.getBounds();
を追加。線の描画領域を矩形情報(X座標、Y座標、幅、高さ)を取得。
さらに、④〜⑩までを以下のif文で囲っただけ。
if (this._circle.x + this._circle.radius > bounds.x
&& this._circle.x - this._circle.radius < bounds.x + bounds.width) {
...
}
スクリプトとコードは以下。
import { Line } from "./line.js";
import { Circle } from "./circle.js";
/**
* 角度のついた跳ね返り運動2(anglebounce2.js)
*/
export class AngleBounce2 {
constructor() {
this._cvs = document.getElementById('canvas');
this._ctx = this._cvs.getContext('2d');
this._line;
this._circle;
this._gravity = 0.2;
this._bounce = -0.6;
this._vx = 0;
this._vy = 0;
// タイマー関連
this._animID;
this._isAnim = 0;
this._FPS = 60;
this._frame = 0;
this._startTime;
this._nowTime;
this._init();
}
//////////////////////////////////
// Private and protected
//////////////////////////////////
_init() {
this._cvs.style.backgroundColor = "#eeeeee";
// 角度のついた反射面を描画
this._line = new Line(this._ctx, 0, 0, 250, 0);
this._line.x = 50;
this._line.y = 200;
// 角度をラジアンに変換して渡す
this._line.rotation = 10 * Math.PI / 180;
this._circle = new Circle(this._ctx, 100, 100, 30);
this._startTime = performance.now();
this._mainLoop();
}
_mainLoop() {
this._nowTime = performance.now();
let elapsedTime = this._nowTime - this._startTime;
let idealTime = this._frame * (1000 / this._FPS);
if (idealTime < elapsedTime) {
this._ctx.clearRect(0, 0, this._cvs.width, this._cvs.height);
// ①線の矩形領域をオブジェクト(x, y, 幅, 高さ)で取得
let bounds = this._line.getBounds();
// ボールに速度を設定.Y軸に重力値を適用
this._vy += this._gravity;
this._circle.x += this._vx;
this._circle.y += this._vy;
// ②ボールが線上にあるか判定
if (this._circle.x + this._circle.radius > bounds.x
&& this._circle.x - this._circle.radius < bounds.x + bounds.width) {
// 角度のサインとコサインを取得
let cos = Math.cos(this._line.rotation);
let sin = Math.sin(this._line.rotation);
// 線を基準にしたボールの位置の取得
let x1 = this._circle.x - this._line.x;
let y1 = this._circle.y - this._line.y;
// 座標の回転
let x2 = cos * x1 + sin * y1;
let y2 = cos * y1 - sin * x1;
// 速度の回転
let vx1 = cos * this._vx + sin * this._vy;
let vy1 = cos * this._vy - sin * this._vx;
// ボールが線に当たったか判定
if (y2 > 0 - this._circle.radius) {
// 回転させた値を使った跳ね返りの実行
y2 = -this._circle.radius;
vy1 *= this._bounce;
}
// 全てを回転させて元状態に戻す
x1 = cos * x2 - sin * y2;
y1 = cos * y2 + sin * x2;
this._vx = cos * vx1 - sin * vy1;
this._vy = cos * vy1 + sin * vx1;
this._circle.x = this._line.x + x1;
this._circle.y = this._line.y + y1;
}
this._circle.draw();
this._line.draw();
this._frame++;
if (elapsedTime >= 1000) {
this._startTime = this._nowTime;
this._frame = 0;
}
}
this._animID = requestAnimationFrame(this._mainLoop.bind(this));
}
}
複数の反射面を跳ね返らせる
最後に反射面を4つに増やしてみる。
4つの反射面を跳ね返り落下していく処理を担当するAngleBounce3クラス。
import { Line } from "./line.js";
import { Circle } from "./circle.js";
/**
* 角度のついた跳ね返り運動3(anglebounce3.js)
*/
export class AngleBounce3 {
constructor() {
this._cvs = document.getElementById('canvas');
this._ctx = this._cvs.getContext('2d');
// ①反射面の線を4本分配列で用意
this._lines = [];
this._NUM_LINES = 4;
this._circle;
this._gravity = 0.2;
this._bounce = -0.6;
this._vx = 0;
this._vy = 0;
// タイマー関連
this._animID;
this._isAnim = 0;
this._FPS = 60;
this._frame = 0;
this._startTime;
this._nowTime;
this._init();
}
//////////////////////////////////
// Private and protected
//////////////////////////////////
_init() {
this._cvs.style.backgroundColor = "#eeeeee";
// ②角度のついた反射面を描画
let lx = [50, 200, 50, 180];
let ly = [100, 150, 200, 280];
let lr = [10, -30, 10, -15];
for (let i = 0; i < this._NUM_LINES; i++) {
this._lines[i] = new Line(this._ctx, 0, 0, 100, 0);
this._lines[i].x = lx[i];
this._lines[i].y = ly[i];
this._lines[i].rotation = lr[i] * Math.PI / 180;
}
this._circle = new Circle(this._ctx, 100, 0, 30);
this._startTime = performance.now();
this._mainLoop();
}
_checkLine(line) {
// 線の矩形領域をオブジェクト(x, y, 幅, 高さ)で取得
let bounds = line.getBounds();
// ボールが線上にあるか判定
if (this._circle.x + this._circle.radius > bounds.x
&& this._circle.x - this._circle.radius < bounds.x + bounds.width) {
// 角度のサインとコサインを取得
let cos = Math.cos(line.rotation);
let sin = Math.sin(line.rotation);
// 線を基準にしたボールの位置の取得
let x1 = this._circle.x - line.x;
let y1 = this._circle.y - line.y;
// 座標の回転
let x2 = cos * x1 + sin * y1;
let y2 = cos * y1 - sin * x1;
// 速度の回転
let vx1 = cos * this._vx + sin * this._vy;
let vy1 = cos * this._vy - sin * this._vx;
// ⑤ボールが線に当たったか判定
if (y2 > 0 - this._circle.radius && y2 < vy1) {
// 回転させた値を使った跳ね返りの実行
y2 = -this._circle.radius;
vy1 *= this._bounce;
}
// 全てを回転させて元状態に戻す
x1 = cos * x2 - sin * y2;
y1 = cos * y2 + sin * x2;
this._vx = cos * vx1 - sin * vy1;
this._vy = cos * vy1 + sin * vx1;
this._circle.x = line.x + x1;
this._circle.y = line.y + y1;
}
line.draw();
}
_mainLoop() {
this._nowTime = performance.now();
let elapsedTime = this._nowTime - this._startTime;
let idealTime = this._frame * (1000 / this._FPS);
if (idealTime < elapsedTime) {
this._ctx.clearRect(0, 0, this._cvs.width, this._cvs.height);
// ③ボールに速度を設定.Y軸に重力値を適用
this._vy += this._gravity;
this._circle.x += this._vx;
this._circle.y += this._vy;
// ④ボールと各線の跳ね返り処理を線の数だけ実行
for (let i = 0; i < this._NUM_LINES; i++) {
if (this._checkLine(this._lines[i])) {
break;
}
}
this._circle.draw();
this._frame++;
if (elapsedTime >= 1000) {
this._startTime = this._nowTime;
this._frame = 0;
}
}
this._animID = requestAnimationFrame(this._mainLoop.bind(this));
}
}
①反射面を4つにするため、配列で管理。
②4つの反射面をfor文で作成。
③ボールは今までと同じ1つのため、ボールに対する重力や速度の計算、適用はメインループで1度だけ行う。
④反射面となる線とボールの跳ね返り処理は、線が4つあるため独立した関数にまとめた。その_checkLine()をfor文で各線分呼び出す。
⑤if文の条件式の後半に「&& y2 < vy1」を追加している。これがないと跳ね返ったボールが上の線を超えると、超えた線に登ってしまう。
参考図書
コメント