JavaScriptでゲーム作り「6:線分の当たり判定を実装する」

線分の当たり判定を実装する

(2019.2.08執筆)

背景スクロールへ移る前に、線分の当たり判定を記述したいと思います。 前回、形状によって当たり判定の式を振り分けることができたので、線分の要素にも手を加えられそうです。
線の当たり判定がとれると、背景マップに道を作ったり、通行判定にも線分が使えるような気がしてます。 傾いた四角形や多角形にも応用が効く。。ぜひ挑戦してみるのです。


今回参考にするサイトさんは、よく判らなかったので調べながらぼちぼちでした。 線分の当たり判定は、ベクトルの数式をフルに使う、値がいっぱいあって式がややこしい、判りづらいのが問題かな。。 やってることは単純なはずなのにね。

では、線分の定義「Line」クラスから作成していきます。

Line extends Colliderクラスの作成


class Collider { //当たり判定に使うクラスの元
  constructor(type, x, y, isObj = false) {
    this._type = type;
    this.x = x;
    this.y = y;
    this.isObj = isObj; //背景オブジェクト同士の判定に利用
  }
  get type() { return this._type; }
}

// ↓ ここから
class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2, isObj) {
    super('line', x, y, isObj);
    this.x2 = x2;
    this.y2 = y2;
  }

  get top() { return this.y < this.y2 ? this.y : this.y2; }  // 四分木の空間登録で使う
  get bottom() { return this.y > this.y2 ? this.y : this.y2; } // 同上
  get left() { return this.x < this.x2 ? this.x : this.x2; } // 同上
  get right() { return this.x > this.x2 ? this.x : this.x2; } // 同上
}
これは、点(x,y)と点(x2,y2)を結ぶ線分の定義です。
Colliderクラスからの拡張になり、typeに'line'を持つColliderクラスでもあります。

Line(線分)同士の衝突判定を考える


class CollisionDetector {
  detectCollision(c1, c2) {    
    if(c1.type == 'line' && c2.type=='line') { //両方線分なら、線分同士の衝突判定式を呼び出す
      return this.deLineLine(c1, c2);
    }
    return false;
  }
  
  deLineLine(lineA, lineB) { //線分と線分の衝突判定式、線分同士は難しいので、直線の傾きと線分の傾きで2回クロス判定している。両方クロス判定なら当たり。
    const lineA.dx = lineA.x2 - lineA.x; // lineAのx軸の移動距離を求める
    const lineA.dy = lineA.y2 - lineA.x; // lineAのy軸の移動距離を求める
    const bs1 = lineA.dx * (lineB.y - lineA.y) - lineA.dy * (lineB.x - lineA.x);
    const bs2 = lineA.dx * (lineB.y2 - lineA.y) - lineA.dy * (lineB.x2 - lineA.x);
    if (bs1 * bs2 > 0) {return false;}

    const lineB.dx = lineB.x2 - lineB.x; // lineBのx軸の移動距離を求める
    const lineB.dy = lineB.y2 - lineB.x; // lineBのy軸の移動距離を求める
    const as1 = lineB.dx * (lineA.y - lineB.y) - lineB.dy * (lineA.x - lineB.x);
    const as2 = lineB.dx * (lineA.y2 - lineB.y) - lineB.dy * (lineA.x2 - lineB.x);
    if (as1 * as2 > 0) {return false;}
    return true;
    }
  }
}
CollisionDetectorは、当たり判定を計算するときに必要となるクラス。
今回は線分の衝突判定式を呼び出して判定するまでの記述です。

判定式はdeLineLine(lineA, lineB)なんですが、中身に要素がいっぱいありすぎて何が何だか分かりません。 線分のやっかいな所は、1つのオブジェクトで要素が4つあり、それらを満遍なく使って判定してる所にあります。線分二つで要素は合計8つ。ホント意味わかりませんね。

もっと何が何だか理解るように、線分の要素をじっくり紐解いて見ていきましょう。

線分のdx,dy要素をクラス内に定義しておく


class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2) {
    super('line', x, y);
    this.x2 = x2;
    this.y2 = y2;
  }
  get dx() { return this.x2 - this.x; } // x軸がどちら側にどれだけ移動したかを求める
  get dy() { return this.y2 - this.y; } // y軸がどちら側にどれだけ移動したかを求める
}
さて、線分をベクトルに当て嵌めて、ベクトル計算によく使う要素を前もって記述しておくことにします。 まずx軸の移動距離、点のx座標から、点2のx2座標までどれだけ移動したかを求める式。

{x2 - x;}ですね。これをthis.dxで呼び出せるように記述します。

同じくy軸の移動距離についても
{y2 - y;}...これをthis.dyで呼び出せるようにしておく。

dxとdy...つまりx軸の移動方向に対してy軸がどちらにどれだけ移動するかが判れば、線分の傾き加減が判るのですよ。 例えば、dxでdyを割った値 > 0 (正)なら右上がりの線分。値 < 0(負)なら右下がりの線分となり、dx=0(x軸が変化しない)なら縦の線だし、dy=0(y軸が変化しない)なら横の線です。

dxとdyは線分の計算で、いっぱい使いまわします。前もって定義しておくと良いかな。
すると、先ほどの線分同士の衝突判定式がこのようになります。

線分の衝突判定式


  deLineLine(lineA, lineB) { //線分と線分の衝突判定式、線分同士は難しいので、直線の傾きと線分でクロス判定する。
    const bs1 = lineA.dx * (lineB.y - lineA.y) - lineA.dy * (lineB.x - lineA.x); //直線A(lineAの傾き)を隔てて、lineBの始点が左右(+-)どちら側にある?
    const bs2 = lineA.dx * (lineB.y2 - lineA.y) - lineA.dy * (lineB.x2 - lineA.x); //直線A(lineAの傾き)を隔てて、lineBの終点が左右(+-)どちら側にある?
    if (bs1 * bs2 > 0) {return false;} // 始点と終点の+-関係。両方同じ側にある場合は正の値になる、つまり交差していない。当たってない判定。

    const as1 = lineB.dx * (lineA.y - lineB.y) - lineB.dy * (lineA.x - lineB.x); //直線B(lineBの傾き)を隔てて、lineAの始点が左右(+-)どちら側にある?
    const as2 = lineB.dx * (lineA.y2 - lineB.y) - lineB.dy * (lineA.x2 - lineB.x); //直線B(lineBの傾き)を隔てて、lineAの終点が左右(+-)どちら側にある?
    if (as1 * as2 > 0) {return false;}

    return true; // 両方交差してるなら、当たってます!
  }
ちょっとスッキリした??
解説はこちらのサイトさんが解りやすかったような気がします。

http://net2.cocolog-nifty.com/blog/2009/11/post-8792.html
http://www5d.biglobe.ne.jp/~tomoya03/shtml/algorithm/Intersection.htm


クロス判定は、始点と終点が線分Aの傾き(直線で考える)を隔ててどちら側にあるかを求める計算です。 線分Aの傾きに対して、線分Bの始点、終点の位置関係を求め、その正負関係によって交差してるかしてないかを判定しています。


線分の傾きに対する各点の位置関係は、外積の計算方法を用いて、このように求まります。
  crossAB(x, y) { return this.dx * (this.y - y) - this.dy * (this.x - x); } //他の点との位置関係...0なら線分の直線上に点が重なる

この公式を、基本となるLineColliderクラスに組み込んでおくとどうでしょう?

Line Colliderクラスに追記


class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2) {
    super('line', x, y);
    this.x2 = x2;
    this.y2 = y2;
  }
  get dx() { return this.x2 - this.x; } // x軸がどちら側にどれだけ移動したかを求める
  get dy() { return this.y2 - this.y; } // y軸がどちら側にどれだけ移動したかを求める
  
  //ここから追記
  crossAB(x, y) { return this.dx * (this.y - y) - this.dy * (this.x - x); } //他の点との位置関係...0なら線分の直線上に点が重なる
  //ここまで
//続く

すると、線分同士の交差判定(当たり判定)が、以下のように短縮できます。

線分の衝突判定式


  deLineLine(lineA, lineB) { //線分と線分の衝突判定式、線分同士は難しいので、直線の傾きと線分でクロス判定する。
    if (lineA.crossAB(lineB.x, lineB.y) * lineA.crossAB(lineB.x2, lineB.y2) > 0) {return false;} //始点と終点の位置関係で、正負が分かれる。
    if (lineB.crossAB(lineA.x, lineA.y) * lineB.crossAB(lineA.x2, lineA.y2) > 0) {return false;} //始点と終点が同じ側のときは正の値になる、当たってない。
  }

かなり見通しが良くなりましたね。
さて、線分と線分と衝突判定(交差判定?)が判れば、線分と矩形の衝突判定もできるようになります。やってみましょう。

線分と矩形の衝突判定式


class CollisionDetector {
  detectCollision(c1, c2) {
    if(c1.type == 'rectangle' && c2.type=='line') { return this.deRectLine(c1, c2); } //矩形と線分同士の衝突判定式を
    if(c1.type == 'line' && c2.type=='rectangle') { return this.deRectLine(c2, c1); } //線分と矩形同士の衝突判定式を
  }

  deLineLine(lineA, lineB) { //線分と線分の衝突判定式、線分同士は難しいので、直線の傾きと線分でクロス判定する。
    if (lineA.crossAB(lineB.x, lineB.y) * lineA.crossAB(lineB.x2, lineB.y2) > 0) {return false;} //+-の位置関係で、正負が分かれる。
    if (lineB.crossAB(lineA.x, lineA.y) * lineB.crossAB(lineA.x2, lineA.y2) > 0) {return false;} //始点と終点が同じ側のときは正の値になる、当たってない。

    return true; // 両方交差してるなら、当たってます!
  }

  //↓ ここから追加
  deRectLine(rect, line) { //矩形と線分の衝突判定式、矩形の各辺の線分と、線分の衝突判定を呼び出す。全てfalseなら当たってない。
    if( this.deRectPoint(rect, line.x, line.y) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.left, rect.top, rect.right, rect.top), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.right, rect.top, rect.right, rect.bottom), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.right, rect.bottom, rect.left, rect.bottom), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.left, rect.bottom, rect.left, rect.top), line) ) { return true; }
    return false;
  }
}

矩形と線分の当たり判定は、矩形を結ぶ各線分との当たり判定計算式を呼び出しています、どれか一つ当たっていればtrueを返す。線分×線分の当たり判定ができてたら、とても簡単です。
この方法を応用すれば、矩形に限らず、三角形や多角形、傾いた矩形同士でも当たり判定を取れるようです。
また、矩形と始点(終点)の当たり判定まで含めれば、矩形内に線分が収まる場合でも判定を返せます。


次に、円と線分の衝突判定について考えてみます。

円と線分の衝突判定式


class CollisionDetector {
  detectCollision(c1, c2) {
    if(c1.type == 'circle' && c2.type=='line') { return this.deCircleLine(c1, c2); } //円と線分同士の衝突判定式を
    if(c1.type == 'line' && c2.type=='circle') { return this.deCircleLine(c2, c1); } //線分と円同士の衝突判定式を
  }

  deCircleLine(circleP, lineAB) { //円と線分の衝突判定式
    const lineAP = new LineCollider(lineAB.x, lineAB.y, circleP.x, circleP.y) //線分の頂点Aから、円の中心Pへのラインを引く1
    const lineBP = new LineCollider(lineAB.x2, lineAB.y2, circleP.x, circleP.y) //線分の頂点Bから、円の中心Pへのラインを引く2

    //点Pを中心とする円から、最短距離となる線AB上の点をXとする。
    const innerAX = lineAB.innerP(lineAP) / lineAB.length; //ベクトルABとベクトルAPから[ベクトルAX]の長さを(内積で)求める
    const crossPX = lineAB.crossP(lineAP) / lineAB.length; //ベクトルABとベクトルAPから[ベクトルPX]の長さを(外積で)求める

    let distance;
    if( innerAX < 0  ){ distance = lineAP.length;}
    else if( innerAX > lineAB.length ){ distance = lineBP.length;}
    else { distance = Math.abs(crossPX); }

    if( distance < circleP.radius ) { return true; } //最短距離が円の半径よりも小さい場合は、当たり
    else {return false;}
  }
}


詳しい解説サイトさんは、こちらとか。
円と線分の当たり判定を行うには


この円と線分の接触判定の式を記述するのに、LineColliderクラス側でいくつかの公式を組み込みました。

class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2, isObj) {
    super('line', x, y, isObj);
    this.x2 = x2;
    this.y2 = y2;
  }
  get dx() { return this.x2 - this.x; } // x軸の移動方向と距離を求める、ベクトル成分のxになる
  get dy() { return this.y2 - this.y; } // y軸の移動方向と距離を求める、ベクトル成分のyになる

  get length() { return Math.sqrt( this.dx * this.dx + this.dy * this.dy ); } // 線分の長さを求める
  innerP(other) { return this.dx * other.dx + this.dy * other.dy; } //他のベクトル線分との内積
  crossP(other) { return this.dx * other.dy - other.dx * this.dy; } //他のベクトル線分との外積

  crossAB(x, y) { return this.dx * (this.y - y) - this.dy * (this.x - x); } //他の点との位置関係...0なら線分の直線上に点が重なる

前回から加えた公式は、この3つです。
  get length() { return Math.sqrt( this.dx * this.dx + this.dy * this.dy ); } // 線分の長さを求める
  innerP(other) { return this.dx * other.dx + this.dy * other.dy; } //他のベクトル線分との内積
  crossP(other) { return this.dx * other.dy - other.dx * this.dy; } //他のベクトル線分との外積

このline.length(線分の長さ)は、dxとdyを用いた三平方の定理で求められます。
しかし、内積とは? 外積とは? なんでしょう。意味わかりませんね。。

線分同士の内積について

線分同士というか、ベクトル同士の内積といいますか。いったいコレは何をやってるのか?
もういっかい記事を参考にすると、ちょっと理解できるかも??
円と線分の当たり判定を行うには


内積とは、各ベクトル同士で始点を合わせた時に成される角度(cosθ)が算出できる式です!といっても計算結果はcosθの値に各ベクトルの長さを掛け合わせた値が乗算されます。

内積の計算結果 = this.length * other.length *(cosθ)

この計算結果から、this.length(この線分の長さ)を除算することで、斜辺(other.length)に対する直角三角形の底辺の長さを求めることができます。。
このことが、円と線分の衝突判定の際に、innerAXで定義している部分になります。
 //Pを中心とする円から、最短距離となる線AB上の点をXとする。
    const innerAX = lineAB.innerP(lineAP) / lineAB.length; //ベクトルAB上にある[ベクトルAX]の長さをベクトルAPとの内積で求める

このinnerAXの値によって、円の中心点からの最短距離が、線分AB上のどの点かが分かります。

if( innerAX < 0 ){ distance = lineAP.length;} //値がマイナスの場合、点Aの始点が点Pからの最短距離
    else if( innerAX > lineAB.length ){ distance = lineBP.length;} //値が線分ABより長くなる場合、点Bの終点が点Pからの最短距離
    else { distance = Math.abs(crossPX); }  //値が線分ABの間に収まる場合、crossPXを求める

線分同士の外積について

外積とは、各ベクトル同士で始点を合わせた時に成される角度(sinθ)が算出できる式です!といっても計算結果はsinθの値に各ベクトルの長さを掛け合わせた値が乗算されます。

外積の計算結果 = this.length * other.length *(sinθ)

外積の計算結果から、this.length(この線分の長さ)を除算することで、斜辺(other.length)に対する直角三角形の高さを求めることができます。。
//Pを中心とする円から、最短距離となる線AB上の点をXとする。
    const crossPX = lineAB.crossP(lineAP) / lineAB.length; //ベクトルABとベクトルAPから[ベクトルPX]の長さを(外積で)求める


この値が、円と線分の衝突判定の際にcrossPXで定義している部分となり、着地点Xが線分ABの間に収まる場合、このcrossPXの値が円の中心点Pから線分ABまでの最短距離として利用されます。

円の中心からの最短距離が、円の半径より小さい場合に接触


    if( sDistance < circleP.radius ) { return true; } //最短距離が円の半径よりも小さい場合は、当たり
    else {return false;}
以上の計算結果から、円の中心からの最短距離が円の半径より小さくなるかどうかをチェックして、円と線分の当たり判定が完了します。 おめでとう、ぱちぱち*:・'゚☆♪ヽ(。◕ v ◕。)ノ

円と線分との当たり判定、実践ver

後日追記。この2ヶ月後、曲線の当たり判定を吟味中に、興味深い記事を発見しました。

割り算を避ける(高速化プログラミング)
高速根号計算 (fast sqrt algorithm)

曰く、割り算の演算処理は、掛け算の倍以上の時間がかかる。
それから√(平方根)を求める演算処理は、単純な2乗計算よりもかなり時間かかる。

よって割り算の箇所を、掛け算で済む所は掛け算で。
√(平方根)を求める演算(この場合はline.lengthにあたる)は、line.lengthの2乗した形で、それぞれを比較すると負担が少ない。といったところです。

例えば、原点(0, 0)を中心とする円と点(x, y)の当たり判定../
半径 >= √(x*x + y*y)とするより
半径×半径 >= x*x + y*y としたほうがやや高速化する。。

ということで、円と線分の当たり判定も、上記の形で(最短距離の2乗 <= 半径の2乗 ? true : false)のように比較すると良いかなって思います。
前もって、LineColliderクラスに線分の長さの2乗を求める計算式を組み込んでおきます。

LineColliderクラスに追記

  get length2() { return this.dx * this.dx + this.dy * this.dy; } // 線分の長さの2乗を求める(2点間の距離を比較するときに使う...平方計算を省略して高速化)

円と線分の当たり判定式(やや高速版)


  deCircleLine(circleP, lineAB) { //円と線分の衝突判定式
    const lineAP = new LineCollider(lineAB.x, lineAB.y, circleP.x, circleP.y) //線分の頂点Aから、円の中心Pへのラインを引く1
    const lineBP = new LineCollider(lineAB.x2, lineAB.y2, circleP.x, circleP.y) //線分の頂点Bから、円の中心Pへのラインを引く2

    // ここから円の半径を2乗した値で最短距離を比較演算する判定式。平方根を求めないので、プログラム計算の負担が少ない。
    const innerAX = lineAB.innerP(lineAP); //ベクトルAB上にある[ベクトルAX]の長さ×[ベクトルAB]の長さをベクトルAPとの「内積」で求める

    if(innerAX <= 0){ // innerAXが負のとき、lineAPの長さが、円とラインの最短距離。
        return lineAP.length2 < circleP.radius*circleP.radius ? true : false; //最短距離の2乗が円の半径の2乗よりも小さい場合は、当たり
    }
    else if(innerAX >= lineAB.length2){ // innerAXがlineABの長さの2乗より大きい、つまりlineAXの長さがlineABより大きいとき、lineBPの長さが円とラインの最短距離。
        // ↑ (lineAX.length*lineAB.length > lineAB.length*lineAB.length)のとき、
        return lineBP.length2 < circleP.radius*circleP.radius ? true : false; //最短距離の2乗が円の半径の2乗よりも小さい場合は、当たり
    }
    else { // innerAXがlineAB間に収まるとき Math.abs(crossPX / lineAB.length)が、円とラインの最短距離
        const crossPX = lineAB.crossP(lineAP); //ベクトルABとベクトルAPから[ベクトルPX]の長さ×[ベクトルAB]の長さを「外積」で求める
        return Math.abs(crossPX) < circleP.radius * lineAB.length ? true : false; //crossPXの絶対値が、円の半径*lineAB.lengthよりも小さい場合は、当たり
    }
  }

これで、線分の当たり判定が全て記述できました。
では、ラインアクタークラスを作成して。シーンに線分を追加してみましょう。

ラインを描画して、LineCollederの当たり判定を割り当てる

では、実際にラインを描画していきます。
danmaku.jsに、シーンに追加するためのラインアクタークラスを定義して...

class LineActor extends Actor {
    constructor(x, y, dx, dy) {
        const hitArea = new LineCollider(x, y, x + dx, y + dy);
        super(x, y, hitArea, ['object']);
        this.dx = dx;
        this.dy = dy;
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.addEventListener('hit', (e) => this._color = 'rgba(255, 0, 0, 0.9)' );//hit中は赤くなります
    }

    get x() { return this._x; }
    set x(value) { this._x = value; }//x座標にvalueを代入する関数をActorから上書き
    get y() { return this._y; }
    set y(value) { this._y = value; }//y座標にvalueを代入する関数をActorから上書き

    update(gameInfo, input) {
        this._color = 'rgba(255, 255, 255, 0.9)';
    }
    render(target) {
        const context = target.getContext('2d');
        context.strokeStyle = this._color;
        context.beginPath(); //描画位置の初期化
        context.moveTo(this.x, this.y); //ラインの始点座標
        context.lineTo(this.x + this.dx , this.y + this.dy); //ラインの終点座標
        context.stroke(); //線を引く
    }
}
danmaku.jsのメインシーンに追加します。

class DanmakuStgMainScene extends Scene {
    constructor(renderingTarget) {
        super('メイン', 'black', renderingTarget);
        const npc = new NPC(150, 100);
        const player = new Player(150, 200);
        this.add(npc);
        this.add(player);

        // MainSceneのconstructorの中で
        this.actors.forEach((actor) => actor.addEventListener('messageWindowRequest', (event) => {
            // この場合のeventの中身はメッセージ配列です
            const messageWindow = new MessageWindow(event);
            // プレイヤーの動きを止めます
            // isActiveみたいな変数をPlayerに用意しておくと良いかもです
            // そしてそれをfalseに切り替えて、あとはPlayer側でif(isAcitve)ならキーボードで動くようにする……とか
            player.isActive = false;

            // そしてメッセージが終わったらプレイヤーをactiveに戻す処理を登録
            messageWindow.addEventListener('textEnd', (e) => player.isActive = true);
            // やっとシーンに追加
            this.add(messageWindow);
        }));

  const line2 = new LineActor(20, 150, 240, 40); //左のライン
     this.add(line2);
  const line3 = new LineActor(350, 70, 0, 220); //右のライン
     this.add(line3);
    }
}

では、確認してみましょう。
ライン描画
⇒ ライン描画のデモを見る



さて、ラインアクターのコードなのですが、
  const line2 = new LineActor(20, 150, 240, 40); //左のライン
     this.add(line2);
  const line3 = new LineActor(350, 70, 0, 220); //右のライン
     this.add(line3);

LineActorの各数字の意味が、LineColliderと少し変わってます。
最初のx座標、y座標は同じですが、その次はx2ではなくdx...つまりx軸方向にどれだけ移動したかのベクトルを表記しています。 同じく4番目がdy...。。。

なぜこのような面倒な表記にしなければならなかったかというと、 継承元のActorクラスにある、set x()とset y()の関数です。

Actorオブジェクトに{x,y,x2,y2}と、xの座標点を2つ、yの座標点を2つ設定してしまうと、xやyが移動した際にx2やy2も平行移動しなければならない(背景スクロール時など) しかし、どうしてもxとx2を同時に変更するのは難しく、線分の交差判定の際に色々やってみましたが無理でした。(最初は当たり判定の式に問題があったかと思った..気づくまで大変でした)

結局、LineActorは初期のx座標y座標にベクトルを加えて記述するしか思いつきませんでした。
class LineActor extends Actor {
    constructor(x, y, dx, dy) {
        const hitArea = new LineCollider(x, y, x + dx, y + dy);
        super(x, y, hitArea, ['object']);
        this.dx = dx;
        this.dy = dy;
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.addEventListener('hit', (e) => this._color = 'rgba(255, 0, 0, 0.9)' );//hit中は赤くなります
    }

    get x() { return this._x; }
    set x(value) { this._x = value; }//x座標にvalueを代入する関数をActorから上書き
    get y() { return this._y; }
    set y(value) { this._y = value; }//y座標にvalueを代入する関数をActorから上書き

できるならば、LineActorも{x,y,x2,y2}で表記したほうが分かりやすいでしょうけど難しいです。ベクトル表記で今はいきます。

さて、それ以外は、だいたい上手く作動しているみたいですね。。アクションにもちゃんと反応します。
残る課題は、ラインにぶつかった時のバウンス判定でしょうか。。

線分に衝突した時の反動ベクトルを求める

さて、次にラインの壁に衝突した時の押し戻しについて考えてみます。 が、これは簡単に求まりました。壁にぶつかった時の反動ベクトルは、ラインに垂直なベクトル方向になってます。
そして、ラインに垂直なベクトルを求める式というのが、以下のURLが参考になります。

参考 ⇒ 直線の方程式の一般形が嬉しい3つの理由


線分の直線式をa*X + b*Y + c = 0 という形に整理すると。
このラインに対する法線ベクトル(a, b)を求められるのです。

まず、線分の始点と終点を結ぶ直線式を整理してみます。
  // 直線の式 dy*(X - x) = dx*(Y - y); // (Xはxとx2の間、Yはyとy2の間)
  // dy*X - dx*Y - dy*x + dx*y = 0; //これが直線の式の一般形。

線分の直線式は、以下のようになります。
dy*X - dx*Y - dy*x + dx*y = 0;

このとき、法線ベクトルが(dy, -dx)となるのが分かります。
...もとの線分のベクトルが(dx, dy)だったのを考えると、面白い対比ですね。

実際にこの法線ベクトルを扱う時は、単位ベクトルに直してからのほうが使いやすいです。 ベクトルの大きさは、元の線分の長さと変わらないので、それぞれをline.lengthで割ると、法線の単位ベクトルが求まります!

法線の単位ベクトル = (dy / line.length, -dx / line.length)

この数式を、Lineクラスに組み込んでおきましょう。
また、法線ベクトルを負の方向でとる場合もあるので、そちらも同時に記述します。

線分の法線ベクトルを、Lineクラス(Colliderの方)に組み込んでおく


class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2, isObj) {
    super('line', x, y, isObj);
    this.x2 = x2;
    this.y2 = y2;
  }
  get dx() { return this.x2 - this.x; } // x軸の移動方向と距離を求める、ベクトル成分のxになる
  get dy() { return this.y2 - this.y; } // y軸の移動方向と距離を求める、ベクトル成分のyになる
  get length() { return Math.sqrt( this.dx * this.dx + this.dy * this.dy ); } // 線分の長さを求める(三平方の定理より)

  // 直線の式 dy*(X - x) = dx*(Y - y); // (Xはxとx2の間、Yはyとy2の間)
  // dy*X - dx*Y - dy*x + dx*y = 0; これが直線の式の一般形。

  // ここから、ラインの法線ベクトル(これに垂直なベクトル)は(dy, -dx)というのが求まる。
  // 法線ベクトルを単位ベクトルに置き換えた値(その長さで割る)を、衝突判定後のバウンス処理で利用できる。
  get boundVect() { return { dx:this.dy / this.length, dy:- this.dx / this.length }; } //法線ベクトルを正でとる
  get boundVect_() {return { dx:- this.dy / this.length, dy:this.dx / this.length }; } //法線ベクトルを負でとる

  innerP(other) { return this.dx * other.dx + this.dy * other.dy; } //他のベクトル線分との内積
  crossP(other) { return this.dx * other.dy - other.dx * this.dy; } //他のベクトル線分との外積
  crossAB(x, y) { return this.dx * (this.y - y) - this.dy * (this.x - x); } //他の点との位置関係...0なら線分の直線上に点が重なる

  get top() { return this.y < this.y2 ? this.y : this.y2; }  // 四分木の空間登録で使う
  get bottom() { return this.y > this.y2 ? this.y : this.y2; } // 同上
  get left() { return this.x < this.x2 ? this.x : this.x2; } // 同上
  get right() { return this.x > this.x2 ? this.x : this.x2; } // 同上
}

追加したのはこの部分です。
  get boundVect() { return { dx:this.dy / this.length, dy:- this.dx / this.length }; } //法線ベクトルを正でとる
  get boundVect_() {return { dx:- this.dy / this.length, dy:this.dx / this.length }; } //法線ベクトルを負でとる

ではこの関数を用いて、実際に動いているオブジェクト(プレイヤー側など)に、ライン衝突時の押し戻しを記述してみます。

あ、その前に、キャラクターのhitAreaに使うRectangleColliderクラスから、矩形の中心座標がとれるよう、関数を追記しておくことにします。

RectangleColliderクラスに追記


class RectangleCollider extends Collider { //当たり判定を四角形で使うクラス
  constructor(x, y, width, height, isObj) {
    super('rectangle', x, y, isObj);
    this.width = width;
    this.height = height;
  }
//ここから追加
  get cx() { return this.x + this.width/2; } //矩形の中心のX座標
  get cy() { return this.y + this.height/2; } //矩形の中心のY座標
  //ここまで
  get top() { return this.y; }
  get bottom() { return this.y + this.height; }
  get left() { return this.x; }
  get right() { return this.x + this.width; }
}

this.hitArea.cxで中心座標x。
this.hitArea.cyで中心座標yが取得できます。

この中心点が、ラインのどちら側にあるかで、反動ベクトルが正か負かを振り分けます。
では、プレイヤークラスに移りましょう。

danmaku.js ⇒ Playerクラス constructor内に記述


        this.addEventListener('hit', (e) => {
            if(!e.target.hasTag('spirit') && !e.target.hasTag('element') && !e.target.hasTag('playerAction') || !e.target.hitArea.type=='line') {
                const dx = e.target.hitArea.cx - this.hitArea.cx;
                const dy = e.target.hitArea.cy - this.hitArea.cy;
                if( dx > 0 && Math.abs(dx) > this.hitArea.width/2 ) { this.x -= this.speed; }
                if( dx < 0 && Math.abs(dx) > this.hitArea.width/2 ) { this.x += this.speed; }
                if( dy > 0 && Math.abs(dy) > this.hitArea.height/2 ) { this.y -= this.speed; }
                if( dy < 0 && Math.abs(dy) > this.hitArea.height/2 ) { this.y += this.speed; }
            }

            //ここから追加
            if(e.target.hitArea.type=='line') {
                const lineAB = e.target.hitArea; //ライン壁を定義
                const delta = lineAB.crossAB(this.hitArea.cx, this.hitArea.cy); // このアクターは、ラインのどちら側に居るか?

                let boundVect; //法線ベクトル(衝突時の反動の方向)を定義する文字列。
                if(delta > 0) { boundVect = lineAB.boundVect; } //deltaが正のときラインの法線ベクトルを正でとる。
                else { boundVect = lineAB.boundVect_; } //deltaが負のとき、ラインの法線ベクトルを負でとる。

                this.x += this.speed * boundVect.dx; //X軸の反動の大きさを反映
                this.y += this.speed * boundVect.dy; //Y軸の反動の大きさを反映
            }
            //ここまで
        });
プレイヤーの衝突判定時に、ぶつかった相手が'line'なら、衝突の反動ベクトル分だけx,yを移動させる記述をしました。この部分です。
            //ここから追加
            if(e.target.hitArea.type=='line') {
                const lineAB = e.target.hitArea; //ライン壁を定義
                const delta = lineAB.crossAB(this.hitArea.cx, this.hitArea.cy); // このアクターは、ラインのどちら側に居るか?

                let boundVect; //法線ベクトル(衝突時の反動の方向)を定義する文字列。
                if(delta > 0) { boundVect = lineAB.boundVect; } //deltaが正のときラインの法線ベクトルを正でとる。
                else { boundVect = lineAB.boundVect_; } //deltaが負のとき、ラインの法線ベクトルを負でとる。

                this.x += this.speed * boundVect.dx; //X軸の反動の大きさを反映
                this.y += this.speed * boundVect.dy; //Y軸の反動の大きさを反映
            }
            //ここまで
デモを見てみると、ちゃんと機能しますね。

ライン描画
⇒ ライン描画の衝突デモを見る



試行錯誤して、ようやくうまくいきました。

ポイントはdeltaで定義した = lineAB.crossAB(this.hitArea.cx, this.hitArea.cy);の値です。 これは、線分の交差判定のところで、始点と終点の位置関係を調べたときに使った関数です。
この関数は、このラインを引いた直線のどちら側に、点(x, y)の座標が存在するか?というのが分かる式。

正の値ならアチラ側。負の値ならコチラがわ。 線分を隔てたアクターとの位置関係が分かります。 それによって、バウンスの方向がどちら向きかを判定してる。if(delta > 0)の部分がそうです。

あとは法線ベクトルを求める関数を使って、移動スピード分だけその方向にバウンスさせるだけです。



苦労して正解に辿り着いた、時間の方もなかなかですが、その間に面白い発見もあるもので、片側に一方通行の壁(というか段差?)の記述も発見しました。

ベクトルの向きを絶対値にすると、一方通行になる

                if(e.target.isFall === true || delta > 0) { boundVect = lineAB.boundVect; } //deltaが正のとき、あるいは一方通行のとき、ラインの法線ベクトルを正でとる。

deltaの値(プレイヤーがラインのどちら側に居るか?)に関わらず、法線ベクトルの向きを固定すると一方通行のラインになる。線分をどの方向から引っ張ってくるかで、ベクトルの正負は調整できます。
この衝突判定を使い分けるには、LineActorの記述に一つ要素を加えます。例えばisFallなんてどうかな?

LineActorの記述にisFall要素を加える


class LineActor extends Actor {
    constructor(x, y, dx, dy, isFall=false) {
        const hitArea = new LineCollider(x, y, x + dx, y + dy);
        super(x, y, hitArea, ['object']);
        this.dx = dx;
        this.dy = dy;
        this.isFall = isFall;
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.addEventListener('hit', (e) => this._color = 'rgba(255, 0, 0, 0.9)' );
    }
new LineActor作成時、5つめの要素にtrueを加えてあげると、一方通行にする設定とします。
あとは、playerクラスの衝突判定時に振り分け設定をば。追記。

Playerクラス ラインとの衝突判定時の関数


            //ここから追加
            if(e.target.hitArea.type=='line') {
                const lineAB = e.target.hitArea; //ライン壁を定義
                const delta = lineAB.crossAB(this.hitArea.cx, this.hitArea.cy); // このアクターは、ラインのどちら側に居るか?

                let boundVect; //法線ベクトル(衝突時の反動の方向)を定義する文字列。
                if(e.target.isFall === true || delta > 0) { boundVect = lineAB.boundVect; } //deltaが正のとき、あるいは一方通行のとき、ラインの法線ベクトルを正でとる。
                else { boundVect = lineAB.boundVect_; } //deltaが負のとき、ラインの法線ベクトルを負でとる。

                this.x += this.speed * boundVect.dx; //X軸の反動の大きさを反映
                this.y += this.speed * boundVect.dy; //Y軸の反動の大きさを反映
            }
            //ここまで

プレイヤークラスはこんなもんか。
最後に、シーンにラインアクターを追加する記述を変更して...

メインシーンの追記を修正


  const line2 = new LineActor(20, 150, 240, 40); //左のライン
     this.add(line2);
  const line3 = new LineActor(350, 70, 0, 220, true); //右のラインを一方通行に
     this.add(line3);
    }
}

右のラインを一方通行に修正しました。


ライン描画
⇒ ライン描画の衝突デモを見る



ラインの衝突判定、無事に完了しました。
これが何に使えるかというと、きっと色々できます。

LineActorについて追記:古都さんのアドバイスより

後日、どうしてもnew LineActor(x,y,x2,y2)と2点の座標でラインを記述したかったので、古都さんにアドバイスを伺いました。

ベクトル表記も普通にありだと思います。そのままでもいいと思いますよ。

仮にx2,y2を導入するならx,yとは独立に動くべきなのではと思います。 連動して動くと線の長さを調整したいだけでもnewしないといけなくなりますし。
スライドさせたりしたいなら、Actor自体に空のtranslate(dx,dy)メソッド(※英語で平行移動はtranslate)を実装して 継承先のLineActorのtranslate(dx,dy)でx,y,x2,y2全部にdx,dyを足すようにすればいいのではないかなと。 これなら図形の種類が増えてもtranslateをそれぞれのActorで実装するだけで済みますし、 ややこしいgetとsetの処理を追う必要も無くなります。

このあたりは答えはなく自由なんで、 「こんなの試したらうまくいった」「これを試したけど全然ダメだった!」 みたいなのも全部記事に書いておくと、読む人が喜ぶかもしれないです。

なるほど...(' '*)!!!
ありがとうございます><・・・


つまり、xとx2を連動させる必要はない。
代わりに平行移動するとき専用の命令を描いて、平行移動の時だけ2つの点座標を一緒に移動させればいい。 すごい.......そちらの方がスマートだし応用効きます。


早速継承元のActorクラスに、メソッドを追加してみます。
    translate(dx,dy) { this.x+= dx; this.y+= dy; } //アクターを並行移動させる(背景スクロールと合わせて使う予定)

engine.js ⇒ Actorクラスの下に追記


class Actor extends EventDispatcher {//EventDispatcherイベント発生関数を持つActorクラスの定義
    constructor(x, y, hitArea, tags = []) {
        super();
        this.hitArea = hitArea;
        this._hitAreaOffsetX = hitArea.x;
        this._hitAreaOffsetY = hitArea.y;
        this.tags = tags;

        this.x = x;
        this.y = y;
    }
    
    update(gameInfo, input) {}//動く仕組みを作る

    render(target) {}//...線画処理

    hasTag(tagName) {//タグは当たり判定などのときに使います
        return this.tags.includes(tagName);
    }

    spawnActor(actor) {//他のActorを発生させるときに使用
        this.dispatchEvent('spawnactor', new GameEvent(actor));
    }

    destroy() {//自身を破壊する、しかし関数で自身を消すことはできないので、イベントだけ発生させてより上位のクラスに実際の処理を任せる
        this.dispatchEvent('destroy', new GameEvent(this));
    }
    
    get x() { return this._x; }//x座標を読み込む関数
    set x(value) {//x座標にvalueを代入する関数
        this._x = value;
        this.hitArea.x = value + this._hitAreaOffsetX;
    }

    get y() { return this._y; }//y座標を読み込む関数
    set y(value) {//y座標にvalueを代入する関数
        this._y = value;
        this.hitArea.y = value + this._hitAreaOffsetY;
    }
    //ここから追記
    translate(dx,dy) { this.x+= dx; this.y+= dy; } //アクターを並行移動させる(背景スクロールなどと合わせて使う予定)
}

合わせて、LineActorクラスにもtranslate(dx,dy)を実装し、このように書き換えます。
    translate(dx,dy) { this.x+= dx; this.x2+= dx; this.y+= dy; this.y2+= dy; } //アクターを並行移動させる(背景スクロールと合わせて使う予定)

そしてクラスをnew LineActor(x, y, x2, y2)の書式に変更するよ、以下のように。

danmaku.js ⇒ LineActorクラスを修正


class LineActor extends Actor {
    constructor(x, y, x2, y2, isFall=false) {
        const hitArea = new LineCollider(x, y, x2, y2);
        super(x, y, hitArea, ['object']);
        this.x2 = x2;
        this.y2 = y2;
        this.isFall = isFall;
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.addEventListener('hit', (e) => this._color = 'rgba(255, 0, 0, 0.9)' );
    }

    get x() { return this._x; }
    set x(value) { this._x = value; }//x座標にvalueを代入する関数をActorから上書き
    get y() { return this._y; }
    set y(value) { this._y = value; }//y座標にvalueを代入する関数をActorから上書き

    translate(dx,dy) { this.x+= dx; this.x2+= dx; this.y+= dy; this.y2+= dy; } //アクターを並行移動させる(背景スクロールと合わせて使う予定)

    update(gameInfo, input) {
        this._color = 'rgba(255, 255, 255, 0.9)';
    }
    render(target) {
        const context = target.getContext('2d');
        context.strokeStyle = this._color;
        context.beginPath(); //描画位置の初期化
        context.moveTo(this.x, this.y); //ラインの始点座標
        context.lineTo(this.x2, this.y2); //ラインの終点座標
        context.stroke(); //線を引く
    }
}

どんなふうになったか確認です。
ライン描画
⇒ ライン描画のデモを見る



お、ラインの傾きが変わってる! 位置がけっこうな嫌がらせ具合です。

メインシーンのnew LineActorの数値


  const line2 = new LineActor(20, 150,   240, 40); //左のライン
     this.add(line2);
  const line3 = new LineActor(350, 70,   0, 220,   true); //右のラインを一方通行に
     this.add(line3);
    }
}
数値は変えてません。つまり、3番目と4番目がベクトルからx2,y2の表記になったということです! 素晴らしい、古都さんありがとうございます><
確かにラインだとベクトル表記にも分かりやすい部分があるかもしれません、でも私のやりたいことにはこちらの表記が合ってる感じもする。

LineActor内に追記

translate(dx,dy)も上手く作動するかどうか試してみよう。

    update(gameInfo, input) {
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.translate(1,1);
    }
ライン描画
⇒ ライン描画のデモを見る



あかん、hitAreaが置いてきぼりや...(' '*);;
set x()にてhitArea.xとthis.xの連動を切ってしまってたのが原因、やはりthis.xとthis.hitArea.xという組み合わせは各々連結させねばなるまい。


    get x() { return this._x; } set x(value) { this._x = value; this.hitArea.x = value; }//x座標に値を代入する関数をActorから上書き
    get y() { return this._y; } set y(value) { this._y = value; this.hitArea.y = value; }//y座標に値を代入する関数をActorから上書き
    get x2() { return this._x2; } set x2(value) { this._x2 = value; this.hitArea.x2 = value; }//x2座標に値を代入するときhitArea.x2も上書き
    get y2() { return this._y2; } set y2(value) { this._y2 = value; this.hitArea.y2 = value; }//y2座標に値を代入するときhitArea.y2も上書き

    translate(dx,dy) { this.x+= dx; this.x2+= dx; this.y+= dy; this.y2+= dy; } //アクターを並行移動させる(背景スクロールと合わせて使う予定)
ライン描画
⇒ 移動するライン描画の当たり判定を見る



こうか。上手く行った。移動するラインは強引にすり抜けられないこともない。
でも、たぶんtranslate(dx,dy)は、背景スクロールの平行移動にしか使わない予定だから良いかな。
ありがとう、助かりました!!


やっと背景スクロールに、移れる。。かな???
いよいよ本格的な取り組みになってまいりました。


【目次】
  1. JavaScriptでゲーム作り「1:基礎編」
  2. JavaScriptでゲーム作り「2:プレイヤーの歩かせ方」
  3. JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
  4. JavaScriptでゲーム作り「4:NPCと会話する」
  5. JavaScriptでゲーム作り「5:当たり判定を最適化する」
  6. JavaScriptでゲーム作り「6:線分の当たり判定を実装する」
  7. JavaScriptでゲーム作り「7:フィールドの背景スクロール」
  8. JavaScriptでゲーム作り「8:プログラムの最適化、高速化、コード整理」
  9. JavaScriptでゲーム作り「9:タッチ・マウスイベント入力」
  10. JavaScriptでゲーム作り「10:法線ベクトルと衝突時のバウンス判定」
  11. JavaScriptでゲーム作り「10EX:2次曲線の当たり判定とバウンス処理」

すぺしゃるさんくす

https://sbfl.net/
古都さん
いつも勉強になってます。たぶん古都さんのフレームワークがなければ何もできなかった。


プレイヤーキャラ
ぴぽやさん、PlayerとNPCの素材を使わせて頂きました。ありがとうです!




今回の差分は、engine.jsの当たり判定部分の拡張。
danmaku.jsに記述したプレイヤークラスのhit判定時の動作
ラインアクター実装の3つです。

engine.js Collider〜修正後のコード


class Collider { //当たり判定に使うクラスの元
  constructor(type, x, y, isObj = false) {
    this._type = type;
    this.x = x;
    this.y = y;
    this.isObj = isObj; //背景オブジェクト同士の判定に利用
  }
  get type() { return this._type; }
}

class LineCollider extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x2, y2, isObj) {
    super('line', x, y, isObj);
    this.x2 = x2;
    this.y2 = y2;
  }
  get dx() { return this.x2 - this.x; } // x軸の移動方向と距離を求める、ベクトル成分のxになる
  get dy() { return this.y2 - this.y; } // y軸の移動方向と距離を求める、ベクトル成分のyになる
  get length() { return Math.sqrt( this.dx * this.dx + this.dy * this.dy ); } // 線分の長さを求める(三平方の定理より)
  get length2() { return this.dx * this.dx + this.dy * this.dy; } // 線分の長さの2乗を求める(2点間の距離を比較するときに使う...平方計算を省略して高速化)

  // 直線の式 dy*(X - x) = dx*(Y - y); // (Xはxとx2の間、Yはyとy2の間)
  // dy*X - dx*Y - dy*x + dx*y = 0; これが直線の式の一般形。

  // ここから、ラインの法線ベクトル(これに垂直なベクトル)は(dy, -dx)というのが求まる。
  // 法線ベクトルを単位ベクトルに置き換えた値(その長さで割る)を、衝突判定後のバウンス処理で利用できる。
  get boundVect() { return { x:this.dy / this.length,  y:- this.dx / this.length}; }

  innerP(other) { return this.dx * other.dx + this.dy * other.dy; } //他のベクトル線分との内積
  crossP(other) { return this.dx * other.dy - other.dx * this.dy; } //他のベクトル線分との外積
  crossAB(x, y) { return this.dx * (this.y - y) - this.dy * (this.x - x); } //他の点との位置関係...0なら線分の直線上に点が重なる

  get top() { return this.y < this.y2 ? this.y : this.y2; }  // 四分木の空間登録で使う
  get bottom() { return this.y > this.y2 ? this.y : this.y2; } // 同上
  get left() { return this.x < this.x2 ? this.x : this.x2; } // 同上
  get right() { return this.x > this.x2 ? this.x : this.x2; } // 同上
}

class RectangleCollider extends Collider { //当たり判定を四角形で使うクラス
  constructor(x, y, width, height, isObj) {
    super('rectangle', x, y, isObj);
    this.width = width;
    this.height = height;
  }
  get cx() { return this.x + this.width/2; } //矩形の中心のX座標
  get cy() { return this.y + this.height/2; } //矩形の中心のY座標
  get top() { return this.y; }
  get bottom() { return this.y + this.height; }
  get left() { return this.x; }
  get right() { return this.x + this.width; }
}

class CircleCollider extends Collider { //当たり判定を円形で使うクラス
  constructor(x, y, radius, isObj) { //中心点(x,y)の座標と、半径radiusを持つ円の定義
    super('circle', x, y, isObj);
    this.radius = radius;
  }
  get cx() { return this.x; } //円の中心のX座標
  get cy() { return this.y; } //円の中心のY座標
  
  get top() { return this.y - this.radius; } // 四分木の空間登録で使う
  get bottom() { return this.y + this.radius; } // 同上
  get left() { return this.x - this.radius; } // 同上
  get right() { return this.x + this.radius; } // 同上
}

class CollisionDetector {
  detectCollision(c1, c2) {
   // if(c1.isObj && c2.isObj) { return false; } //背景オブジェ同士なら接触しない
    if(c1.type == 'rectangle' && c2.type=='rectangle') { return this.deRectRect(c1, c2); } //矩形なら、矩形同士の衝突判定式を
    if(c1.type == 'rectangle' && c2.type=='circle') { return this.deRectCircle(c1, c2); } //矩形と円同士の衝突判定式を
    if(c1.type == 'circle' && c2.type=='rectangle') { return this.deRectCircle(c2, c1); } //円と矩形同士の衝突判定式を
    if(c1.type == 'circle' && c2.type=='circle') { return this.deCircleCircle(c1, c2); } //円なら、円同士の衝突判定式を
    if(c1.type == 'rectangle' && c2.type=='line') { return this.deRectLine(c1, c2); } //矩形と線分同士の衝突判定式を
    if(c1.type == 'line' && c2.type=='rectangle') { return this.deRectLine(c2, c1); } //線分と矩形同士の衝突判定式を
    if(c1.type == 'circle' && c2.type=='line') { return this.deCircleLine(c1, c2); } //円と線分同士の衝突判定式を
    if(c1.type == 'line' && c2.type=='circle') { return this.deCircleLine(c2, c1); } //線分と円同士の衝突判定式を

    return false;
  }
  
  deRectRect(rect1, rect2) { //矩形同士の衝突判定式
    const horizontal = (rect2.left < rect1.right) && (rect1.left < rect2.right); //水平方向の距離
    const vertical = (rect2.top < rect1.bottom) && (rect1.top < rect2.bottom); //垂直方向の距離
    return (horizontal && vertical); //両方の条件が一致すれば接触判定
  }
  deRectPoint(rect, x, y) { //矩形と点(x, y)の衝突判定式
    const horizontal = (rect.left < x) && (x < rect.right); //水平方向の距離
    const vertical = (rect.top < y) && (y < rect.bottom); //垂直方向の距離
    return (horizontal && vertical); //両方の条件が一致すれば接触判定
  }
  deCircleCircle(circle1, circle2) { //円同士の衝突判定式、中心と中心の距離、xの差、yの差を用いた三平方の定理で求める
    const dx = circle1.x - circle2.x; //水平方向の距離
    const dy = circle1.y - circle2.y; //鉛直方向の距離
    const dr = circle1.radius + circle2.radius; //斜辺の長さ = 半径の和
    return ( dr*dr > dx*dx + dy*dy ); //斜辺の累乗 > 水平距離の累乗 + 垂直距離の累乗 なら、接触判定trueで返す
  }
  deCirclePoint(circle, x, y) { //円と点(x, y)の衝突判定式、円の半径、xの差、yの差を用いた三平方の定理で求める
    const dx = circle.x - x; //水平方向の距離
    const dy = circle.y - y; //鉛直方向の距離
    const dr = circle.radius; //斜辺の長さ = 半径
    return ( dr*dr > dx*dx + dy*dy ); //斜辺の累乗 > 水平距離の累乗 + 垂直距離の累乗 なら、接触判定trueで返す
  }
  deRectCircle(rect, circle) { //円と矩形の衝突判定式
    const leftR = rect.left - circle.radius; //x軸左の境界(矩形の左端から、円の半径分さらに左のx座標)
    const topR = rect.top - circle.radius; //y軸トップの境界(矩形のトップから、円の半径分さらに上のy座標)
    const widthR = rect.width + circle.radius*2; //矩形の幅と円の直径を足した値
    const heightR = rect.height + circle.radius*2; //矩形の高さと円の直径を足した値

    const bigRect = new RectangleCollider (leftR, topR, widthR, heightR);
    if ( !this.deRectPoint(bigRect, circle.x, circle.y) ) { return false; } //大きな矩形と円の中心点で判定。当たってないならfalseを返す。本来は必要ないが、判定高速化のため追記している。

    const wideRect = new RectangleCollider (leftR, rect.top, widthR, rect.height); //円の半径分、横幅を増やした矩形
    const heightRect = new RectangleCollider (rect.left, topR, rect.width, heightR); //円の半径分、縦幅を増やした矩形
    if ( this.deRectPoint(wideRect, circle.x, circle.y) || this.deRectPoint(heightRect, circle.x, circle.y) ) {return true;} //それぞれの矩形と円の中心点で判定

    if (this.deCirclePoint( circle, rect.left, rect.top) || this.deCirclePoint( circle, rect.right, rect.top) ||
        this.deCirclePoint( circle, rect.left, rect.bottom) || this.deCirclePoint( circle, rect.right, rect.bottom) )  
       {return true;} //矩形の四隅の点と、円との衝突判定

    else {return false;} //3つとも判定が無ければ当たってない。
  }

  deLineLine(lineA, lineB) { //線分と線分の衝突判定式、線分同士は難しいので、直線の傾きと線分でクロス判定する。
    if (lineA.crossAB(lineB.x, lineB.y) * lineA.crossAB(lineB.x2, lineB.y2) > 0) {return false;} //始点と終点の位置関係で、正負が分かれる。
    if (lineB.crossAB(lineA.x, lineA.y) * lineB.crossAB(lineA.x2, lineA.y2) > 0) {return false;} //始点と終点が同じ側のときは正の値になる、当たってない。
    return true; // 両方交差してるなら、当たってます!
  }

  deRectLine(rect, line) { //矩形と線分の衝突判定式、矩形の各辺の線分と、線分の衝突判定を呼び出す。全てfalseなら当たってない。
    if( this.deRectPoint(rect, line.x, line.y) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.left, rect.top, rect.right, rect.top), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.right, rect.top, rect.right, rect.bottom), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.right, rect.bottom, rect.left, rect.bottom), line) ) { return true; }
    if( this.deLineLine(new LineCollider(rect.left, rect.bottom, rect.left, rect.top), line) ) { return true; }
    return false;
  }

  deCircleLine(circleP, lineAB) { //円と線分の衝突判定式
    const lineAP = new LineCollider(lineAB.x, lineAB.y, circleP.x, circleP.y) //線分の頂点Aから、円の中心Pへのラインを引く1
    const lineBP = new LineCollider(lineAB.x2, lineAB.y2, circleP.x, circleP.y) //線分の頂点Bから、円の中心Pへのラインを引く2

/*  コメント内の計算は、平方根を求める式を含むので、ちょっと重い。理論的には理解しやすい数式。
    //点Pを中心とする円から、最短距離となる線AB上の点をXとする。
    const innerAX = lineAB.innerP(lineAP) / lineAB.length; //ベクトルAB上にある[ベクトルAX]の長さをベクトルAPとの「内積」で求める
    const crossPX = lineAB.crossP(lineAP) / lineAB.length; //ベクトルABとベクトルAPから[ベクトルPX]の長さを「外積」で求める

    let sDistance; //最短距離の定数
    if( innerAX < 0  ){ sDistance = lineAP.length;} //innerAXが負のとき、lineAPの長さが、円とラインの最短距離。
    else if( innerAX > lineAB.length ){ sDistance = lineBP.length;} // innerAXがlineABの長さより大きいとき、lineBPの長さが、円とラインの最短距離。
    else { sDistance = Math.abs(crossPX); } //それ以外のとき、crossPXの絶対値が円とラインの最短距離

    if( sDistance < circleP.radius ) { return true; } //最短距離が円の半径よりも小さい場合は、当たり
    else {return false;}
*/
    // ここから円の半径を2乗した値で最短距離を比較演算する判定式。平方根を求めないので、プログラム計算の負担が少ない。
    const innerAX = lineAB.innerP(lineAP); //ベクトルAB上にある[ベクトルAX]の長さ×[ベクトルAB]の長さをベクトルAPとの「内積」で求める

    if(innerAX <= 0){ // innerAXが負のとき、lineAPの長さが、円とラインの最短距離。
        return lineAP.length2 < circleP.radius*circleP.radius ? true : false; //最短距離の2乗が円の半径の2乗よりも小さい場合は、当たり
    }
    else if(innerAX >= lineAB.length2){ // innerAXがlineABの長さの2乗より大きい、つまりlineAXの長さがlineABより大きいとき、lineBPの長さが円とラインの最短距離。
        // ↑ (lineAX.length*lineAB.length > lineAB.length*lineAB.length)のとき、
        return lineBP.length2 < circleP.radius*circleP.radius ? true : false; //最短距離の2乗が円の半径の2乗よりも小さい場合は、当たり
    }
    else { // innerAXがlineAB間に収まるとき Math.abs(crossPX / lineAB.length)が、円とラインの最短距離
        const crossPX = lineAB.crossP(lineAP); //ベクトルABとベクトルAPから[ベクトルPX]の長さ×[ベクトルAB]の長さを「外積」で求める
        return Math.abs(crossPX) < circleP.radius * lineAB.length ? true : false; //crossPXの絶対値が、円の半径*lineAB.lengthよりも小さい場合は、当たり
    }
  }
}

danmaku.js Playerクラス修正後のコード


class Player extends SpriteActor {
    constructor(x, y) {
        const sprite = new Sprite(assets.get('player'), new Rectangle(32, 96, 32, 32));
        const hitArea = new RectangleCollider(6, 4, 20, 28);
        super(x, y, sprite, hitArea, ['player']);
        
        this.speed = 2;
        this._velocityX = 0;
        this._velocityY = 0;
        this.walkCount = 0;
        this._interval = 5;
        this._timeCount = 0;
        this._dir = 90 * Math.PI/180; //プレイヤーの向き...角度(ラジアン)の単位。。角度°にπ/180をかけた値
        this._dirGo = 90 * Math.PI/180; //進む方角...初期は上向き
        this.isActive = true;

        this.addEventListener('hit', (e) => {
            if(!e.target.hasTag('spirit') && !e.target.hasTag('element') && !e.target.hasTag('playerAction') || !e.target.hitArea.type=='line') {
                const dx = e.target.hitArea.cx - this.hitArea.cx; //正の値ならプレイヤーが左側
                const dy = e.target.hitArea.cy - this.hitArea.cy; //正の値ならプレイヤーが上側
                if( dx > 0 && Math.abs(dx) > this.hitArea.width/2 ) { this.x -= this.speed; }
                if( dx < 0 && Math.abs(dx) > this.hitArea.width/2 ) { this.x += this.speed; }
                if( dy > 0 && Math.abs(dy) > this.hitArea.height/2 ) { this.y -= this.speed; }
                if( dy < 0 && Math.abs(dy) > this.hitArea.height/2 ) { this.y += this.speed; }
            }
            if(e.target.hitArea.type=='line') {
                const lineAB = e.target.hitArea; //ラインの壁
                const delta = lineAB.crossAB(this.hitArea.cx, this.hitArea.cy); // 自分の中心点とラインとの位置関係を調べる。
                if(delta > 0 || e.target.isFall) { //ラインに対する自分の中心点が正の位置にあるなら、ベクトルを正の値でとる
                    this.x += this.speed * lineAB.boundVect.x;
                    this.y += this.speed * lineAB.boundVect.y;
                }
                else if(delta < 0) { //ラインに対する自分の中心点が負の位置にあるなら、ベクトルを負の値でとる
                    this.x -= this.speed * lineAB.boundVect.x;
                    this.y -= this.speed * lineAB.boundVect.y;
                }
            }
        });
    }
    
    update(gameInfo, input) {
        this._velocityX = 0;
        this._velocityY = 0;
        const rect = this.sprite.rectangle;

        //矢印キーを押しただけの時に、プレーヤーの向きを変える。
        if(!input.getKey(' ')) {
            if(input.getKey('ArrowUp')) { rect.y = 96; this._dir = 90 * Math.PI/180;} //上
            if(input.getKey('ArrowRight')) { rect.y = 64; this._dir = 0 * Math.PI/180;} //右
            if(input.getKey('ArrowDown')) { rect.y = 0; this._dir = -90 * Math.PI/180;} //下
            if(input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 180 * Math.PI/180; } //左
            if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = 45 * Math.PI/180;} //右上
            if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { rect.y = 64; this._dir = -45 * Math.PI/180;} //右下
            if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = -135 * Math.PI/180;} //左下
            if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { rect.y = 32; this._dir = 135 * Math.PI/180;} //左上
        }

        //進む方角の設定
        if(input.getKey('ArrowUp')) { this._dirGo = 90 * Math.PI/180;} //上
        if(input.getKey('ArrowRight')) { this._dirGo = 0 * Math.PI/180;} //右
        if(input.getKey('ArrowDown')) { this._dirGo = -90 * Math.PI/180;} //下
        if(input.getKey('ArrowLeft')) { this._dirGo = 180 * Math.PI/180; } //左
        if(input.getKey('ArrowUp') && input.getKey('ArrowRight')) { this._dirGo = 45 * Math.PI/180;} //右上
        if(input.getKey('ArrowDown') && input.getKey('ArrowRight')) { this._dirGo = -45 * Math.PI/180;} //右下
        if(input.getKey('ArrowDown') && input.getKey('ArrowLeft')) { this._dirGo = -135 * Math.PI/180;} //左下
        if(input.getKey('ArrowUp') && input.getKey('ArrowLeft')) { this._dirGo = 135 * Math.PI/180;} //左上

        //歩行カウントが一定以上に達した時、歩くモーションを変化
        if(this.walkCount > 0) { rect.x = 64; }
        if(this.walkCount > 16) { rect.x = 32; }
        if(this.walkCount > 32) { rect.x = 0; }
        if(this.walkCount > 48) { rect.x = 32; this.walkCount = -15; }
        
        //立ち止まった時に直立姿勢
        if(input.getKeyUp('ArrowUp') || input.getKeyUp('ArrowDown') || input.getKeyUp('ArrowRight') || input.getKeyUp('ArrowLeft')) {rect.x = 32; this.walkCount = 0;}

    if(this.isActive) {//isActive = trueのときのみ、移動やアクションができる
        //矢印キーを押してる間、歩行カウントをすすめて、進む方角に応じた移動距離を計算
        if(input.getKey('ArrowUp') || input.getKey('ArrowDown') || input.getKey('ArrowRight') || input.getKey('ArrowLeft')) {
            this.walkCount += this.speed;
            this._velocityX += this.speed * Math.cos(this._dirGo);
            this._velocityY += -this.speed * Math.sin(this._dirGo);
        }

        //移動を反映させる
        this.x += this._velocityX;
        this.y += this._velocityY;

        const boundWidth = gameInfo.screenRectangle.width - this.width;
        const boundHeight = gameInfo.screenRectangle.height - this.height;
        const bound = new Rectangle(this.width, this.height, boundWidth, boundHeight);
        
        if(this.isOutOfBounds(bound)) {
            this.x -= this._velocityX;
            this.y -= this._velocityY;
        }
        
        //スペースキーでアクション発生
        if(input.getKeyDown(' ')) {
            const actorX = this.x + rect.width * Math.cos(this._dir);
            const actorY = this.y - rect.height * Math.sin(this._dir);
            this.spawnActor( new PlayerAction(actorX, actorY, this._dir) );
        }
    }//if(isActive)
    }
}

danmaku.js 追加したLineActorのコード


class LineActor extends Actor {
    constructor(x, y, x2, y2, isFall=false) {
        const hitArea = new LineCollider(x, y, x2, y2);
        super(x, y, hitArea, ['object']);
        this.x2 = x2;
        this.y2 = y2;
        this.isFall = isFall;
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.addEventListener('hit', (e) => this._color = 'rgba(255, 0, 0, 0.9)' );
    }

    get x() { return this._x; } set x(value) { this._x = value; this.hitArea.x = value; }//x座標に値を代入する関数をActorから上書き
    get y() { return this._y; } set y(value) { this._y = value; this.hitArea.y = value; }//y座標に値を代入する関数をActorから上書き
    get x2() { return this._x2; } set x2(value) { this._x2 = value; this.hitArea.x2 = value; }//x2座標に値を代入するときhitArea.x2も上書き
    get y2() { return this._y2; } set y2(value) { this._y2 = value; this.hitArea.y2 = value; }//y2座標に値を代入するときhitArea.y2も上書き

    translate(dx,dy) { this.x+= dx; this.x2+= dx; this.y+= dy; this.y2+= dy; } //アクターを並行移動させる(背景スクロールと合わせて使う予定)

    update(gameInfo, input) {
        this._color = 'rgba(255, 255, 255, 0.9)';
        this.translate(1,1);
    }
    render(target) {
        const context = target.getContext('2d');
        context.strokeStyle = this._color;
        context.beginPath(); //描画位置の初期化
        context.moveTo(this.x, this.y); //ラインの始点座標
        context.lineTo(this.x2, this.y2); //ラインの終点座標
        context.stroke(); //線を引く
    }
}