この記事はtomowarkar ひとりAdvent Calendar 2019の5日目の記事です。
今日は少し前に作ったCLIへのフラッシュ画像の描画について書いていきます。
はじめに
こんな感じで一定時間スパンでn*m行のフィールドにランダムに⚪️🔵🔴を描画しています。
- 時間ループ、キーアクションでGo Channelを使用
- 描画範囲を構造体で定義
と少し発展的なGoの仕組みを学ぶのに役立ちました。
注意
今回こちらのライブラリを使用させていただきましたがREADMEにもあるように、すでに運用が終了しているライブラリです。(2019年12月5日現在)
使用の際はご注意ください。
なお、このライブラリでは代替案として以下のライブラリが挙げられています。
コード全文
コード全文は少し長くなるので折りたたんでおいておきます。
また、今回参考としてライブラリのデモを参考にしました。
参考: https://github.com/nsf/termbox-go/tree/master/_demos
コード全文
(クリックしてください)
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
を設定しろってどこかで見た気がするので(忘れた)設定。
確か初期値が決まっていて厳密に乱数ではないからだった気がする。
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)
としています。
この辺りは直接数字を打ち込むのではなく、外で定義してから変数を入れるほうが良さそうですね(書きながら反省するスタイル)
// 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
を書いていきます。
// 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()
}
- フィールドを初期化
- フィールド情報の0,1,2をそれぞれ⚪️🔵🔴に変換
- 描画
この流れです。
しかしこのままではコンマ秒単位で描画されるので、描画されていることを認識することができません。
なので以下のようにしてループを定義して描画の様子を確認してみます。
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()
}
}
Step3. 描画イベントから抜け出すキーイベントを定義
いちいち描画時間を設定するのはナンセンスなので、描画を無限ループさせ、Escキーによって描画画面から脱出できるようにします。
描画のループとキーイベントは別軸で評価したいため、Go Channelを使用します。
なのでキーイベントを判定するループkeyEventLoop
とメインの描画のループmainLoop
をそれぞれ定義します。
//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)
}
- メインループで描画を更新、描画のループを行う。
- メインループが回っている間並行してキーイベントのループが回っていて、キーイベントがあった場合
kch
に情報を送る - メインループは
kch
からキーイベントの情報を受け取り、キーイベントの情報によってイベントを実行(今回はEscキーでループの脱出)
これでコードを走らせると、すごい勢いで描画が⚪️🔵🔴に更新されていて、かつEscキーを押すことで描画画面から抜け出すことができることがわかります。
Step4. 時間イベントを定義して一定時間ごとに描画を更新
このままでは描画の更新が早すぎます。
一定時間ごとに描画が更新されるように時間イベントtimeEventLoop
を設定し、メインループmainLoop
を更新します
//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)
}
- メインループは無限ループを行う。
- メインループが回っている間並行して時間イベントのループが回っていて任意の時間[ms]ごとに
tch
に情報を送る - メインループは
tch
から時間イベントの情報を受け取り、時間イベントが発生して場合において描画の更新、再描画を行う。
という形になりました。
まとめ
これで、
- 500ms毎に15×15行列がランダムで更新され描画される
- Escキーを押すことで描画画面から脱出
ということができました。
行列の数や描画の更新時間を変えて遊んでみてください。
コードはGithub Gistにもおいておきます!
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019
追記
先日(4日目)のアドベントカレンダーで同じくtermbox-goを使った記事を発見したので載せておきます。
TUI版インベーダーゲームをGo言語で作る