Help us understand the problem. What is going on with this article?

Goを使ってCLIにフラッシュ画を描画する

この記事はtomowarkar ひとりAdvent Calendar 2019の5日目の記事です。

今日は少し前に作ったCLIへのフラッシュ画像の描画について書いていきます。

はじめに

gogo.gif
こんな感じで一定時間スパンでn*m行のフィールドにランダムに⚪️🔵🔴を描画しています。

  • 時間ループ、キーアクションでGo Channelを使用
  • 描画範囲を構造体で定義

と少し発展的なGoの仕組みを学ぶのに役立ちました。

注意


今回こちらのライブラリを使用させていただきましたがREADMEにもあるように、すでに運用が終了しているライブラリです。(2019年12月5日現在)
使用の際はご注意ください。

なお、このライブラリでは代替案として以下のライブラリが挙げられています。

コード全文

コード全文は少し長くなるので折りたたんでおいておきます。
また、今回参考としてライブラリのデモを参考にしました。
参考: https://github.com/nsf/termbox-go/tree/master/_demos

コード全文



(クリックしてください)
flash.go
package main

import (
    "math/rand"
    "time"

    "github.com/nsf/termbox-go"
)

const coldef = termbox.ColorDefault

// Maze ...
type Maze struct {
    width  int
    height int
    field  [][]int
}

// InitMaze ...
func (m *Maze) InitMaze(h, w int) {
    m.width = w
    m.height = h
    m.field = make([][]int, h)
    for i := 0; i < h; i++ {
        m.field[i] = make([]int, w)
    }
}

// RandMaze ...
func (m *Maze) RandMaze() {
    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            m.field[j][i] = rand.Intn(3)
        }
    }
}

// DrawField ...
func (m Maze) DrawField() {
    termbox.Clear(coldef, coldef)

    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            if m.field[j][i] == 0 {
                termbox.SetCell(i*2, j, '⚪', coldef, coldef)
            } else if m.field[j][i] == 1 {
                termbox.SetCell(i*2, j, '🔵', coldef, coldef)
            } else {
                termbox.SetCell(i*2, j, '🔴', coldef, coldef)
            }
        }
    }
    termbox.Flush()
}

//key event
func keyEventLoop(kch chan termbox.Key) {
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            kch <- ev.Key
        default:
        }
    }
}

//time event
func timeEventLoop(tch chan bool, span int) {
    for {
        tch <- true
        time.Sleep(time.Duration(span) * time.Millisecond)
    }
}

func mainLoop(mz Maze, tch chan bool, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc, termbox.KeyCtrlC: //end event
                return
            }
        case <-tch: //time event
            mz.RandMaze()
            mz.DrawField()
            break
        default:
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

    rand.Seed(time.Now().UnixNano())

    var maze Maze
    maze.InitMaze(15, 15)

    kch := make(chan termbox.Key)
    tch := make(chan bool)
    go keyEventLoop(kch)
    go timeEventLoop(tch, 500)

    mainLoop(maze, tch, kch)
}

コード詳細

Step0. ライブラリを使うためのおまじない

ライブラリを使用するためのおまじないです。詳細はこちらをどうぞ。

Go で "rand" を使うときは Seedを設定しろってどこかで見た気がするので(忘れた)設定。
確か初期値が決まっていて厳密に乱数ではないからだった気がする。

step0.go
package main

import (
    "math/rand"
    "time"

    "github.com/nsf/termbox-go"
)

const coldef = termbox.ColorDefault

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

    rand.Seed(time.Now().UnixNano())
}

Step1. CLIに描画する構造体を定義

  • N×M行列を描画しようと思うので、構造体Mazeを定義。(※なんでMazeやねんとか言わないで🥺)
  • Mazeは描画するフィールド情報と、幅、高さの情報を持たせる。
  • また同時にMazeを初期化してN×M行列を作るInitMazeとランダムにMazeのフィールド情報を更新するRandMazeも作成。

今回のフィールド情報は⚪️🔵🔴の3つの情報をもち、それぞれ0,1,2でフィールド情報として持たせるのでrand.Intn(3)としています。

この辺りは直接数字を打ち込むのではなく、外で定義してから変数を入れるほうが良さそうですね(書きながら反省するスタイル)

step1.go
// Maze ...
type Maze struct {
    width  int
    height int
    field  [][]int
}

// InitMaze ...
func (m *Maze) InitMaze(h, w int) {
    m.width = w
    m.height = h
    m.field = make([][]int, h)
    for i := 0; i < h; i++ {
        m.field[i] = make([]int, w)
    }
}

// RandMaze ...
func (m *Maze) RandMaze() {
    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            m.field[j][i] = rand.Intn(3)
        }
    }
}

Step2. CLIに構造体を描画

次に描画する関数DrawFieldを書いていきます。

step2.go
// DrawField ...
func (m Maze) DrawField() {
    termbox.Clear(coldef, coldef)

    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            if m.field[j][i] == 0 {
                termbox.SetCell(i*2, j, '⚪', coldef, coldef)
            } else if m.field[j][i] == 1 {
                termbox.SetCell(i*2, j, '🔵', coldef, coldef)
            } else {
                termbox.SetCell(i*2, j, '🔴', coldef, coldef)
            }
        }
    }
    termbox.Flush()
}
  1. フィールドを初期化
  2. フィールド情報の0,1,2をそれぞれ⚪️🔵🔴に変換
  3. 描画

この流れです。
しかしこのままではコンマ秒単位で描画されるので、描画されていることを認識することができません。
なので以下のようにしてループを定義して描画の様子を確認してみます。

tmp.go
func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

    rand.Seed(time.Now().UnixNano())

    var maze Maze
    maze.InitMaze(15, 15)
    for i := 0; i < 10000; i++ {
        maze.DrawField()
    }
}

image.png
このような⚪️の15×15行列が確認できていれば成功です。

Step3. 描画イベントから抜け出すキーイベントを定義

いちいち描画時間を設定するのはナンセンスなので、描画を無限ループさせ、Escキーによって描画画面から脱出できるようにします。

描画のループとキーイベントは別軸で評価したいため、Go Channelを使用します。

なのでキーイベントを判定するループkeyEventLoopとメインの描画のループmainLoopをそれぞれ定義します。

step3.go
//key event
func keyEventLoop(kch chan termbox.Key) {
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            kch <- ev.Key
        default:
        }
    }
}

func mainLoop(mz Maze, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc: //end event
                return
            }
        default:
            mz.RandMaze()
            mz.DrawField()
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

    rand.Seed(time.Now().UnixNano())

    var maze Maze
    maze.InitMaze(15, 15)
    kch := make(chan termbox.Key)
    go keyEventLoop(kch)

    mainLoop(maze, kch)
}
  1. メインループで描画を更新、描画のループを行う。
  2. メインループが回っている間並行してキーイベントのループが回っていて、キーイベントがあった場合kchに情報を送る
  3. メインループはkchからキーイベントの情報を受け取り、キーイベントの情報によってイベントを実行(今回はEscキーでループの脱出)

これでコードを走らせると、すごい勢いで描画が⚪️🔵🔴に更新されていて、かつEscキーを押すことで描画画面から抜け出すことができることがわかります。

Step4. 時間イベントを定義して一定時間ごとに描画を更新

このままでは描画の更新が早すぎます。
一定時間ごとに描画が更新されるように時間イベントtimeEventLoopを設定し、メインループmainLoopを更新します

step4.go
//time event
func timeEventLoop(tch chan bool, span int) {
    for {
        tch <- true
        time.Sleep(time.Duration(span) * time.Millisecond)
    }
}

func mainLoop(mz Maze, tch chan bool, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc, termbox.KeyCtrlC: //end event
                return
            }
        case <-tch: //time event
            mz.RandMaze()
            mz.DrawField()
            break
        default:
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

    rand.Seed(time.Now().UnixNano())

    var maze Maze
    maze.InitMaze(15, 15)

    kch := make(chan termbox.Key)
    tch := make(chan bool)
    go keyEventLoop(kch)
    go timeEventLoop(tch, 500)

    mainLoop(maze, tch, kch)
}
  1. メインループは無限ループを行う。
  2. メインループが回っている間並行して時間イベントのループが回っていて任意の時間[ms]ごとにtchに情報を送る
  3. メインループはtchから時間イベントの情報を受け取り、時間イベントが発生して場合において描画の更新、再描画を行う。

という形になりました。

まとめ

これで、
- 500ms毎に15×15行列がランダムで更新され描画される
- Escキーを押すことで描画画面から脱出

ということができました。
行列の数や描画の更新時間を変えて遊んでみてください。

コードはGithub Gistにもおいておきます!

https://gist.github.com/tomowarkar/3bb50298393b66148e43e00238e52083

以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019

追記

先日(4日目)のアドベントカレンダーで同じくtermbox-goを使った記事を発見したので載せておきます。
TUI版インベーダーゲームをGo言語で作る

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away