LoginSignup
2
2

More than 1 year has passed since last update.

JavaScriptでゲームを作る🎮 その3

Posted at

はじめに

続きをやっていきます。

今回は、当たり判定を処理することが主な目的になります。

今回の目次 🗒

  1. 敵を簡易的に動くようにする 🕹
  2. 弾が敵に当たるようにする 🚀
  3. 弾が当たった時にエフェクトを表示する 🐾
  4. 敵が攻撃するようにする 🔫

敵を簡易的に動くようにする 🕹

概要

敵が全く動かないのは寂しいので、とりあえず簡易的に動くようにしたいと思います。

敵の操作処理は、コントローラーの入力を自動で生成するようにし、それに応じて、機体の挙動はプレイヤーのものと全く同じにしたいです。

そのための準備としてひとまず、プレイヤー用の処理関数の引数にコントローラー入力用の変数を追加して、そちらから入力の判定を行うようにします。

また、キャラクタの色をデータとして持たせて、データから描画させるようにします。

let gameObjects = [
  {
    type: 'player',
    ...
    color: 'rgb(0, 0, 255)',
  },
  {
    type: 'enemy',
    ...
    color: 'rgb(150, 0, 0)',
  },
  ...
};

const functions = {
  ...
  player: (obj, input) => {
    input = input || gameInput;
    ...
    drawObject(
      ...
      obj.color,
      ...
    );

  }
};

記述省略していますが、playerの関数の中でgameInputからの入力を見ていたところは、inputからの入力を見るように変更しています。

これで準備ができたので、敵を動かす処理を作成します。

今回は、座標(250, 200)に向かって自動で向かうようなコントローラー入力を生成させます。目的地と現在地を比べて、ズレていればその方向への入力を生成するだけです。

あまり小刻みに動くと気持ち悪いので、x方向については6ピクセル以内に入っていれば到達していると判定することにしました。

入力値を生成したら、それをplayer関数に渡して処理を委譲するだけです。

const functions = {
  ...
  enemy: (obj) => {
    const input = {};

    const target_x = 250;
    const target_y = 200;

    const target_distance_x = Math.abs(target_x - obj.x);

    // x座標がズレていたら左右の入力を生成
    if (target_x < obj.x && target_distance_x > 6) {
      input.l_left = true;
      input.l_right = false;
    } else if (target_distance_x > 6) {
      input.l_left = false;
      input.l_right = true;
    }

    // y座標が足りてなければ上昇入力を生成
    if (target_y < obj.y) {
      input.l_top = true;
    } else {
      input.l_top = false;
    }

    return functions.player(obj, input);
  },
  ...
};

動作サンプル

  • 動作サンプル
  • 操作方法
    • 左スティック(WASDキー)で移動
    • 右スティック(カーソルキー)で方向転換
    • 左右で振り向き
    • 上下で角度変更

弾が敵に当たるようにする 🔫

概要

敵が動くようになったので、次は攻撃が当たるようにしていきたいと思います。

当たり判定の取り方は色々あるとは思うのですが、今回はシンプルに、直線同士の交差判定を利用して行っていきたいと思います。
キャラクタを囲むエリアを直線の組み合わせで用意し、そのそれぞれの直線と、弾の軌跡の直線との交差を判定することで、当たり判定を行います。

ということで、一旦直線同士の交差判定を行う関数を作成します。
ここでは、個人的にシンプルなやり方な気がした、ベクトルの外積を利用する方法で判定を行います。

引数から、オブジェクトの当たり判定エリアを構成する直線の配列を渡し、二つのオブジェクトが交差しているのか判定します。
直線が一つでも交差していれば、当たっているとみなします。

function hitCheck(linesA, linesB) {
  for (let lineA of linesA) {
    for (let lineB of linesB) {
      const vAx = lineA.ex - lineA.bx;
      const vAy = lineA.ey - lineA.by;
      const vBBx = lineB.bx - lineA.bx;
      const vBBy = lineB.by - lineA.by;
      const vBEx = lineB.ex - lineA.bx;
      const vBEy = lineB.ey - lineA.by;
      const vAvBc0 = vAx * vBBy - vAy * vBBx;
      const vAvBc1 = vAx * vBEy - vAy * vBEx;
      if (vAvBc0 * vAvBc1 > 0) continue;

      const vBx = lineB.ex - lineB.bx;
      const vBy = lineB.ey - lineB.by;
      const vABx = lineA.bx - lineB.bx;
      const vABy = lineA.by - lineB.by;
      const vAEx = lineA.ex - lineB.bx;
      const vAEy = lineA.ey - lineB.by;
      const vBvAc0 = vBx * vABy - vBy * vABx;
      const vBvAc1 = vBx * vAEy - vBy * vAEx;
      if (vBvAc0 * vBvAc1 > 0) continue;

      return true;
    }
  }

  return false;
}

直線同士の交差判定は

名称未設定-Page-1.drawio.png

  • 片側の直線(直線A)の始点から終点へ向かう線をベクトルXとする
  • その直線Aの始点から、もう一つの直線(直線B)の始点へ向かう線をベクトルYとする
  • ベクトルXとYの外積をとる(結果A)
    • YがXより左の方にあれば正の値、右にあれば負の値になる
  • 直線Aの始点から、直線Bの終点へ向かう線をベクトルZとする
  • ベクトルXとZの外積をとる(結果B)
    • ZがXより左の方にあれば正の値、右にあれば負の値になる
  • 直線Bの始点と終点が直線Aの左と右に分かれていれば、交差していると言える
    • その場合は結果Aと結果Bの符号が異なるはず
  • 結果Aと結果Bを掛けて、ゼロより小さければ交差している
    • 符号が異なれば掛けるとマイナスになるため

という手順で行っています。
また、それだけだと以下のような場合にも交差している判定になってしまうので、直線B側から直線Aに対しても同様の判定を再度行なっています。

名称未設定-Page-2.drawio.png

交差判定ができたので、これを利用して当たり判定処理を作成していきます。

まず、自分の弾が自分に当たったりしないように、属性情報をデータに追加します1

let gameObjects = [
  {
    type: 'player',
    layer: 0,
  },
  {
    type: 'enemy',
    layer: 1,
  },
  ...
];

const functions = {
  player: (obj, input) => {
    ...
      newObjects.push({
        type: 'bullet',
        ...
        layer: obj.layer,
      });
    ...
  },
  ...
};

当たり判定処理の本体は、以下になります。
ゲームオブジェクトの処理関数とは別に、当たり判定専用の処理関数を作成し、最後にまとめて判定処理を行なっています。

const hitCheckFunctions = {
  player: (obj, attack) => {
    // 相手の弾とだけ判定をとる
    if (attack.type !== 'bullet' || obj.layer === attack.layer) return;

    const DRAW_CENTER_HEIGHT = 10;
    const mirror = obj.backward ? -1 : 1;

    // キャラクタの当たり判定は長方形
    const charHitLines = [
      {
        bx: obj.x + rx(-5, -15, obj.r) * mirror,
        by: obj.y + ry(-5, -15, obj.r) - DRAW_CENTER_HEIGHT,
        ex: obj.x + rx(5, -15, obj.r) * mirror,
        ey: obj.y + ry(5, -15, obj.r) - DRAW_CENTER_HEIGHT,
      },
      {
        bx: obj.x + rx(5, -15, obj.r) * mirror,
        by: obj.y + ry(5, -15, obj.r) - DRAW_CENTER_HEIGHT,
        ex: obj.x + rx(5, 10, obj.r) * mirror,
        ey: obj.y + ry(5, 10, obj.r) - DRAW_CENTER_HEIGHT,
      },
      {
        bx: obj.x + rx(5, 10, obj.r) * mirror,
        by: obj.y + ry(5, 10, obj.r) - DRAW_CENTER_HEIGHT,
        ex: obj.x + rx(-5, 10, obj.r) * mirror,
        ey: obj.y + ry(-5, 10, obj.r) - DRAW_CENTER_HEIGHT,
      },
      {
        bx: obj.x + rx(-5, 10, obj.r) * mirror,
        by: obj.y + ry(-5, 10, obj.r) - DRAW_CENTER_HEIGHT,
        ex: obj.x + rx(-5, -15, obj.r) * mirror,
        ey: obj.y + ry(-5, -15, obj.r) - DRAW_CENTER_HEIGHT,
      },
    ];

    // 弾の当たり判定は線
    const bulletHitLines = [
      {
        bx: attack.x,
        by: attack.y,
        ex: attack.x - attack.ax,
        ey: attack.y - attack.ay,
      },
    ];

    if (hitCheck(charHitLines, bulletHitLines)) {
      // 当たっていたら相手を若干後ろに押す
      obj.ax += attack.ax / 10;
      obj.ay += attack.ay / 10;

      obj.hit = true;
      attack.hit = true;
    }
  },

  enemy: (obj, attack) => {
    hitCheckFunctions.player(obj, attack);
  },
};

...

function gameLoop() {
  ...
  // 攻撃判定
  gameObjects.forEach((defence) => {
    gameObjects.forEach((attack) => {
      if (defence === attack || !hitCheckFunctions[defence.type]) {
        // 自分自身とは判定をとらない
        return;
      }
      hitCheckFunctions[defence.type](defence, attack);
    });
  });
  ...
}

当たり判定処理を別に分けている理由は、ゲームオブジェクトの処理関数の中で当たり判定処理を行なってしまうと、先に処理したオブジェクト(配列の前の方にいるオブジェクト)が有利になったりする事態が起こるため、それを避けるための処置です。
オブジェクトの移動処理と当たり判定処理を分けることで、各オブジェクトを平等に扱うようにしています。

当たっていた場合は、当たったことをオブジェクトに通知するとともに、相手は少し後ろに押されるようにしました。

最後に、弾は当たっていた場合は消去するようにします。

const functions = {
  ...
  bullet: (obj) => {
    if (obj.hit) {
      return false;
    }
    ...
  }
  ...
}

動作サンプル

弾が当たった時にエフェクトを表示する 🐾

概要

若干画面が寂しいので、弾が当たった時にエフェクトを表示しようと思います。

まず、弾が当たった時に、キャラクタの色が変わるようにします。

const functions = {
  player: (obj, input) => {
    ...
    drawObject(
      ...
      obj.hit ? 'lightgray' : obj.color,
      ...
    );

    obj.hit = false;
    ...
  },
  ...
};

次に、弾が当たった時に瓦礫?を表示するようにします。
瓦礫用のゲームオブジェクトを作成し、画面に追加します2

const functions = {
  ...
  bullet: (obj) => {
    const mirror = obj.backward ? -1 : 1;
    if (obj.hit) {
      newObjects.push({
        type: 'debris',
        x: obj.x,
        y: obj.y,
        ax: rx(-5, -5 * Math.random(), obj.r) * mirror,
        ay: ry(-5, -5 * Math.random(), obj.r),
        lifeLimit: 10,
      });
      return false;
    }
    ...
  },
  ...
  debris: (obj) => {
    obj.ay += 1;

    obj.x += obj.ax;
    obj.y += obj.ay;

    if (obj.y > HORIZON_HEIGHT) {
      obj.y = HORIZON_HEIGHT - (obj.y - HORIZON_HEIGHT);
      obj.ay = -obj.ay * 0.5;
      obj.ax = obj.ax * 0.5;
    }

    ctx.fillStyle = '#aaa';
    ctx.fillRect(obj.x - 2, obj.y - 2, 4, 4);

    if (obj.lifeLimit-- < 0) {
      return false;
    }

    return true;
  },
};

瓦礫は地面に当たると若干跳ね返ります。

動作サンプル

敵が攻撃するようにする 🔫

概要

一方的にプレイヤーから攻撃できるだけなのもアレなので、一旦ランダムに敵も弾を発射するようにします。

const functions = {
  ...
  enemy: (obj) => {
    ...
    if (Math.random() < 0.1) {
      input.b = true;
    }
    ...
  },
  ...
};

動作サンプル

今回はここまでにしたいと思います。

ソースが若干見辛くなってきた(特に座標計算周りとか)ので、一旦整理したいですね…😅

次回へ続く..

ソースはこちら -> https://github.com/sfjwr/jsgame

  1. 処理に無駄があるとは思いますが、シンプルなので、一旦これで。

  2. 本当は交差した場所からエフェクトを出すのが適切だと思いますが、面倒だったので弾の位置から出現させています(やってみたら表示にあまり違和感がなかったし)

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2