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

【目次】
  1. JavaScriptでゲーム作り「1:基礎編」
  2. JavaScriptでゲーム作り「2:プレイヤーの歩かせ方」
  3. JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
  4. JavaScriptでゲーム作り「4:NPCと会話する」
  5. JavaScriptでゲーム作り「5:当たり判定を最適化する」
  6. JavaScriptでゲーム作り「6:線分の当たり判定を実装する」
  7. JavaScriptでゲーム作り「7:フィールドの背景スクロール」

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

(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(other) { return this.dx * (this.y - other.y) - this.dy * (this.x - other.x); } //クロス判定1_他の線分(始点)との位置関係
  crossAB2(other){ return this.dx * (this.y - other.y2) - this.dy * (this.x - other.x2); }//クロス判定2_他の線分(終点)との位置関係

この公式を、基本となる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(other) { return this.dx * (this.y - other.y) - this.dy * (this.x - other.x); } //クロス判定1_他の線分(始点)との位置関係
  crossAB2(other){ return this.dx * (this.y - other.y2) - this.dy * (this.x - other.x2); }//クロス判定2_他の線分(終点)との位置関係
  //ここまで
//続く

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

線分の衝突判定式


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

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

線分と矩形の衝突判定式


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) * lineA.crossAB2(lineB) > 0) {return false;}
    if (lineB.crossAB(lineA) * lineB.crossAB2(lineA) > 0) {return false;}

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

  //↓ ここから追加
  deRectLine(rect, line) { //矩形と線分の衝突判定式、矩形の各辺の線分と、線分の衝突判定を呼び出す。全てfalseなら当たってない。
    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(other) { return this.dx * (this.y - other.y) - this.dy * (this.x - other.x); } //クロス判定1_他の線分(始点)との位置関係
  crossAB2(other){ return this.dx * (this.y - other.y2) - this.dy * (this.x - other.x2); }//クロス判定2_他の線分(終点)との位置関係

前回から加えた公式は、この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 ◕。)ノ

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

ラインを描画して、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}で表記したほうが分かりやすいでしょうけど難しいです。ベクトル表記で今はいきます。

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

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

さて、次にラインの壁に衝突した時の押し戻しについて考えてみます。 先ほどの点Pを中心とする円と、線分ABの衝突判定で使われた式が、ここでも役立ちます。

もう一度参考に ⇒ 円と線分の当たり判定を行うには

ラインに衝突するオブジェクトを円に見立てた時、線分AB上の衝突点Xから垂直に、オブジェクトの中心Pに向かうXPベクトルの向きを求めることが出来ます。 この向きが衝突時の反動ベクトルです。この向きが判ればあとは簡単。XPの単位ベクトルに、オブジェクトのthis.speedを乗算することで、X軸への押し戻し、Y軸への押し戻しの大きさがそれぞれ分かります。

では線分との衝突時、オブジェクトの中心から線分AB上の垂直となる点XをinnerP(内積)で求め、その点Xからオブジェクトの中心へと向かうXPベクトルを導き出してみましょう。 実際に動いているオブジェクト(プレイヤー側など)に、ライン衝突時の反動ベクトルを求める関数を記述します。

あ、その前に、キャラクターの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 lineAP = new LineCollider (lineAB.x, lineAB.y, this.hitArea.cx, this.hitArea.cy); //ラインの始点Aから自分の中心位置Pまでの線分
                const innerAX = lineAB.innerP(lineAP) / (lineAB.length); //線分上の始点Aから、衝突点Xまでの距離を測る=AXの長さ
                const pointX = {x:lineAB.x + lineAB.dx * innerAX/lineAB.length, y:lineAB.y + lineAB.dy * innerAX/lineAB.length}; //衝突点Xを求める。始点Aから線分ABの単位ベクトルとAXの長さ(innerAX)を掛けあわせた分だけ、xとyを移動した座標。
                const lineXP = new LineCollider (pointX.x, pointX.y, this.hitArea.cx, this.hitArea.cy); //衝突点Xから中心Pまでのベクトルを引く。(この矢印の向きが、押し戻す方向となる)
                this.x += this.speed * lineXP.dx/lineXP.length;
                this.y += this.speed * lineXP.dy/lineXP.length;
            }
            //ここまで
        });
プレイヤーの衝突判定時に、ぶつかった相手が'line'なら、衝突の反動ベクトル分だけx,yを移動させる記述をしました。この部分です。
            //ここから追加
            if(e.target.hitArea.type=='line') {
                const lineAB = e.target.hitArea; //ラインの壁
                const lineAP = new LineCollider (lineAB.x, lineAB.y, this.hitArea.cx, this.hitArea.cy); //ラインの始点Aから自分の中心位置Pまでの線分
                const innerAX = lineAB.innerP(lineAP) / (lineAB.length); //線分上の始点Aから、衝突点Xまでの距離を測る=AXの長さ
                const pointX = {x:lineAB.x + lineAB.dx * innerAX/lineAB.length, y:lineAB.y + lineAB.dy * innerAX/lineAB.length}; //衝突点Xを求める。始点Aから線分ABの単位ベクトルとAXの長さ(innerAX)を掛けあわせた分だけ、xとyを移動した座標。
                const lineXP = new LineCollider (pointX.x, pointX.y, this.hitArea.cx, this.hitArea.cy); //衝突点Xから中心Pまでのベクトルを引く。(この矢印の向きが、押し戻す方向となる)
                this.x += this.speed * lineXP.dx/lineXP.length;
                this.y += this.speed * lineXP.dy/lineXP.length;
            }
            //ここまで
ほぼほぼ、円と線分の衝突判定に用いた式を使いまわせます。 デモを見てみると、ちゃんと機能しますね。

プレイヤーキャラ
⇒ ライン描画の衝突デモを見る



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

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

                this.x += Math.abs(this.speed * lineXP.dx/lineXP.length);
                this.y += Math.abs(this.speed * lineXP.dy/lineXP.length);

最後のベクトルの向きを絶対値で指定すると一方通行のラインになる。線分をどの方向から引っ張ってくるかで、ベクトルの正負は調整できます。

この衝突判定を使い分けるには、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 lineAP = new LineCollider (lineAB.x, lineAB.y, this.hitArea.cx, this.hitArea.cy); //ラインの端から自分の中心位置までの線分
                const innerAX = lineAB.innerP(lineAP) / (lineAB.length);
                const pointX = {x:lineAB.x + lineAB.dx * innerAX/lineAB.length, y:lineAB.y + lineAB.dy * innerAX/lineAB.length};
                const lineXP = new LineCollider (pointX.x, pointX.y, this.hitArea.cx, this.hitArea.cy);
                if(! e.target.isFall ) { //ラインが一方通行かどうかで、ベクトルを絶対値で取るかどうか分ける
                    this.x += this.speed * lineXP.dx/lineXP.length;
                    this.y += this.speed * lineXP.dy/lineXP.length;
                } else {
                    this.x += Math.abs(this.speed * lineXP.dx/lineXP.length);
                    this.y += Math.abs(this.speed * lineXP.dy/lineXP.length);
                }
            }
            //ここまで

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

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


  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)は、背景スクロールの平行移動にしか使わない予定だから良いかな。
ありがとう、助かりました!!


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


すぺしゃるさんくす

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 ); } // 線分の長さを求める(三平方の定理より)

  innerP(other) { return this.dx * other.dx + this.dy * other.dy; } //他のベクトル線分との内積
  crossP(other) { return this.dx * other.dy - other.dx * this.dy; } //他のベクトル線分との外積

  crossAB(other) { return this.dx * (this.y - other.y) - this.dy * (this.x - other.x); } //クロス判定1_他の線分(始点)との位置関係
  crossAB2(other){ return this.dx * (this.y - other.y2) - this.dy * (this.x - other.x2); }//クロス判定2_他の線分(終点)との位置関係

  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); //両方の条件が一致すれば接触判定
  }  
  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で返す
  }

  deRectCircle(rect, circle) { //矩形と円の衝突判定式
    const topRC = rect.top - circle.radius; //y軸トップの境界(矩形のトップに、円の半径分を足す)
    const bottomRC = rect.bottom + circle.radius; //y軸ボトムの境界(矩形のボトムに、円の半径分を足す)
    const leftRC = rect.left - circle.radius; //x軸左の境界(矩形の左端から、円の半径分を足す)
    const rightRC = rect.right + circle.radius; //x軸右の境界(矩形の右端に、円の半径分を足す)
    if (topRC < circle.y && bottomRC > circle.y && rect.left < circle.x && rect.right > circle.x) {return true;} //矩形の上下境界の間に円の中心点yが重なり、かつ矩形のx軸間に中心点xがあるなら接触
    if (rect.top < circle.y && rect.bottom > circle.y && leftRC < circle.x && rightRC > circle.x) {return true;} //矩形の左右境界の間に円の中心点xが重なり、かつ矩形のy軸間に中心点yがあるなら接触

    const topRC2 = Math.pow( rect.top - circle.y ,2); //三平方の定理に使う top
    const bottomRC2 = Math.pow( rect.bottom - circle.y ,2); //三平方の定理に使う bottom
    const leftRC2 = Math.pow( rect.left - circle.x ,2); //三平方の定理に使う left
    const rightRC2 = Math.pow( rect.right - circle.x ,2); //三平方の定理に使う right
    const radius2 = circle.radius * circle.radius; //円の半径の累乗
    if ( topRC2 + leftRC2 < radius2 || bottomRC2 + leftRC2 < radius2 || topRC2 + rightRC2 < radius2 || bottomRC2 + rightRC2 < radius2 ) {return true;} //矩形の四隅の点と、円との衝突判定
    else {return false;} //3つとも判定が無ければ当たってない。
  }

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

  deRectLine(rect, line) { //矩形と線分の衝突判定式、矩形の各辺の線分と、線分の衝突判定を呼び出す。全てfalseなら当たってない。
    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;}
    else if( innerAX > lineAB.length ){ sDistance = lineBP.length;}
    else { sDistance = Math.abs(crossPX); }

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

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 lineAP = new LineCollider (lineAB.x, lineAB.y, this.hitArea.cx, this.hitArea.cy); //ラインの端から自分の中心位置までの線分
                const innerAX = lineAB.innerP(lineAP) / (lineAB.length);
                const pointX = {x:lineAB.x + lineAB.dx * innerAX/lineAB.length, y:lineAB.y + lineAB.dy * innerAX/lineAB.length};
                const lineXP = new LineCollider (pointX.x, pointX.y, this.hitArea.cx, this.hitArea.cy);
                if(! e.target.isFall ) { //ラインが一方通行かどうかで、ベクトルを絶対値で取るかどうか分ける
                    this.x += this.speed * lineXP.dx/lineXP.length;
                    this.y += this.speed * lineXP.dy/lineXP.length;
                } else {
                    this.x += Math.abs(this.speed * lineXP.dx/lineXP.length);
                    this.y += Math.abs(this.speed * lineXP.dy/lineXP.length);
                }
            }
        });
    }
    
    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(); //線を引く
    }
}