tcellで球を常に動かしつつ、キーボードの入力を許可する


ターミナル上に出力した球(●)を常に動かせるようにしたいとする。

この要件を満たす為には、

package main

import (
	"time"

	"github.com/gdamore/tcell/v2"
)

func main() {
	screen, err := tcell.NewScreen()
	if err != nil {
		panic(err)
	}
	if err := screen.Init(); err != nil {
		panic(err)
	}

	defer screen.Fini()

	pos_x, pos_y := 5, 5
	ball := '●'

	for {
		screen.Clear()
		screen.SetContent(pos_x, pos_y, ball, nil, tcell.StyleDefault)
		screen.Show()

		pos_x += 1
		time.Sleep(1 * time.Second)
	}
}

のようなコードにし、

pos_x += 1
time.Sleep(1 * time.Second)

で一秒毎に1ずつ右に移動するようにすれば良いですが、これだと終了することができません。


そこで、

package main

import (
	"time"

	"github.com/gdamore/tcell/v2"
)

func main() {
	screen, err := tcell.NewScreen()
	if err != nil {
		panic(err)
	}
	if err := screen.Init(); err != nil {
		panic(err)
	}

	defer screen.Fini()

	pos_x, pos_y := 5, 5
	ball := '●'

	for {
		ev := screen.PollEvent()
		switch ev := ev.(type) {
		case *tcell.EventKey:
			switch ev.Key() {
			case tcell.KeyEscape:
				return
			}
		default:
			screen.Clear()
			screen.SetContent(pos_x, pos_y, ball, nil, tcell.StyleDefault)
			screen.Show()

			pos_x += 1
			time.Sleep(1 * time.Second)
		}
	}
}

のようにEventKeyでEsc(エスケープ)を押した時に終了するように書き換えても、思ったような動きになってくれません。


意図通りの動作にならない要因は、

ev := screen.PollEvent()

で何らかのイベントが着火するまで待機(ポーリング)している為です。




球を常に動かすこととキーボードからのイベントを両立させる為には、球用のイベントループとキーボードからのイベントループを分けることで実現することが出来ます。


イベントループの切り分けは下記のようにします。

package main

import (
	"time"

	"github.com/gdamore/tcell/v2"
)

func handleKeyEvent(s tcell.Screen, ch chan<- int) {
	for {
		ev := s.PollEvent()
		switch ev := ev.(type) {
		case *tcell.EventKey:
			switch ev.Key() {
			case tcell.KeyEscape:
				ch <- 1
			}
		}
	}
}

func main() {
	screen, err := tcell.NewScreen()
	if err != nil {
		panic(err)
	}
	if err := screen.Init(); err != nil {
		panic(err)
	}

	defer screen.Fini()

	ch := make(chan int)
	go handleKeyEvent(screen, ch)

	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	pos_x, pos_y := 5, 5
	ball := '●'

	for {
		select {
		case <-ticker.C:
			screen.Clear()
			screen.SetContent(pos_x, pos_y, ball, nil, tcell.StyleDefault)
			screen.Show()

			pos_x += 1
		case i := <-ch:
			if i == 1 {
				return
			}
		}
	}
}

キーボード操作のイベントループは

func handleKeyEvent(s tcell.Screen, ch chan<- int) {
	for {
		ev := s.PollEvent()
		switch ev := ev.(type) {
		case *tcell.EventKey:
			switch ev.Key() {
			case tcell.KeyEscape:
				ch <- 1
			}
		}
	}
}

のように関数の形式にしておきます。

この関数はチャネルを用いており、main関数(エンドポイント)内で

ch := make(chan int)
go handleKeyEvent(screen, ch)

のようにチャネル用の変数を用意した後にキーボード用のイベントループを実行します。

この時、関数の実行の前に go を追加することで、ゴルーチン(goroutine:並行処理)として実行しておきます。


後は

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

とエンドポイント内のイベントループ内で

select {
case <-ticker.C:
	screen.Clear()
	screen.SetContent(pos_x, pos_y, ball, nil, tcell.StyleDefault)
	screen.Show()

	pos_x += 1
case i := <-ch:
	if i == 1 {
		return
	}
}

のように毎秒毎の実行とキーボード用のイベントループから送信された値を受取り終了する処理を書いておきます。


処理がぶつかりそうな箇所がありましたら、その都度ゴルーチンを導入すべきか?を検討すると良いでしょう。

同じカテゴリーの記事
マインクラフト用ビジュアルエディタを開発しています。
詳しくはinunosinsi/mcws_blockly - githubをご覧ください。