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

# はじめに
- JavaScript(JS)でゲームを作ります
- 生のJSで作ります
    - TypeScriptなどは使用しません
    - 同じようなことをやろうと思った人たちの敷居を上げないため
- HTMLやJS自体の説明はしません
- ゲーム用のライブラリ等は使用しません
- 2Dで作ります
- ブラウザで動かします
- 環境差異は気にしません
    - 最新のChromeでのみ動作確認します
- ゲーム内容はとりあえず未定です
    - コントローラーを使いたいので、アクション系にはしようかなと考え中

# 今回の目次 🗒
1. ゲームループを作る 🌀
1. オブジェクト制御を作る 📡
1. FPSを画面に表示する 📝
1. ゲームパッドからの入力を作る 🎮
1. ゲームパッドからの入力を画面に表示する 📝
1. プレイヤーキャラを作る 🚀

# ゲームループを作る 🌀

## 概要

まず初めに、ゲームループを作ります。

ゲームループとは、要は単なる無限ループなのですが、唯一の機能として「一定時間に決まった回数だけ繰り返す」というものがあります。1秒間に60回とか繰り返すことが一般的です。

なぜ60回繰り返すのかというと、ディスプレイの画面更新回数がそのような回数である[^1]ためで、そこに合わせているからです。つまり、ゲームループの最終的な目的は「ディスプレイに映し出す映像を作る」ということになります。ディスプレイに映し出す画像を秒間60回生成し、パラパラアニメをさせることで、ゲームの動きを作り出します。

最終的にディスプレイに映し出す映像を作り出すためには、その前処理としてキャラクタの移動であったり、生死判定であったり、スコア計算であったり、必要な処理を行う必要があり、それらの全てをループ１回(1/60秒)の中で実施しなくてはなりません。

ちなみに画面を一秒間に30回更新するなら30fps[^2]、60回なら60fpsなどと呼ばれます。今回はJSのタイマーの精度の問題などから、30fpsで作っていこうと思います。

30fpsということは、一回のループあたりの時間は1/30秒=0.0333…秒になります。そのため、約0.03秒の間に、1フレーム内で必要な全ての処理をやり切らねばなりません。もし約0.03秒を超えてしまった場合、30fpsのはずが20fpsや10fpsになったりして、画面がカクカクする結果になってしまいます。また、逆に約0.03秒より早く処理が終わった場合、0.03秒になるまで待機する必要があります。そうしないと逆に40fpsや50fpsなど、想定より早い動きのゲームになってしまいます。

通常１フレーム内でやることとしては

1. 画面を消去する
1. キャラクタの動きや当たり判定などのゲームロジックを実行する
1. ゲームロジックの結果を画面に描画する
1. 次のフレームの開始時間まで、残った時間を待機する

というような内容になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184660/b4aab7ee-0979-6c32-b495-3a1c724fdc7c.png)

## JSでの実装

以上をJSで実装すると、以下のようになります。

### ファイル game.js

``` JS
const ctx = document.getElementById('screen').getContext('2d');

function gameLoop() {
  const begin = Date.now();

  ctx.clearRect(0, 0, 320, 240); // 画面を消去

  // ゲームロジック(とりあえず線を描画するだけ)
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(10, 10);
  ctx.lineTo(20, 20);
  ctx.stroke();

  const end = Date.now();
  setTimeout(gameLoop, 33 - (end - begin)); // 0.33msから実際かかった時間を引いた秒数待つ
}

gameLoop();
```
### ファイル game.html

``` html
<html>
  <body>
    <canvas
      id="screen"
      width="320"
      height="240"
      style="border: 1px solid gray"
    />
    <script src="./game.js"></script>
  </body>
</html>
```
画面の描画にはCanvasを利用しています。サイズはとりあえず320x240にしました。初めにCanvasをクリアして、とりあえず線を引いて、`setTimeout`で次の実行タイミングを仕込んでいます。
これを動かすと、画面の左上に短い線が表示されます。線は動きませんが、秒間30回の書き換え処理が行われています。

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/0.html)

# オブジェクト制御を作る 📡

## 概要

次は、画面内にゲームオブジェクト(キャラクタとか弾とか)を表示していきたいと思います。画面には複数のゲームオブジェクトが描画されることになるので、配列を使って複数のゲームオブジェクトの処理をいい感じに実行できる仕組みを作ります。

ゲームオブジェクトを格納するための配列を作成し、ループによって全オブジェクトの処理を実行します。オブジェクトには種類を持たせ、その種類に応じた処理を呼び出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184660/db2d1e5d-2a37-0859-38a0-b3a9f9a15686.png)

## JSでの実装

以上をJSで実装すると、以下のようになります。

### ファイル game.js

``` JS
const gameObjects = [{ type: 'player', x: 30, y: 50 }];

const functions = {
  player: (obj) => {
    obj.x += 0.2;

    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(obj.x, obj.y);
    ctx.lineTo(obj.x, obj.y + 20);
    ctx.stroke();
  },
};

function gameLoop() {
  ...

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

  ...
}

```

ゲームオブジェクトとしては、とりあえず先の、線を引くだけの処理をプレイヤーとして設定しています。

`type`と`x`,`y`座標を持たせ、ゲームループの中ではtypeを見て対応した関数を呼び出しています。`player`関数の中では、x座標を若干右に動かしたあと、x,y座標の位置に線を描画しています。実行すると、線が徐々に右に動いていく様子が確認できます。

ゲームオブジェクトを新たに画面に追加したい場合は、配列`gameObjects`に同様にオブジェクトを追加することで実現できます。例えば`gameObjects`を

``` JS
const gameObjects = [
  { type: 'player', x: 30, y: 50 },
  { type: 'player', x: 50, y: 100 }
];
```

のようにすれば、線が２本に増えて表示されます。

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/1.html)

# FPSを画面に表示する 📝

## 概要

ゲームと本質的には関係ありませんが、処理落ち(30fpsで動かしたいのに10fpsしか出ておらずゲームの動きが遅くなる)などに気づけるように、現在画面が何FPSで描画されているのかを表示するようにします。

単に1秒間に何回ゲームループが実行されたかをカウンタを用意して数えるだけです。
実際の表示は、FPS値を描画するためのゲームオブジェクトを用意して、そちらに描画させます。

## JSでの実装

### ファイル game.js

``` JS
let prevFps = 0;
let fps = 0;
let prevTime = Date.now();

const gameObjects = [
  ...
  { type: 'fps' }
];

const functions = {
  ...
  fps: (obj) => {
    // FPSを画面に描画する
    ctx.fillText('fps: ' + prevFps.toString(), 10, 15);
  }
}

function gameLoop() {
  ...
  // FPS値を計算する
  if (begin - prevTime > 1000) {
    prevFps = fps;
    fps = 1;
    prevTime = begin;
  } else {
    fps++;
  }
  ...
}

```

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/2.html)

# ゲームパッドからの入力を作る 🎮

## 概要

ゲームパッドからの入力を作ります。ゲームパッドAPIを使えば、ブラウザでもゲームパッドからの入力を受け付けることができます。

必要な作業としては

1. ゲームパッドをPCに接続すると、コールバック関数が呼ばれる
1. コールバック関数からインデックス（何番目のゲームパッドか）が取得できる
1. `gamepads`配列からそのインデックスのゲームパッドを取得し、入力を得る

という内容になります。

コールバックからインデックスを取得しなくても、そもそも1個しかパッドを繋いでいなければ`gamepads`配列は一個しか無いだろうし、インデックスは自明なような気もしますが、手元のPCではなぜかゲームパッドをつないでなくても`gamepads`配列が４つもあったので、とりあえずこのような形で実装します。

## JSでの実装

### ファイル game.js

``` JS

let gamePadIndex;

// ゲームパッドからの入力を保持する
let gameInput = {
  left: false,
  right: false,
  top: false,
  bottom: false,
  a: false,
  b: false,
};

addEventListener('gamepadconnected', (e) => {
  // パッドが接続されたらインデックスを保存
  gamePadIndex = e.gamepad.index;
});

function gameLoop() {
  ...
  if (gamePadIndex !== undefined) {
    // パッドが接続されていれば入力を取得する
    const gamePad = navigator.getGamepads()[gamePadIndex];
    gameInput = {
      left: (gamePad.axes[0] < -0.5),
      right: (gamePad.axes[0] > 0.5),
      top: (gamePad.axes[1] < -0.5),
      bottom: (gamePad.axes[1] > 0.5),
      a: (gamePad.buttons[1].pressed),
      b: (gamePad.buttons[0].pressed)
    };
  }
  ...
}
```

ゲームパッドがつながっていれば入力を取得し、結果を`controller`オブジェクトに設定しています。

`axes`はスティックの傾きに応じたアナログ値が入っているのですが、今回はデジタル入力でいいかなと思ったので、中間の0.5以上と以下でON/OFFの判断をしています。

ボタンについては二つだけ、それぞれAボタンBボタンとして取得しています。

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/3.html)

# ゲームパッドからの入力を画面に表示する 📝

## 概要

開発時に便利なので、今ゲームパッドから何が入力されているのかを画面に表示する機能を作ります。

表示させる機能を持ったゲームオブジェクトを追加します。

## JSでの実装

### ファイル game.js

``` JS

const gameObjects = [
  ...
  { type: 'gameInput' }
];


const functions = {
  ...
  gameInput: () => {
    // パッドからの入力を画面に表示する
    let s = '';
    s += gameInput.left ? 'L' : '_';
    s += gameInput.top ? 'T' : '_';
    s += gameInput.right ? 'R' : '_';
    s += gameInput.bottom ? 'B' : '_';
    s += gameInput.a ? 'A' : '_';
    s += gameInput.b ? 'B' : '_';
    ctx.fillText('controller: ' + s, 10, 30);
  }
}

```

画面の左上に、入力に応じて文字を表示するようにしています。

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/4.html)

# キーボードに対応する

## 概要

ゲームパッドからの入力のみだと、ゲームパッドを持っている人しかプレイできなくなるのはもちろん、開発時にも不便になるので、キーボードからの入力にも対応させます。

上下左右をWASDキー、スペースとシフトをゲームパッドのABボタンに対応させます。

`keydown`、`keyup`イベントから入力を生成し、ゲームパッドからの入力と合成します。

## JSでの実装

### ファイル game.js
``` JS

const keyCodes = {
  65: 'left',
  87: 'top',
  68: 'right',
  83: 'bottom',
  32: 'a',
  16: 'b',
};

const keyStatus = {
  left: false,
  right: false,
  top: false,
  bottom: false,
  a: false,
  b: false,
};


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

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

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

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


function gameLoop() {
  ...
  // キーボードから入力を生成する
  gameInput = {
    left: keyStatus.left,
    right: keyStatus.right,
    top: keyStatus.top,
    bottom: keyStatus.bottom,
    a: keyStatus.a,
    b: keyStatus.b,
  };

  if (gamePadIndex !== undefined) {
    // パッドが接続されていればキーボードからの入力と合成する
    const gamePad = navigator.getGamepads()[gamePadIndex];

    gameInput.left |= gamePad.axes[0] < -0.5;
    gameInput.right |= gamePad.axes[0] > 0.5;
    gameInput.top |= gamePad.axes[1] < -0.5;
    gameInput.bottom |= gamePad.axes[1] > 0.5;
    gameInput.a |= gamePad.buttons[1].pressed;
    gameInput.b |= gamePad.buttons[0].pressed;
  }
  ...
}

```

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/5.html)

# プレイヤーキャラを作る 🚀

## 概要

ゲームパッドやキーボードから入力できるようになったので、肝心のゲームの内容を何も考えていませんが、とりあえずゲームパッドで操作できるキャラクタを画面上に出したいと思います。

既存の`player`の処理を変更して、左右ボタンで左右に移動、Aボタンでジャンプさせる形で一旦実装します。

キャラクタと言っていますが、姿形は今までと同じ単なる縦線として画面には描画します😂

## JSでの実装

### ファイル game.js

``` JS
const HORIZON_HEIGHT = 200; // 地面の高さ

const gameObjects = [
  ...
  { type: 'player', x: 30, y: HORIZON_HEIGHT, ay: 0 },
];

const functions = {
  ...
  player: function (obj) {
    // 地面より上にいれば下方向に加速させる
    if (obj.y < HORIZON_HEIGHT) {
      obj.ay += 0.5;
    }

    // 入力に応じでキャラを動かす
    if (gameInput.left) {
      obj.x -= 3;
    }
    if (gameInput.right) {
      obj.x += 3;
    }
    if (gameInput.a && obj.y == HORIZON_HEIGHT) {
      obj.ay -= 8;
    }

    obj.y += obj.ay;

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

    ctx.beginPath();
    ctx.lineWidth = 1;
    ctx.moveTo(obj.x, obj.y);
    ctx.lineTo(obj.x, obj.y - 20);
    ctx.stroke();
  }
}


```
左右ボタンによって、キャラクタのx座標を変更しています。

また、新しく属性として`ay`を追加しています。縦方向の加速度を格納する目的で、重力の付与に使用します。地面にいるとき以外は`ay`を増やすことで下方向に落下させています。また、ジャンプボタンが押された時は、上方向に加速度を設定してやることでジャンプさせることができます。

## 動作サンプル

- [開く](https://sfjwr.github.io/jsgame/src/history/6.html)

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

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

[^1]: 昔のモニタの画面の更新回数がこれらの回数だったため。最近は120や240などになってたりしますね。
[^2]: Frame per secondの略。一秒間に何フレーム描画するか。