TUI版インベーダーゲームをGo言語で作る

この記事は[千 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/sen)の4日目の記事です。

# はじめに

GoのTUIツールに興味が沸いたので、いろいろ調べている時に[ブロック崩し](https://qiita.com/kurehajime/items/a02eaae3f0c17750c07e)を見つけました。
これがシンプルな作りで理解しやすかったので、参考にさせてもらって別のゲームを作ってみた！
という記事です。
termbox-goというライブラリの解説は[ブロック崩し](https://qiita.com/kurehajime/items/a02eaae3f0c17750c07e)を見てもらった方がいいです ^^;
注）インベーダーゲームってタイトルにしてますがルールは全く違う`もどき`です。
　　ちゃんと再現したGo実装は、[ここ](https://github.com/sausheong/invaders)とか見た方がいいです　∑(ﾟДﾟ) 完成度すごい

# どんなゲーム？

特徴は、統率がとれていない＆武器がないので体当たりしかできない侵略者w、を撃ち落とす
インベーダーゲームです
↓こんな感じです

![fvahq-pmxqk.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/e87ca11f-c3bc-a04a-55de-7dbde0e6fd5d.gif)

* ルール
    * インベーダーに3回当たるとゲームオーバー
        * 体当たりしてくるので十字キー(←,→)で避けてください
    * インベーダーに３発当てると撃墜
        * スペースキーで弾を発射
        * １発当たる度にインベーダーの色が変わります
        * 弾を連続で当てるとコンボポイントでスコアが伸びます
    * 全てのインベーダーを撃墜すればゲームクリア
    * ESCキーでゲーム終了

# 環境

### 開発環境

* Go v1.13
* macOS
    * Terminal.appよりiTerm2(&標準フォントMonaco)の方がキレイに表示されます

### 動作環境

* OSはmacOS,Linux,Windowsで確認済み
* 等幅フォント
    * ~~Windowsは、標準設定のコマンドプロンプトだと表示崩れます。~~  
      ~~いくつかの等幅フォント試したけどやっぱり崩れます（誰か崩れない設定探してw）~~  
      Windows10で表示が崩れないことを確認しました！  
      (@mattn さんに[PR](https://github.com/miyaz/invaders/pull/1)頂きました。ありがとうございます！)

# 実行方法

```console
git clone https://github.com/miyaz/invaders.git
cd invaders
go run main.go
```
OR
[こちら](https://github.com/miyaz/invaders/releases)からバイナリダウンロードして実行

# 開発の思い出

開発過程で`こんなんあったなぁ`ということを思い出しながらやったことを説明します。
なお、それぞれのフェーズは下記のように実行できます。

```terminal
git checkout phase1  # phase1から6まであります
go run main.go
```

## フェーズ1
[ブロック崩し...フェーズ1差分](https://github.com/miyaz/invaders/compare/c67c82b...phase1)

### ブロック要素で作るインベーダー

マルチOS対応を考えるとターミナルで画像を使うのは難しいので、[ブロック要素](https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E8%A6%81%E7%B4%A0)のみを使ってインベーダーを作りました。
termboxでセルを描画するSetCell関数は、１文字ずつ座標(x,y)と文字（rune型)を渡す必要があります。
キャラクター文字列を改行で区切り、rune配列に変換して１文字ずつSetCellで描画しています。

```golang:該当コード
//インベーダーを描画
func drawInvader(x, y int) {
  invader := `
 ▚▄▄▞
▟█▟▙█▙
▘▝▖▗▘▝
`
  scanner := bufio.NewScanner(strings.NewReader(invader))
  j := 0
  for scanner.Scan() {
    line := scanner.Text()
    runes := []rune(line)
    for i := 0; i < len(runes); i++ {
      termbox.SetCell(x+i-3, y+j-2, runes[i], termbox.ColorDefault, termbox.ColorDefault)
    }
    j++
  }
}
```

## フェーズ2
[フェーズ1...2差分](https://github.com/miyaz/invaders/compare/phase1...phase2)

### 一定周期で動作させるためのタイマーをSleepからTickerに変更

複数インベーダーをgoroutineで登場させてみました↓　うじゃうじゃいるw
![1ujbm-tiwg6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/0202fde4-1200-3fca-e612-781fda3175e7.gif)

ブロック崩しはボールの移動をtime.Sleepを使って一定周期で動くように実装されていました。
今回はインベーダを撃ち落とすというゲームにしたかったので、goroutineにchannelで通知してリアルタイムに
処理ができるようにtickerを使ったタイマーにし、for-select-caseを使いました。

```diff:変更差分
 //タイマーイベント
-func invaderTimerLoop(itch chan bool) {
+func moveLoop(moveCh chan int, mover, ticker int) {
+       t := time.NewTicker(time.Duration(ticker) * time.Millisecond)
        for {
-               itch <- true
-               time.Sleep(time.Duration(_invaderTimeSpan) * time.Millisecond)
+               select {
+               case <-t.C: //タイマーイベント
+                       moveCh <- mover
+                       break
+               }
        }
+       t.Stop()
 }
```

## フェーズ3
[フェーズ2...3差分](https://github.com/miyaz/invaders/compare/phase2...phase3)

### インベーダー同士の衝突で跳ね返る

インベーダーオブジェクトを配列で持たせて、個々のインベーダーを配列のインデックスで識別するようにしました。
衝突判定関数に配列のインデックスを渡し、そのインベーダーと座標が重なった物体（壁、他のインベーダー、戦闘機）
がないかをチェックし、あれば方向を反転させます。

![9fsql-t60ax.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/9f995055-b631-f1e5-f631-d3dafc74671e.gif)


```diff:差分
 //衝突判定
-func checkCollision(st state) state {
+func checkCollision(st state, i int) state {
+	//左の壁
+	if st.Invaders[i].Pos.X <= 0 {
+		st.Invaders[i].Pos.X = 1
+		st.Invaders[i].Vec.X = 1
以降判定処理が続く
```

## フェーズ4
[フェーズ3...4差分](https://github.com/miyaz/invaders/compare/phase3...phase4)

### 弾を発射してインベーダーを撃墜

スペースキーで戦闘機から弾を発射できるようにしました。
（ちなみに戦闘機のデザインは娘(小４)にやってもらいました！)

弾の発射ごとにgoroutineを生成して、それぞれがtickerで刻み、それをcontroller側でchannel経由で
通知を受け取ってY座標をインクリメントし描画する＝前に飛んでいく、という作りです。
（言葉で書くとわかりづらいな）

ここで、撃墜されたインベーダーを消す場合に配列だと扱いづらいのでmapに変更しました。
変更前) []invader
変更後) map[int]*invader

配列(正確にはスライス)の要素を消すのは下記のような関数でやればいいですが、インデックスがズレます（当然
配列インデックスでインベーダーを識別しているとバグになりやすい、というのがmapにする理由です。

```golang:配列要素削除関数の例
func remove(s []invader, i int) []invader {
  s = s[:i+copy(s[i:], s[i+1:])]
  return s
}
```
また、mapを使えばキーを指定してGo標準のdelete関数で消せるので簡単。
指定キーに紐づいたインベーダーを削除、ということができるのでコードもシンプルになるし、キーも連番ではなく
自由に作れるのもメリットですね。

## フェーズ5
[フェーズ4...5差分](https://github.com/miyaz/invaders/compare/phase4...phase5)

### 消えた弾丸goroutineをちゃんとクローズする

フェーズ４では、弾をうつほどCPU負荷が増えていきました。
インベーダーの体当たり以外にPCがフリーズするかも、という緊張感も楽しめるゲームに仕上がりました！（違う

原因は簡単で、弾の発射ごとに生成されるgoroutineが、外れる（画面外）or命中して消えた後も動き続けていました。
使い終わったgoroutineは止めてあげる必要があります（お作法

対処としては、弾(bullet)というstructに`CloseCh chan bool`を持たせて、生成したgoroutineにこのCloseChを
渡してcloseしたことを外から通知できるようにしました。
弾が外れる（画面外）or命中して消える際にgoroutineを止めるには、`close(CloseCh)` を呼び出せばいいだけです。

```diff:
 //タイマーイベント
-func moveLoop(moveCh chan int, mover, ticker int) {
+func moveLoop(moveCh chan int, closeCh chan bool, mover, ticker int) {
    t := time.NewTicker(time.Duration(ticker) * time.Millisecond)
+   defer t.Stop()
    for {
        select {
        case <-t.C: //タイマーイベント
            moveCh <- mover
            break
+       case <-closeCh:
+           return
        }
     }
-    t.Stop()
 }
```

## フェーズ6(完成)
[フェーズ5...6差分](https://github.com/miyaz/invaders/compare/phase5...master)

### ゲームとして整えた

* 画面上部にスコアやライフを表示してゲームっぽく
    * おまけで、goroutine数をリアルタイムで表示するNumGoroutine欄をつけました
* インベーダーは３回命中で撃墜に変更（難易度・ゲーム性UP

# おわりに

TUIでも動きのあるアプリが簡単に作れるtermbox-go、使いやすい！！と思いました。
また、非同期でタイマー/キーイベントで動いている物体（インベーダー、弾丸,戦闘機）同士の衝突判定や処理が
簡単にできるのも並列処理を書きやすいGoだからこそだな、と実感しました。

最後に、実はこのゲームにはバグがあって簡単に最高得点をとることができます。
興味があれば探してみてください！