メルカリで5000円で買った脳波計とElectronで車を動かすゲームを作る

![脳波とElectronで車を動かすゲームを作る](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/fe4f5def-f58f-54cd-9af6-4190ba6ef3c6.gif)

# はじめに

ある朝、目が覚めると、[イーロン・マスクがSF映画も真っ青なプレゼンをしていました](https://jp.techcrunch.com/2019/07/18/2019-07-16-elon-musks-neuralink-looks-to-begin-outfitting-human-brains-with-faster-input-and-output-starting-next-year/)。なんでも脳とマシンを直接接続するデバイスを作って、私たち凡夫の脳の潜在パワーを引き出してくれるというではありませんか。この手のデバイスはBMI（Brain Machine Interface）と呼ばれ、アカデミック領域では1970年代から研究されているようです[^1]。

[^1]: Wikipedia, 『ブレイン・マシン・インターフェース』, https://ja.wikipedia.org/wiki/ブレイン・マシン・インタフェース,  2019/8/14

2020年のうちに人間を対象に臨床実験を始めると言うイーロン・マスクの神託が実現されるかは不透明ですが、MRIを始めとする非侵襲式（＝脳みそに電極をぶっ刺さない）技術の発展を追い風に、BMIはここ十数年で飛躍的に伸びているアツアツの研究領域であるようです。

すぐにメルカリでBMIデバイスを探したところ、MindWave Mobileという簡易脳波計が5,000円で出品されていました。買いました。どうやら定価でも20,000円弱の模様です。
![MindWave mobile Amazon](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/acb5abae-d37d-e78c-6043-3b1d5e0203de.png)

この記事ではBMIの面白さを体験することを目的に、MindWave Mobileを用いて簡単なゲームを作ってみます。

# 作るもの

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">脳波計をメルカリで買いました。車を脳波で動かすクソゲーをElectronで作って遊んでます。集中すると加速、瞑想すると停止、まばたきで車線変更します。MindWave Mobileという脳波計です。コードはGitHubに上げました。そのうちQiitaとかにまとめたい。<a href="https://t.co/V0BIKyLbtT">https://t.co/V0BIKyLbtT</a> <a href="https://t.co/CWMWejLVNO">pic.twitter.com/CWMWejLVNO</a></p>&mdash; 秋山 雅之@Bizerのエンジニア (@aki202) <a href="https://twitter.com/aki202/status/1162758414641950720?ref_src=twsrc%5Etfw">August 17, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

上は私が脳波計を装着しながらベローチェ[^2]で書いたゲームです。ソースコードはMITライセンスで https://github.com/aki202/neuro_drive_electron に置いてあります。（MindWave Mobileさえご自宅にあれば）ビルドしてすぐに遊べます。

[^2]: ベローチェ：日本のカフェを牛耳るシャノアールグループが展開するコーヒーチェーン。新宿駅周辺に10店舗程あるため、待ち合わせ場所に選ぶと大体待ち合わせできない。

この記事では下の画像の通り、車を脳波で動かすところまでを作ってみましょう。
![MindWave mobile 脳波ゲーム](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/8efb8f49-348f-20c7-ca9b-32b9e213903f.gif)

# MindWave Mobileとは

[MindWave Mobile](https://store.neurosky.com/pages/mindwave)は[NeuroSky社](http://neurosky.com/)が作っている簡易的な脳波計です。前頭葉に当てた1cm四方以下のチップで3〜100Hzの脳波を計測し、Bluetoothで送信してくれます。

人間の脳波は次のように分類されます。

| 名称 | 周波数(Hz) | いつ出るの|
|:---|---:|:---|
| デルタ波（δ波） | 0.5-4 | 深入り眠り、無意識、脳死。 |
| シータ波（θ波） | 4-8 | 深い瞑想状態、まどろみ。 |
| アルファ波（α波） | 8-14 | リラックス状態。 |
| ベータ波（β波） | 14-38 | やや緊張状態。 |
| ガンマ波（γ波） | 26-70 |  |

この表から、3〜100Hzというレンジは人間の脳波をカバーするのに充分であると言えそうです。なお、γ波はβ波の一部であるとする流派も存在しますが、γ波として別個の種類としておくと何かと研究に有意らしいです。

MindWave Mobileは各周波数別の脳波の強度と、次の３つの値を0〜100の数値で取得できます。３つの値の計算式はブラックボックスです。

- Attention（集中度）
- Meditation（瞑想度）
- Blinkness（またばき）

![付属アプリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/34c989f9-d4cc-2b71-ca38-3a5c343c7b48.png)

# Electronとは

[Electron](https://electronjs.org)は[GitHub社](https://github.com)がエディタAtomを創造するために作った、デスクトップアプリケーション用のフレームワークです。HTML, CSS, JavaScriptを用いて、Windows, OSX, Linux上で動作するGUIアプリをビルドできます。

# 作ってみよう

Node.jsがインストール済みであることを確認してください。なお`npm`コマンドの代わりに`yarn`コマンドを使います。npm派の方は[コマンド対応表](https://qiita.com/morrr/items/558bf64cd619ebdacd3d)などを参考に読み替えてください。

## Electronプロジェクトを作る

プロジェクト用のディレクトリを作成し、その中でnode packageの`electron`を追加します。

```bash
mkdir neuro_drive_app
cd neuro_drive_app
yarn init -y
yarn add electron
```

package.jsonにelectronコマンドを叩くscriptを追加します。

```json:package.json
{
  "name": "neuro_drive_app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "electron": "^6.0.8"
  },
  "scripts": {
    "start": "electron ."
  }
}
```

エントリポイントとなる`index.js`、Electronウィンドウ内に表示する`index.html`、index.htmlから読み込まれる`src/app.js`をそれぞれ次のように作ります。

```javascript:index.js
const { app, BrowserWindow } = require('electron')

let win

const createWindow = () => {
  win = new BrowserWindow({
    width: 1150,
    height: 838,
    webPreferences: {
      nodeIntegration: true
    }
  })
  win.loadFile('index.html')
  win.webContents.openDevTools()
  win.on('closed', () => {
    win = null
  })
}

app.on('ready', createWindow)

app.on('activate', () => {
  if (win != null) return
  createWindow()
})
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';">
    <meta charset="UTF-8">
    <title>Neuro Drive App</title>
    <style>
    #game { background: lightgrey }
    </style>
  </head>
  <body>
    <canvas id="game" width="580" height="800"></canvas>
    <script src="src/app.js"></script>
  </body>
</html>
```

```javascript:src/app.js

console.log("Hello, I'm Electron app!")
```

ファイルツリーは次にようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/b61864db-9873-f5c2-96b8-b7ed405e97b3.png)

ここまで済んだら実際にビルドして立ち上げてみましょう。下記のコマンドを叩きます。

```bash
yarn start
```

次にようなウィンドウが表示されればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/66cc5f3b-1549-f1fa-df8f-33d5ffae8c25.png)

## 車を置く

いらすとや様から車の画像をお借りします。
https://www.irasutoya.com/2015/11/blog-post_793.html
車のpngファイルを`src/images/car.png`として保存します。

車を表現するクラスを`src/components/Car.js`に作ります。

```javascript:src/components/Car.js
const width  = 100
const height = 134

class Car {
  constructor(canvas) {
    this.canvas = canvas
    this.loaded = false

    this.img = new Image()
    this.img.onload = () => this.loaded = true
    this.img.src = path.resolve('src/images/car.png')

    this.x = canvas.width/2 - width/2
    this.y = canvas.height*0.75
  }

  draw() {
    if (!this.loaded) return

    this.canvas.getContext('2d').drawImage(this.img, this.x, this.y, width, height)
  }
}

module.exports = Car
```

`src/app.js`の中身を消して次にように書き換えます。

```javascript:src/app.js
const path = require('path')
const Car  = require(path.resolve('src/components/Car.js'))

const canvas = document.getElementById('game')
const ctx    = canvas.getContext('2d')

const car = new Car(canvas)

const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  car.draw()
}

setInterval(draw, 10)
```

Electronアプリをリロードすると次にようになるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/94c88420-4c16-1a3b-ebc6-e1745508490b.png)


## 芝生を張る

このままでは寂しいので芝生を張りましょう。
いらすとや様から芝生の画像をダウンロードして`src/images/lawn.png`として保存します。
https://www.irasutoya.com/2015/07/blog-post_732.html

背景を表現するクラスを`src/components/Background.js`に作ります。

```javascript:src/components/Background.js
class Background {
  constructor(canvas) {
    this.canvas = canvas
    this.loaded = false

    this.img = new Image()
    this.img.onload = () => this.loaded = true
    this.img.src = path.resolve('src/images/lawn.png')
  }

  draw() {
    if (!this.loaded) return

    const ctx = this.canvas.getContext('2d')
    ctx.drawImage(this.img, 0, 0, this.canvas.width, this.canvas.height)
  }
}

module.exports = Background
```

芝生画像をキャンバス全体に描画するため、`src/app.js`を次にように修正します。

```javascript:src/app.js
const path       = require('path')
const Car        = require(path.resolve('src/components/Car.js'))
const Background = require(path.resolve('src/components/Background.js')) // 追加

const canvas = document.getElementById('game')
const ctx    = canvas.getContext('2d')

const car        = new Car(canvas)
const background = new Background(canvas) // 追加

const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  background.draw() // 追加
  car.draw()
}

setInterval(draw, 10)
```

リロードするとこんな感じなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/c3a57fc0-21bd-cbc9-01d8-c7cf8338d935.png)


## 等速で車を動かす

車が走っているよう見せかけるため、背景を動かします。いわゆるミニドライブ方式です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/0ec18ecd-7750-bd86-b51f-907760c7eb26.png)

そのためにまず速度を管理するクラス`src/SpeedSource.js`を作ります。

```javascript:src/SpeedSource.js
class SpeedSource {
  constructor() {
    this.speed = 40
  }

  getSpeed() {
    return this.speed
  }
}

module.exports = SpeedSource
```

`src/app.js`を変更し、`Background`のコンストラクタに`SpeedSource`のインスタンスを渡します。

```javascript:src/app.js
const path        = require('path')
const Car         = require(path.resolve('src/components/Car.js'))
const Background  = require(path.resolve('src/components/Background.js'))
const SpeedSource = require(path.resolve('src/SpeedSource.js')) // 追加

const canvas = document.getElementById('game')
const ctx    = canvas.getContext('2d')

const car         = new Car(canvas)
const speedSource = new SpeedSource() // 追加
const background  = new Background(canvas, speedSource) // 変更

const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  background.draw()
  car.draw()
}

setInterval(draw, 10)
```

２つの背景画像を縦に並べ、速度に応じて上から下に移動させます。背景画像が画面外に出たらもう一方の背景画像の上に移動させることで、無限に背景がループします。

```javascript:src/components/Background.js
class Background {
  constructor(canvas, speedSource) { // 変更
    this.canvas = canvas
    this.speedSource = speedSource // 追加
    this.loaded = false
    this.y1 = -this.canvas.height // 追加
    this.y2 = 0 // 追加

    this.img = new Image()
    this.img.onload = () => this.loaded = true
    this.img.src = path.resolve('src/images/lawn.png')
  }

  draw() {
    if (!this.loaded) return

    const speed = this.speedSource.getSpeed() * 0.1 // 追加
    const ctx = this.canvas.getContext('2d')

    ctx.drawImage(this.img, 0, this.y1, this.canvas.width, this.canvas.height) // 変更
    ctx.drawImage(this.img, 0, this.y2, this.canvas.width, this.canvas.height) // 追加
    this.y1 += speed // 追加
    this.y2 += speed // 追加
    if (this.y1 > this.canvas.height) this.y1 = -this.canvas.height + speed // 追加
    if (this.y2 > this.canvas.height) this.y2 = -this.canvas.height + speed // 追加
  }
}

module.exports = Background
```

リロードすると車が軽快に動き始めます。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/ccee688a-e27c-4078-9ca4-8e697b81ca19.gif)



## 満を持して脳波計

MindWave MobileにはThinkGear Connectorというアプリケーションが付属しています。ThinkGear Connectorは脳波計本体からBluetoothで飛ばされたデータをシリアルポートで受け取って、ソケットにリダイレクトしてくれます。

<img width="684" alt="Screen Shot 2019-09-11 at 8.50.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/b655700f-f01f-f1b5-d4cf-9bf72d4ac327.png">


ThinkGear Connectorとソケット通信を行い、脳波データを受け取るクラス`src/EegReceiver.js`を作ります。

```javascript:src/EegReceiver.js
const net    = require('net')
const events = require('events')
const util   = require('util')

class EegReceiver {
  constructor(options = {}) {
    this.port = options.port || 13854
    this.host = options.host || 'localhost'

    this.config = {
      enableRawOutput: !!options.enableRawOutput,
      format         : 'Json'
    }

    events.EventEmitter.call(this);
  }

  connect() {
    this.client = net.connect(this.port, this.host, () => {
      this.client.write(JSON.stringify(this.config))
    })

    this.client.on('data', data => {
      try {
        const json = JSON.parse(data.toString())
        this.emit('data', json)
      } catch(e) {
        this.emit('parse_error', data.toString());
      }
    });
  }
}

util.inherits(EegReceiver, events.EventEmitter);

module.exports = EegReceiver
```

MindWave Mibileが正常に接続されていれば、`EegReceiver.js`が脳波データを取得します。

## 脳波で車を動かす

`src/SpeedSource.js`が脳波データを基に速度を決定するようにします。次にように書き換えてください。

```javascript:src/SpeedSource.js
class SpeedSource {
  constructor(eegReceiver) {
    this.eegReceiver = eegReceiver
    this.speed = 0

    this.eegReceiver.on('data', data => {
      if (this.stopping) return
      this.speed = data.eSense.attention * 1000
      if (this.speed < 0) this.speed = 0
      console.log(`eeg: attention=${data.eSense.attention}`)
    })
    this.eegReceiver.connect()
  }

  getSpeed() {
    return this.speed
  }
}

module.exports = SpeedSource
```

最後に、`src/app.js`で`EegReceiver`インスタンスを`SpeedSource`のコンストラクタに渡してあげます。

```javascript:src/app.js
const path        = require('path')
const Car         = require(path.resolve('src/components/Car.js'))
const Background  = require(path.resolve('src/components/Background.js'))
const EegReceiver = require(path.resolve('src/EegReceiver.js')) // 追加
const SpeedSource = require(path.resolve('src/SpeedSource.js'))

const canvas = document.getElementById('game')
const ctx    = canvas.getContext('2d')

const eegReceiver = new EegReceiver() // 追加
const speedSource = new SpeedSource(eegReceiver) // 変更
const car         = new Car(canvas)
const background  = new Background(canvas, speedSource)

const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  background.draw()
  car.draw()
}

setInterval(draw, 10)
```

MindWave Mobileを装着してリロードすると、脳波データに応じて速度が変化することが分かります。
![MindWave mobile 脳波ゲーム](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10656/8efb8f49-348f-20c7-ca9b-32b9e213903f.gif)

なお脳波データの値を車の**加速度**ではなく**速度**として代入しています。そのため停まっていた車がノーモーションでトップスピードに入ることもあります。

# おわりに

20人程度の方にMindWave Mobileを装着しゲームをプレイしてもらいましたが、集中度や瞑想度を意識によってコントロールすることは非常に難しいです。一方、まばたきの検知はそこそこの精度がでます。

今後さらにBMI技術が進化すれば、私たちソフトウェアエンジニアが力を発揮できる領域がさらに現実へ侵食できるかも知れません。

この記事のコード：https://github.com/aki202/neuro_drive_app
完成したゲームのコード：https://github.com/aki202/neuro_drive_electron

---

変なものを作ってTwitterで共有するのが趣味です：[@aki202](https://twitter.com/aki202)