golang实现跨平台单文件无依赖贪吃蛇游戏

作者:
淡白
创建时间:
2022-03-29 20:30:54
Go c

摘要:该代码是一个简单的贪吃蛇游戏的实现。主要包含以下几个部分: 1. 声明了一些需要使用的常量和变量,如蛇的类型、速度等。 2. 实现了清空终端、获取终端宽高的函数。 3. 初始化蛇的位置和身体。 4. 随机食物位置的函数。 5. 按键监听的函数,根据按键设置蛇的移动方向和加速开关。 6. 计算下一次移动位置和判断蛇是否撞到边界或自己的函数。 7. 绘制地图的函数。 以上就是该代码的主要内容。完整的代码可以查看作者的GitHub仓库。

定义声明需要的数据和结构

const (
	food      = 3
	head      = 2
	body      = 1
	baseSpeed = 100
)

var bodyArray []point   //蛇点位记录数组
var integral = 0        //积分
var vector = int64(6)   //方向 根据小数字键盘 2下 4左 6右 8上 方向来
var speed = 400         //附加间隔时间
var speedUp = uint32(0) //是否加速

type point struct {
	x    int
	y    int
	Type int
}

var foodP point

终端的操作

每一次的画面实现更新需要清除原来的输出再输出新的。 通过使用命令来清空控制台。 unix通过clear命令windows通过cls命令

//清空终端
func clear() {
	switch runtime.GOOS {
	case "linux", "darwin":
		cmd := exec.Command("clear")
		cmd.Stdout = os.Stdout
		_ = cmd.Run()
	case "windows":
		cmd := exec.Command("cmd", "/c", "cls")
		cmd.Stdout = os.Stdout
		_ = cmd.Run()
	}
}

直接使用终端的宽高来限定地图边界。 windows和unix系统都有stty这个命令

//获取终端宽高
func getWSZ() (int, int, error) {
	cmd := exec.Command("stty", "size")
	cmd.Stdin = os.Stdin
	out, err := cmd.Output()
	if err != nil {
		return 0, 0, err
	}
	a := strings.ReplaceAll(strings.ReplaceAll(string(out), "\r", ""), "\n", "")
	split := strings.Split(a, " ")
	h, _ := strconv.Atoi(split[0])
	w, _ := strconv.Atoi(split[1])
	return h, w, err
}

初始化工作

初始化蛇的位置和身体

默认出生在左上角向右移动

func iniBody() {
	var err error
	h, w, err = getWSZ()
	if err != nil {
		println(err.Error())
		return
	}
	h -= 2 //减去两行用来输出额外信息
	bodyArray = []point{
		{
			x:    2,
			y:    0,
			Type: head,
		},
		{
			x:    1,
			y:    0,
			Type: body,
		},
		{
			x:    0,
			y:    0,
			Type: body,
		},
	}
}

随机食物位置

func randFood() {
	x := rand.Int63n(int64(w))
	y := rand.Int63n(int64(h))
	flg := false
    //确保不会刷新到身体上
	for _, p := range bodyArray {
		if int64(p.x) == x && int64(p.y) == y {
			flg = true
		}
	}
	if flg {
		randFood()
		speed -= 10
	} else {
		foodP.x = int(x)
		foodP.y = int(y)
		foodP.Type = food
	}
}

按键监听

监听实现

原本我是直接使用的github.com/robotn/gohook库,但后来为了去掉依赖,使用cgo重新实现了下简单的监听。

/*
#cgo windows CFLAGS: -D CGO_OS_WINDOWS=1
#cgo darwin  CFLAGS: -D CGO_OS_DRWIN=1
#cgo linux   CFLAGS: -D CGO_OS_LINUX=1
#if defined(CGO_OS_DRWIN)
	#include <termios.h>
#elif defined(CGO_OS_LINUX)
	#include <termio.h>
#endif

#if defined(CGO_OS_WINDOWS)
	#include <conio.h>
	char getcharGo(){
		return getch();
	}
#else
	#include <stdio.h>
	char getcharGo()
	{
	int in;
	struct termios new_settings;
	struct termios stored_settings;
	tcgetattr(0,&stored_settings);
	new_settings = stored_settings;
	new_settings.c_lflag &= (~ICANON);
	new_settings.c_cc[VTIME] = 0;
	tcgetattr(0,&stored_settings);
	new_settings.c_cc[VMIN] = 1;
	tcsetattr(0,TCSANOW,&new_settings);
	in = getchar();
	tcsetattr(0,TCSANOW,&stored_settings);
	return in;
	}
#endif

*/
import "C"

按键处理

需要携程调用该方法,根据C.getcharGo()返回的按键来设置蛇的下一步移动方向 以及空格键的开关加速

func listeningInput() {
	for {
		result := C.getcharGo()
		if result == 32 {
			loadUint32 := atomic.LoadUint32(&speedUp)
			if loadUint32 == 0 {
				atomic.StoreUint32(&speedUp, uint32(1))
			} else {
				atomic.StoreUint32(&speedUp, uint32(0))
			}
		}
		switch result {
		case 119, 72, 65:
			if atomic.LoadInt64(&vector) != 2 {
				atomic.StoreInt64(&vector, 8)
			}
		case 97, 75, 68:
			if atomic.LoadInt64(&vector) != 6 {
				atomic.StoreInt64(&vector, 4)
			}
		case 115, 80, 66:
			if atomic.LoadInt64(&vector) != 8 {
				atomic.StoreInt64(&vector, 2)
			}
		case 100, 77, 67:
			if atomic.LoadInt64(&vector) != 4 {
				atomic.StoreInt64(&vector, 6)
			}

		}
	}
}

计算下一次移动位置和判断

func next() bool {
	//蛇的移动是去掉最后一个点位 把头部点位移到新的点位
	newP := point{
		x:    bodyArray[0].x,
		y:    bodyArray[0].y,
		Type: bodyArray[0].Type,
	}
	//根据方向移动
	switch atomic.LoadInt64(&vector) {
	case 8:
		newP.y -= 1
	case 2:
		newP.y += 1
	case 4:
		newP.x -= 1
	case 6:
		newP.x += 1
	}
	//添加到头部
	bodyArray = append([]point{newP}, bodyArray...)
	//如果没有吃到食物就去掉尾巴
	if !(newP.x == foodP.x && newP.y == foodP.y) {
		bodyArray = bodyArray[:len(bodyArray)-1]
	} else {
		//吃到食物就添加积分 随机新的食物
		randFood()
		integral++
	}
	//改变原来头部类型为身体
	bodyArray[1].Type = body
	//超过边界 结束
	if newP.y >= h || newP.y < 0 {
		return false
	}
	if newP.x >= w || newP.x < 0 {
		return false
	}
	//撞到自己
	for _, p := range bodyArray[1:] {
		if bodyArray[0].x == p.x && bodyArray[0].y == p.y {
			return false
		}
	}
	return true
}

绘制地图

func drawMap() {
	clear()
	drawBar()
	m := make(map[string]int)
	m[fmt.Sprintf("%d-%d", foodP.x, foodP.y)] = food
	for _, p := range bodyArray {
		m[fmt.Sprintf("%d-%d", p.x, p.y)] = p.Type
	}
	for i := 0; i < h; i++ {
		for j := 0; j < w; j++ {
			k := fmt.Sprintf("%d-%d", j, i)
			out := " "
			if v, ok := m[k]; ok {
				switch v {
				case body:
					out = "*"
				case head:
					out = "@"
				case food:
					out = "A"
				}
			}
			print(out)
		}
	}
	drawBottom()
}

完整源码

github