webpack+VSCodeで、Vanilla JS+Canvas APIの簡単なゲーム開発準備

13KB以下でHTMLゲームを開発するコンテスト [js13kGames](https://js13kgames.com/) のために、とても簡単なゲーム開発の環境を構築しました。

やることは単なる伝統的なCanvas API操作なのですが、ファイルサイズを抑えるため一切ライブラリを同梱しないVanilla JSで記述していきます。ECMAScript 2015+(ES6+)で開発し、webpackでビルドします。Visual Studio CodeのJavaScript開発支援機能も少し使っていきます。

## js13kGamesについて
下記の記事が詳しいです。

- [1か月かけてJavaScriptで13KBのゲームを作るゲームジャム「js13kGames」がもたらした知見とは \- GamesIndustry\.biz Japan Edition](http://jp.gamesindustry.biz/article/1710/17101603/)
- [js13kGames：コードゴルフ型ゲーム開発大会 \| Mozilla Developer Street \(modest\)](https://dev.mozilla.jp/2016/08/js13kgames-code-golf-for-game-devs/)

js13kGamesは参加してみて結構面白いと思ったのですが、日本語の情報があまり見つかりませんでした。もし来年以降も開催されるならば初学者の方が参加しやすくなるよう、このような単純な開発環境の構築手順をメモしておきます。

## 環境
- Windows Subsystem for Linux / Ubuntu 16.04.4 LTS
    - たぶんMacでもほぼ同じです
- node 9.4.0
- npm 6.4.1
- webpack 4.19.0
- Visual Studio Code 1.27.2

[jQuery](https://jquery.com/), [Lodash](https://lodash.com/), [Phaser](https://phaser.io/)のようなパッケージは一切使いません。

## 作成
適当な名前でリポジトリを作ります。ここでは`my-game`とします。

```bash
$ mkdir my-game
$ cd my-game
$ git init
$ curl 'https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore' > .gitignore
```
`package-lock.json`が`git diff`の出力結果に混ざって欲しくない場合は、バイナリ扱いにしておきます。

```bash
$ echo 'package-lock.json binary' >> .gitattributes
```

### webpackによるビルドの設定
まずpackage.jsonを作成します。
`npm init`で作成できますが、回答は適当で構いません。

```bash
$ npm init
```
webpackをインストールします。

```bash
$ npm install --save-dev webpack webpack-cli webpack-merge
```
本番出力用の`webpack.config.js`を作成します。

```js:webpack.config.js
module.exports = {
  mode: 'production',
  entry: './src/index.js',
};
```
開発時に使うための`webpack.dev.js`を作成します。

```js:webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
});
```
`webpack --config webpack.config.js`で本番用のビルドを、`webpack --config webpack.dev.js`で開発用のビルドをします。これらのコマンドを実行しやすいよう [npm scripts](https://docs.npmjs.com/misc/scripts) に記述しておきます。

最終的にpackage.jsonはこんな感じになります。

```json:package.json
{
  "private": true,
  "scripts": {
    "prepare": "webpack --config webpack.config.js",
    "watch": "webpack --config webpack.dev.js --watch"
  },
  "devDependencies": {
    "webpack": "^4.19.0",
    "webpack-cli": "^3.1.0",
    "webpack-merge": "^4.1.4"
  }
}
```

### Hello World
`dist/index.html`にHTMLを記述していきます。
VSCodeで空のファイルを作成して`html:5`と入力してTabを押すことで、Emmetのテンプレートを呼び出すと便利です。

```html:dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <script src="./main.js"></script>
  <title>My Game</title>
</head>
<body>
  <canvas id="main-canvas" width="640" height="480"></canvas>
</body>
</html>
```

JavaScriptの処理を記述していきます。
`#main-canvas`に「Hello, World」と描画してみます。

```src/index.js
document.addEventListener('DOMContentLoaded', () => {
  // Canvas initialize
  const canvasElem = document.getElementById('main-canvas');
  const canvasContext = canvasElem.getContext('2d');
  canvasContext.font = '18px serif';

  // Canvas Clear
  canvasContext.fillStyle = 'black';
  canvasContext.clearRect(0, 0, canvasElem.width, canvasElem.height);
  canvasContext.fillRect(0, 0, canvasElem.width, canvasElem.height);

  // Render
  canvasContext.fillStyle = 'white';
  canvasContext.fillText('Hello, World!', 4, 20);
});
```

### Visual Studio Codeでnpm watch
`$ npm run watch`コマンドでJavaScriptをビルドします。

Visual Studio Codeのnpm統合機能を使う場合は、コマンドパレットを開き、`>Preferences: Open User Settings`で「ワークスペースの設定」から`npm.enableScriptExplorer`を`true`にします。

```.vscode/settings.json
{
  "npm.enableScriptExplorer": true
}
```
ここで、「NPMスクリプト」欄から`npm run watch`を実行できるようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/3238ef82-168e-9a7f-2abd-3a78cc767c86.png)

### ゲームの実行
`dist/index.html`を開きます。

```bash
# Windows Subsystem for Linuxの場合
$ explorer.exe $(wslpath -w dist/index.html)

# Macとかの場合
$ open dist/index.html
```
黒い背景に「Hello，World!」と表示されたら成功です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/3ff5bf3b-294b-2d86-4819-23bbecd2d99f.png)

これでゲームを実行するところまでは完成しました。あとは、JavaScriptでメイン処理を記述していくだけです。

#### 例
たとえば以下のように記述すると、「Hello, World!」の文字をカーソルキーで操作できるようになります。

```src/index.js
document.addEventListener('DOMContentLoaded', () => {
  // Keyboard Settings
  const keyCodes = {
    Right: 39,
    Left: 37,
    Up: 38,
    Down: 40,
    D: 68,
    A: 65,
    W: 87,
    S: 83,
  };
  let rightPressed;
  let leftPressed;
  let upPressed;
  let downPressed;
  function setKeyPressed(eventName, value) {
    document.addEventListener(eventName, (e) => {
      switch (e.keyCode) {
        case keyCodes.Right:
        case keyCodes.D:
          rightPressed = value;
          break;
        case keyCodes.Left:
        case keyCodes.A:
          leftPressed = value;
          break;
        case keyCodes.Up:
        case keyCodes.W:
          upPressed = value;
          break;
        case keyCodes.Down:
        case keyCodes.S:
          downPressed = value;
          break;
        default:
      }
    }, false);
  }
  setKeyPressed('keydown', true);
  setKeyPressed('keyup', false);

  // Canvas initialize
  const canvasElem = document.getElementById('main-canvas');
  const canvasContext = canvasElem.getContext('2d');
  canvasContext.font = '18px serif';

  // Cursor initialize
  let x = 4;
  let y = 20;
  const speed = 10;

  function main() {
    // Canvas Clear
    canvasContext.fillStyle = 'black';
    canvasContext.clearRect(0, 0, canvasElem.width, canvasElem.height);
    canvasContext.fillRect(0, 0, canvasElem.width, canvasElem.height);

    // User input
    if (rightPressed) {
      x += speed;
    } else if (leftPressed) {
      x -= speed;
    }
    if (downPressed) {
      y += speed;
    } else if (upPressed) {
      y -= speed;
    }

    // Render
    canvasContext.fillStyle = 'white';
    canvasContext.fillText('Hello, World!', x, y);
  }

  // loop
  setInterval(main, 1000 / 30);
});
```

たとえば[MDNのブロック崩しチュートリアル](https://developer.mozilla.org/ja/docs/Games/Workflows/2D_Breakout_game_pure_JavaScript)などを参考に、ゲームを開発していきましょう。

### ESLintの導入
Canvas APIについてググって出てくる情報には、古い内容も多いです。
きちんとECMAScript 2015+に対応できるよう、予め [ESLint](https://eslint.org/) を入れておくと効果を発揮すると思います。

```bash
$ npm install --save-dev eslint
$ ./node_modules/.bin/eslint --init
```
`eslint --init`の質問はお好みで。今回はこんな感じで設定しました。

Question|Answer
:--|:--
How would you like to configure ESLint?|Use a popular style guide
Which style guide do you want to follow?|Airbnb (https://github.com/airbnb/javascript)
Do you use React?|No
What format do you want your config file to be in?|JavaScript
Would you like to install them now with npm?|Yes

`"env.browser": true`の設定は入れておきましょう。

```js:.eslintrc.js
module.exports = {
  "env": {
    "browser": true
  },
  "extends": "airbnb-base"
};
```

Visual Studio Codeの [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 拡張を導入することで、ルールに違反している箇所が「問題」として警告が出るようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/a40c9e23-035e-9279-c1e4-028dda90d6a8.png)

### 完成
本番用のwebpackビルドをして、JavaScriptを圧縮します。

```bash
$ npm run prepare
```

もしjs13kGamesに応募するならば、`dist/`以下を`zip`で固めた物が提出物になります。このzipファイルが13KBを下回っていなければなりません。

```bash
$ zip --recurse-paths dist.zip dist/*
$ du --human-readable dist.zip
4.0K    dist.zip
```

## 参考
- [js13kGames \- HTML5 and JavaScript Game Development Competition in just 13 kilobytes](https://js13kgames.com/)
- [webpack](https://webpack.js.org/)
- [ESLint \- Pluggable JavaScript linter](https://eslint.org/)
- [Canvas API \- HTML: HyperText Markup Language \| MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Canvas)

最終的に私が開発してjs13kGamesに提出した物はこちらです。
https://js13kgames.com/entries/inside-the-rect
~~締切当日夜20時に作り始めたため~~単純な内容であり、本稿以外の複雑な技術は使っていません。