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

# はじめに

- 前回の記事は[コチラ](https://qiita.com/sfjwr/items/57ef93be349af401779c)

# 今回の目次 🗒

1. 前回まずかったところを直す 🔧
1. キャラクタの描画をまともにする 🖋
1. キャラクタの操作を変更する 🕹
1. ゲームオブジェクトを消去できるようにする 😶‍🌫️
1. 弾を発射できるようにする　✊

# 前回まずかったところを直す 🔧

コメントでも指摘を頂いたのですが、`keydown`イベントから取得できる`KeyboardEvent`の`keyCode`は利用が非推奨になっているようなので、代わりに`code`を利用する形に修正します。

``` JS
const keyCodes = {
  KeyA: 'left',
  KeyW: 'top',
  KeyD: 'right',
  KeyS: 'bottom',
  Space: 'a',
  ShiftRight: 'b',
  ShiftLeft: 'b',
};

...

window.addEventListener(
  'keydown',
  (e) => {
    if (!(e.code in keyCodes)) return;

    keyStatus[keyCodes[e.code]] = true;
  },
  false
);

window.addEventListener(
  'keyup',
  (e) => {
    if (!(e.code in keyCodes)) return;

    keyStatus[keyCodes[e.code]] = false;
  },
  false
);
```

キーコードじゃなくて`KeyA`とかの形で指定できるようになったんですね。わかりやすい。

特に動作に変化はありません。

# キャラクタの描画をまともにする 🖋

## 概要

キャラクタが単なる縦線だと寂しすぎるので、ゲームっぽい絵を出そうと思います。

ドット絵でも書くかなと思ったのですが、絵心がなく、まともなものが描ける気がしないので、妥協して簡単な仕組みでそれっぽい絵を出すことにします。今までと同じ`lineTo`を使用して、多少複雑な絵を描ける仕組みを作ります。


また、枚数をたくさん描きたくないので、左右反転機能と回転機能を持たせます。

以下のように配列でデータを作成し、それを描画できるようにします。

``` JS
[
  // 線１（四角形を描く）
  [
    { x: 10, y: 10 },
    { x: -10, y: 10 },
    { x: -10, y: -10 },
    { x: 10, y: -10 },
    { x: 10, y: 10 },
  ],
  // 線2（三角形を描く）
  [
    { x: 0, y: 10 },
    { x: -10, y: -10 },
    { x: 10, y: -10 },
    { x: 0, y: 10 },
  ],
]
```

`x,y`座標を持ったオブジェクトで座標を表現し、配列にすることで線を表現し、さらにそれを配列にすることで複数の線を利用した絵を表現します。

データの座標は原点を中心に設定します。なぜなら、回転機能を持たせたいからです。座標の回転は高校の数学あたりで習ったような気がする

```math
x' = x\cos\theta - y\sin\theta\\
y' = x\sin\theta + y\cos\theta
```

で行うのですが、この式は原点を中心とした回転になるため、原点中心で座標を設定しておかないといい感じの回転にならないためです。

![座標系.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184660/04de1547-9b73-c6d5-6a8e-45920158093b.png)

実際画面に描画を行う際は、一旦画像データの原点を中心に座標を回転させた後、描画させたい画面の位置に並行移動させて描画を行うことになります。

とりあえず、回転の計算は色々なところで使いそうなので、関数にしておきます。
``` JS
const deg2rad = (degree) => (degree * Math.PI) / 180;

const rx = (x, y, degree) => x * Math.cos(deg2rad(degree)) - y * Math.sin(deg2rad(degree));
const ry = (x, y, degree) => x * Math.sin(deg2rad(degree)) + y * Math.cos(deg2rad(degree));
```

`deg2rad` は角度から弧度への変換です。

``` JS
const x = 10;
const y = 0;
const xd = rx(x, y, 90);
const yd = ry(x, y, 90);
```

のように使います。この例では、`(10, 0)`が90度回転して、`(xd, yd)`には`(0, 10)`が入ることになります。

これを使いつつ、先ほどのデータ形式のデータの描画が行える関数を作成します。

``` JS
function drawObject(x, y, degree, backword, color, lines) {
  ctx.beginPath();

  ctx.lineWidth = 1;
  ctx.strokeStyle = color;

  const rxm = (x, y, degree) => rx(x, y, degree) * (backword ? -1 : 1); // x軸反転対応

  for (let line of lines) {
    // 始点へ移動
    const p = line[0];
    ctx.moveTo(x + rxm(p.x, p.y, degree), y + ry(p.x, p.y, degree));

    // 線を残りの座標について引く
    for (let i = 1; i < line.length; i++) {
      to = line[i];
      ctx.lineTo(x + rxm(to.x, to.y, degree), y + ry(to.x, to.y, degree));
    }
  }

  ctx.stroke();
}
```

左右反転機能を持たせたいため、引数`backword`が`true`の時は左右反転をついでに行う回転関数`rxm`を内部で作成して利用しています。また、ついでに色を指定できるようにしています。

これを利用して、プレイヤーキャラクタを描画します。多分みんなロボットが大好きだと思うので、ロボットっぽい何かを描画することにします。

``` JS

const robotImage = [
  [
    { x: -3, y: -16 },
    { x: 2, y: -15 },
    { x: 2, y: -10 },
    { x: -2, y: -10 },
    { x: -3, y: -16 },
  ],
  [
    { x: 4, y: -5 },
    { x: 4, y: 0 },
    { x: -4, y: 0 },
    { x: -4, y: -10 },
    { x: 4, y: -10 },
  ],
  [
    { x: -1, y: 0 },
    { x: 3, y: 0 },
    { x: 4, y: 10 },
    { x: -8, y: 10 },
    { x: -1, y: 0 },
  ],
  [
    { x: 0, y: -10 },
    { x: 10, y: -10 },
    { x: 10, y: -5 },
    { x: 0, y: -5 },
    { x: 0, y: -10 },
  ],
];

...

let gameObjects = [
  ...
  {
    type: 'player',
    ...
    r: 0,
    backward: false,
  },
];

...

const functions = {
  ...
  player: (obj) => {
    const DRAW_CENTER_HEIGHT = 10;

    ...

    if (gameInput.left) {
      obj.x -= 3;
      obj.backward = true;
    }
    if (gameInput.right) {
      obj.x += 3;
      obj.backward = false;
    }

    ...

    drawObject(
      obj.x,
      obj.y - DRAW_CENTER_HEIGHT,
      obj.r,
      obj.backward,
      'rgb(0, 0, 255)',
      robotImage
    );
  }
};
```
キャラクタの(x, y)座標をロボットの足元にしたいので、10ピクセル上にずらして描画しています。

(ロボットに見えないという指摘は受け付けていません😎)

## 動作サンプル

- 動作サンプル
    - [開く](https://sfjwr.github.io/jsgame/src/history/7.html)
- 操作方法
    - 左スティック(WASDキー)で移動
    - Aボタン(スペースキー)でジャンプ

# キャラクタの操作を変更する 🕹

## 概要

せっかく回転機能を作成したので、キャラクタを回転できるようにします。そのためには入力ボタンが足りないため、パッドのいわゆる右スティックも使えるようにします。

また、キャラクタがロボットっぽくなったので、なんとなくジャンプではなく浮上するような動きに変更します。

``` js
const functions = {
  player: (obj) => {
    ...

    // 入力に応じでキャラを動かす
    {
      // 左右移動
      if (gameInput.l_left) {
        obj.x -= 3;
      }
      if (gameInput.l_right) {
        obj.x += 3;
      }

      // 上昇
      if (gameInput.l_top) {
        obj.ay -= 1.6;
      }

      // 回転
      if (gameInput.r_left) {
        obj.backward = true;
      }
      if (gameInput.r_right) {
        obj.backward = false;
      }
      if (gameInput.r_top) {
        obj.r -= 8;
      }
      if (gameInput.r_bottom) {
        obj.r += 8;
      }
      if (obj.r < -40) obj.r = -40;
      if (obj.r > 40) obj.r = 40;
    }

    ...
  },
};
```

右スティック(カーソルキー)の上下で回転できるようにしました。角度は40度で制限しています。また、左スティック上方向(wキー)を押しっぱなしにすることで浮上できます。

入力ボタンを増やした部分のソースコードは、単にデータを増やしただけの単純なものなので記載を省略しました。気になる人は動作サンプルからソースを確認してください。

ちなみに、既に操作が激ムズという説がありますw

## 動作サンプル

- 動作サンプル
    - [開く](https://sfjwr.github.io/jsgame/src/history/8.html)
- 操作方法
    - 左スティック(WASDキー)で移動
    - 右スティック(カーソルキー)で方向転換
        - 左右で振り向き
        - 上下で角度変更

# ゲームオブジェクトを消去できるようにする 😶‍🌫️

## 概要

現状、画面に描画したゲームオブジェクトを消去する手段がないため、それを用意します。次の章のための準備です。

ゲームオブジェクトの処理関数が`false`を返した場合に、配列から消去するようにします。
ただし、配列はデータ構造的に途中の要素の削除が遅いため、ここでは新しい配列を作ってそちらに残したいオブジェクトを追加して、最後に配列全体を差し替える形にします[^1]。

ついでに、FPSを画面に描画するときにオブジェクトの数も描画するようにします。

```js

let gameObjects = [
  ...
];
let newObjects = [];

const functions = {
  fps: (obj) => {
    // 内部情報を画面に描画する
    ctx.fillText(
      `fps: ${prevFps.toString()}, Objects: ${gameObjects.length}`,
      10,
      15
    );
    return true;
  },
  gameInput: () => {
    ...
    return true;
  },
  player: (obj) => {
    ...
    return true;
  },
}

function gameLoop() {
  ...

  // ゲームオブジェクトを処理
  newObjects = [];
  gameObjects.forEach((obj) => {
    if (functions[obj.type](obj)) {
      newObjects.push(obj);
    }
  });
  gameObjects = newObjects;

  ...
}
```

`gameObjects`は書き換えるため、`const`から`let`に変更します。

さらについでに、キャラクター１体だと寂しいので、敵っぽい何かを描画しておきます。

```js

let gameObjects = [
  ...
  {
    type: 'enemy',
    x: 290,
    y: HORIZON_HEIGHT,
    ay: 0,
    r: 0,
    backward: true,
  },
];

const functions = {
  ...

  enemy: (obj) => {
    const DRAW_CENTER_HEIGHT = 10;

    drawObject(
      obj.x,
      obj.y - DRAW_CENTER_HEIGHT,
      obj.r,
      obj.backward,
      'rgb(150, 0, 0)',
      robotImage
    );

    return true;
  },
}
```

プレイヤーの画像の使い回しです。色違いにしました。

## 動作サンプル

- 動作サンプル
    - [開く](https://sfjwr.github.io/jsgame/src/history/9.html)
- 操作方法
    - 左スティック(WASDキー)で移動
    - 右スティック(カーソルキー)で方向転換
        - 左右で振り向き
        - 上下で角度変更


# 弾を発射できるようにする　✊

## 概要
プレイヤーが攻撃をできるようにします。

キーが押された際に、弾用のゲームオブジェクトを生成して`gameObjects`に`push`します。弾は加速度方向に真っ直ぐ飛んでいって、画面外に出たら消えるようにします。

また、撃った時に反動を入れたいので、プレイヤーの加速度にx方向[^2]の加速度も追加します。

``` js

let gameObjects = [
  ...

  {
    type: 'player',
    ...
    ax: 0,
    fireCounter: 0,
  },

  ...
];


const functions = {
　　　　...

  player: (obj) => {
    const mirror = obj.backward ? -1 : 1;

    ...

    // 左右移動
    if (gameInput.l_left) {
      if (-3 < obj.ax && obj.ax < 0) {
        obj.ax = -3;
      } else {
        obj.ax -= 0.5;
      }
    }
    if (gameInput.l_right) {
      if (0 < obj.ax && obj.ax < 3) {
        obj.ax = 3;
      } else {
        obj.ax += 0.5;
      }
    }

    ...

    // 弾を打つ
    if (gameInput.b && obj.fireCounter === 0) {
      obj.fireCounter = 10;
    }
    if (obj.fireCounter > 5) {
      newObjects.push({
        type: 'bullet',
        x: obj.x + rx(0, -10, obj.r) * mirror,
        y: obj.y - DRAW_CENTER_HEIGHT + ry(0, -10, obj.r),
        ax: rx(30, 0, obj.r) * mirror,
        ay: ry(30, 0, obj.r),
        backward: obj.backward,
        r: obj.r,
      });

      // 反動を付与
      obj.ax += rx(-2, -0.3, obj.r) * mirror;
      obj.ay += ry(-2, -0.3, obj.r);
    }
    if (obj.fireCounter > 0) obj.fireCounter--;

    // 重力
    if (obj.y < HORIZON_HEIGHT) {
      // 地面より上にいれば下方向に加速させる
      obj.ay += 0.6;
    }

    // 横方向の減衰
    if (obj.ax != 0) obj.ax = obj.ax * 0.8;

    // 加速度を反映
    obj.x += obj.ax;
    obj.y += obj.ay;

    // 地面に着いたら加速を止める
    if (obj.y > HORIZON_HEIGHT) {
      obj.ax = 0;
      obj.ay = 0;
      obj.y = HORIZON_HEIGHT;
    }

    ...
  },

  bullet: (obj) => {
    obj.x += obj.ax;
    obj.y += obj.ay;

    // 弾を画面に描画
    drawObject(obj.x, obj.y, obj.r, obj.backward, 'rgb(0, 0, 255)', [
      [
        { x: 0, y: 0 },
        { x: -20, y: 0 },
      ],
    ]);

    // 画面外に出たら消す
    if (obj.x < -20 || obj.x > 340 || obj.y < -20 || obj.y > 260) {
      return false;
    }

    return true;
  },
};
```

一度入力すると、５発連続で発射されるようにしました。

プレイヤーの向きや方向に合わせて弾の方向の制御をしないといけないので、その辺りの計算も併せて行っています。

y方向の加速度はキャラクタの上昇操作と重力でうまく相殺されますが、x方向の加速度は何もしないとずっと横方向に勝手に動き続ける形になってしまうため、適当に減衰させています。

左右移動の計算が若干ややこしいのは、プレイヤーの速度が速くなりすぎないように、かつ急に停止することがないようにするための処置です。

## 動作サンプル

- 動作サンプル
    - [開く](https://sfjwr.github.io/jsgame/src/history/10.html)
- 操作方法
    - 左スティック(WASDキー)で移動
    - 右スティック(カーソルキー)で方向転換
        - 左右で振り向き
        - 上下で角度変更
    - Bボタン(シフトキー)で弾を発射

**次回へ続く..** → 続きは[コチラ](https://qiita.com/sfjwr/items/5400993e87075f1657b1)

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

[^1]: 実質どちらが重いのかはよくわかりませんが…。
[^2]: y方向は重力添加用に元からあります。