LoginSignup
2
1

Go言語+WindowsTerminalで「ブロック崩し」を作ろうと挑戦したが四苦八苦[termbox-goは未使用]

Last updated at Posted at 2023-09-17

動作画面

↓version 0.9
PowerShell-2023-09-18-00-45-01.gif

↓version 2.0
PowerShell-2023-10-22-09-09-58.gif

説明:とりあえず動けば良いということで

  • ボールは、対角斜め(45度・135度・225度・315度)のみ動きます。
  • ブロックを全部消しても、ゲームクリアーにならないです。
  • ゲームオーバーも無し。

[version 0.9]

  • 白棒(バー)は、[a]を押すと左方向、[s]を押すと右方向に動きます。
  • スタート画面はないです。コンパイルすると、いきなりゲーム開始。
  • キー入力はライブラリの「mattn/go-tty」を使用した。
  • 「キー入力」と「画面表記」の非同期はゴルーチンを使用した。

[version 2.0]

  • 白棒(バー)は、[←]を押すと左方向、[→]を押すと右方向に動きます。
  • スタート画面を追加しました。
  • キー入力はWin32の「GetAsyncKeyState」を使用した。
  • Win32なので「キー入力」と「画面表記」の非同期は不要となり、ゴルーチンを削除した。

車輪の再発明

↓先人が2016年に今回と同じテーマを投稿しており、車輪(タイヤ)の再発名ですが、私個人として「色」と「日本語や絵文字」がGo言語の定番ライブラリ「termbox-go」だと思い通りに使用できなかったので改めて挑戦しました。

創意工夫した点

  • 背景やブロックはANSIエスケープシーケンスを使って着色🟥🟨🟦しています。
  • ボールは絵文字の「⚪」です。
  • キーボード入力イベントは、Go言語のライブラリ「github.com/mattn/go-tty」を使ってます。
  • キーボード入力イベントで検索すると、Go言語の定番TUI(Text User Interface) ライブラリ「termbox-go」がヒットしますが、上記で記載の通り、色や、日本語・絵文字をうまく表示してくれなかったので使用しませんでした。

苦労した点

[version 0.9]

  • 画面表示はfor文の永久ループ+ゴルーチンを使ってますが、キーイベントを途中に入れ込むことが出来ず苦労しました。
  • 最終的にミリ秒の等間隔で、キーイベントを実行させてむりやり入れ込んでます。
  • もっとエレガントな方法があれば、良かったんですが限界でした。

[version 2.0]

  • Win32の「GetAsyncKeyState」を使ったら多少エレガントになりました。
  • そもそも「Win32」って何?、「GetAsyncKeyState」って何?という話になりますが、長くなるので割愛。

パソコン環境

  • Windows10
  • WindowsTerminal 1.19.2831
  • go 1.21.3

参考にしたサイト(ブロック崩し)

参考にしたサイト(ANSIエスケープシーケンス)

参考にしたサイト(キー入力)

Githubで参考にしたGetAsyncKeyState

プログラム本文[Version0.9]

157行
約4100文字

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mattn/go-tty"
)

type Bar struct {
	Left int
	Top  int
	W    int
	H    int
}

type Ball struct {
	Left    float64
	Top     float64
	Speed_x float64
	Speed_y float64
}

type Block struct {
	Left   int
	Top    int
	Color  string
	Exists int
}

const tick_time_str time.Duration = 200
const tick_time_tty time.Duration = 10
const white string = "\033[48;2;255;255;255m \033[0m" //⬜
const red string = "\033[48;2;255;0;0m \033[0m"       //🟥
const blue string = "\033[48;2;0;0;255m \033[0m"      //🟦
const yellow string = "\033[48;2;255;255;0m \033[0m"  //🟨

func main() {
	fmt.Println("\033[2J") //画面全体を消去

	ball := Ball{
		Left: 15, Top: 5, Speed_x: 1, Speed_y: 1,
	}

	bar := Bar{
		Left: 7, Top: 27, W: 5, H: 1,
	}

	block_data := []Block{
		{2, 2, red, 1}, {4, 2, red, 1}, {6, 2, red, 1}, {8, 2, red, 1}, {10, 2, red, 1}, {12, 2, red, 1}, {14, 2, red, 1},
		{3, 3, blue, 1}, {4, 3, blue, 1}, {5, 3, blue, 1}, {6, 3, blue, 1}, {8, 3, blue, 1}, {9, 3, blue, 1}, {10, 3, blue, 1}, {11, 3, blue, 1}, {13, 3, blue, 1},
		{2, 4, yellow, 1}, {3, 4, yellow, 1}, {4, 4, yellow, 1}, {5, 4, yellow, 1}, {6, 4, yellow, 1}, {7, 4, yellow, 1}, {8, 4, yellow, 1}, {9, 4, yellow, 1}, {10, 4, yellow, 1},
	}

	chan_Print_str := make(chan string)
	go go_Print_str(chan_Print_str)
	defer close(chan_Print_str)

	tty, err := tty.Open()
	if err != nil {
		log.Fatal(err)
	}
	defer tty.Close()

	go func() {
		for range time.Tick(tick_time_str * time.Millisecond) {
			chan_Print_str <- creat_str(ball, bar, block_data)
			ball, block_data = ballHitCheck(ball, bar, block_data)
		}
	}()

	for range time.Tick(tick_time_tty * time.Millisecond) {
		r, err := tty.ReadRune()
		if err != nil {
			log.Fatal(err)
		}

		//aが左に、sが右に
		if string(r) == "s" { //r=115
			bar.Left++
		} else if string(r) == "a" { //r=97
			bar.Left--
		}

	} //for range time.Tick(tick_time_tty * time.Millisecond) {
}

func go_Print_str(chan_Print_str chan string) {
	fmt.Println("\033[2J\033[0;0H")
	for {
		//ゲーム画面を描画する
		fmt.Println(<-chan_Print_str)
		time.Sleep(tick_time_str * time.Millisecond)
	}
}

func creat_str(ball Ball, bar Bar, block []Block) string {

	str := "\033[0;0H" //カーソルを原点に戻す
	var pixel [22][32]string

	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			if float64(pixel_left) <= ball.Left && ball.Left < float64(pixel_left+1) && float64(pixel_top) <= ball.Top && ball.Top < float64(pixel_top+1) {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m⚪\033[0m"
			} else if bar.Left < pixel_left && bar.Left >= pixel_left-bar.W && bar.Top < pixel_top && bar.Top >= pixel_top-bar.H {
				pixel[pixel_left][pixel_top] = white
			} else {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m \033[0m"
			}

			for i, _ := range block {
				if pixel_top == block[i].Top && pixel_left == block[i].Left && block[i].Exists == 1 {
					pixel[pixel_left][pixel_top] = block[i].Color
				}
			}

		}
	}

	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			str += pixel[pixel_left][pixel_top]
		}
		str += "\n"
	}

	return str
}

func ballHitCheck(ball Ball, bar Bar, block []Block) (Ball, []Block) {
	ball.Left += ball.Speed_x
	ball.Top += ball.Speed_y
	//左右の壁と接触判定
	if ball.Left < 1 || ball.Left > 20 {
		ball.Speed_x = -ball.Speed_x
	}
	//上下の壁と接触判定
	if ball.Top < 1 || ball.Top > 30 {
		ball.Speed_y = -ball.Speed_y
	}
	//棒とボールの接触判定
	if ball.Top > float64(bar.Top) && ball.Top <= float64(bar.Top+bar.H) && ball.Left > float64(bar.Left) && ball.Left <= float64(bar.Left+bar.W) {
		ball.Speed_y = -ball.Speed_y
	}

	//ボールとブロックの接触判定
	for i, _ := range block {
		if ball.Top > float64(block[i].Top) && ball.Top <= float64(block[i].Top+1) && ball.Left > float64(block[i].Left) && ball.Left <= float64(block[i].Left+1) {
			ball.Speed_y = -ball.Speed_y
			block[i].Exists = 0
		}
	}

	return ball, block
}


プログラム本文[Version2.0]

160行
約4700文字

package main

import (
	"fmt"
	"time"

	"golang.org/x/sys/windows"
)

type Bar struct {
	Left int
	Top  int
	W    int
	H    int
}

type Ball struct {
	Left    float64
	Top     float64
	Speed_x float64
	Speed_y float64
}

type Block struct {
	Left   int
	Top    int
	Color  string
	Exists int
}

var game_start = 0

const game_time = 100
const white string = "\033[48;2;255;255;255m \033[0m" //⬜
const red string = "\033[48;2;255;0;0m \033[0m"       //🟥
const blue string = "\033[48;2;0;0;255m \033[0m"      //🟦
const yellow string = "\033[48;2;255;255;0m \033[0m"  //🟨
const start_screen string = `*--------------------*
|Go言語でブロック崩し|
*--------------------*

*開始するには「スペース」ボタンを押してください*`

var (
	moduser32            = windows.NewLazyDLL("user32.dll")
	procGetAsyncKeyState = moduser32.NewProc("GetAsyncKeyState")
)

func main() {
	//fmt.Println("\033[2J")    //画面全体を消去
	fmt.Println("\033[2J\033[0;0H")
	fmt.Println(start_screen) //画面全体を消去

	ball := Ball{
		Left: 15, Top: 5, Speed_x: 1, Speed_y: 1,
	}

	bar := Bar{
		Left: 7, Top: 27, W: 5, H: 1,
	}

	block_data := []Block{
		{2, 2, red, 1}, {4, 2, red, 1}, {6, 2, red, 1}, {8, 2, red, 1}, {10, 2, red, 1}, {12, 2, red, 1}, {14, 2, red, 1}, {16, 2, red, 1}, {18, 2, red, 1},
		{3, 3, blue, 1}, {4, 3, blue, 1}, {5, 3, blue, 1}, {6, 3, blue, 1}, {8, 3, blue, 1}, {9, 3, blue, 1}, {10, 3, blue, 1}, {11, 3, blue, 1}, {13, 3, blue, 1}, {14, 3, blue, 1}, {15, 3, blue, 1},
		{2, 4, yellow, 1}, {3, 4, yellow, 1}, {4, 4, yellow, 1}, {5, 4, yellow, 1}, {6, 4, yellow, 1}, {7, 4, yellow, 1}, {8, 4, yellow, 1}, {9, 4, yellow, 1}, {10, 4, yellow, 1}, {11, 4, yellow, 1}, {12, 4, yellow, 1}, {13, 4, yellow, 1}, {14, 4, yellow, 1},
	}

	for {
		asynch32, _, _ := procGetAsyncKeyState.Call(uintptr(32)) //space
		if asynch32 != 0 {
			game_start = 1
			break
		}
	}

	if game_start == 1 {
		fmt.Println("\033[2J\033[0;0H")
		for {
			//ゲーム画面を描画する
			ball, block_data = ballHitCheck(ball, bar, block_data)
			asynch37, _, _ := procGetAsyncKeyState.Call(uintptr(37)) //←
			if asynch37 != 0 {
				bar.Left--
			}

			asynch39, _, _ := procGetAsyncKeyState.Call(uintptr(39)) //→
			if asynch39 != 0 {
				bar.Left++
			}
			fmt.Println("asynch37", asynch37, asynch37&0x1)
			fmt.Println("asynch39", asynch39, asynch39&0x1)

			fmt.Println(creat_str(ball, bar, block_data))
			time.Sleep(game_time * time.Millisecond)
		}
	}
}

func creat_str(ball Ball, bar Bar, block []Block) string {
	str := "\033[0;0H" //カーソルを原点に戻す
	var pixel [22][32]string
	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		//玉と空間の位置情報をデータに格納
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			if float64(pixel_left) <= ball.Left && ball.Left < float64(pixel_left+1) && float64(pixel_top) <= ball.Top && ball.Top < float64(pixel_top+1) {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m⚪\033[0m"
			} else if bar.Left < pixel_left && bar.Left >= pixel_left-bar.W && bar.Top < pixel_top && bar.Top >= pixel_top-bar.H {
				pixel[pixel_left][pixel_top] = white
			} else {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m \033[0m"
			}

			//ブロックの位置情報をデータ格納
			for i, _ := range block {
				if pixel_top == block[i].Top && pixel_left == block[i].Left && block[i].Exists == 1 {
					pixel[pixel_left][pixel_top] = block[i].Color
				}
			}
		}
	}

	//strデータに、全位置情報を格納
	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			str += pixel[pixel_left][pixel_top]
		}
		str += "\n"
	}

	return str
}

// 接触判定
func ballHitCheck(ball Ball, bar Bar, block []Block) (Ball, []Block) {
	ball.Left += ball.Speed_x
	ball.Top += ball.Speed_y
	//左右の壁と接触判定
	if ball.Left < 1 || ball.Left > 20 {
		ball.Speed_x = -ball.Speed_x
	}
	//上下の壁と接触判定
	if ball.Top < 1 || ball.Top > 30 {
		ball.Speed_y = -ball.Speed_y
	}
	//棒とボールの接触判定
	if ball.Top > float64(bar.Top) && ball.Top <= float64(bar.Top+bar.H) && ball.Left > float64(bar.Left) && ball.Left <= float64(bar.Left+bar.W) {
		ball.Speed_y = -ball.Speed_y
	}

	//ボールとブロックの接触判定
	for i, _ := range block {
		if block[i].Exists == 1 && ball.Top > float64(block[i].Top) && ball.Top <= float64(block[i].Top+1) && ball.Left > float64(block[i].Left) && ball.Left <= float64(block[i].Left+1) {
			ball.Speed_x = -ball.Speed_x
			ball.Speed_y = -ball.Speed_y
			block[i].Exists = 0
		}
	}

	return ball, block
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1