GO勇闯贪吃蛇|青训营
贪吃蛇游戏
游戏规则
贪吃蛇是一款经典的电子游戏,最早在1976年出现在电脑上。游戏的目标是控制一条蛇,在一个有障碍物的区域内移动,并通过吃食物来增长自己的长度。玩家需要操作蛇头的移动方向,使蛇身不撞到墙壁或自己的身体,否则游戏结束。
在开始时,蛇只有很短的身体长度,而玩家需要不断地让它吃到食物,每吃到一个食物,蛇的身体长度会增加一节。游戏的难度会逐渐增加,食物会以较快的速度出现,同时蛇的移动速度也会增加。玩家需要灵活地操作蛇头的动作,以避免撞墙或撞到自己的身体。
贪吃蛇游戏简单易懂、容易上手,但随着时间的推移,蛇身越来越长,操作的难度也逐渐增加。因此,这款游戏需要玩家具备良好的观察能力和对策略的思考,以便在有限的空间内维持蛇身的移动并获取更高的分数。
为什么选择贪吃蛇
- 简单易懂:贪吃蛇是一个相对简单的游戏,规则简单明确,逻辑清晰。对于初学者来说,通过实现一个贪吃蛇游戏可以比较容易地理解程序的基本结构和控制流程,有助于建立起对编程语言的基本理解。
- 实践操作:编程语言的学习需要实践操作来加深对知识的理解和应用能力。贪吃蛇游戏提供了一个相对小规模的项目,适合初学者进行实践编程。通过亲自编写代码、调试错误、运行程序并观察结果,可以帮助初学者更好地理解编程语言的各种概念和语法。
- 综合应用:贪吃蛇游戏涉及到多个编程概念的综合应用,如数据结构(如蛇的身体长度和位置的表示)、控制流程(如判断蛇是否撞墙或撞到自身)、用户交互(如接收玩家输入控制蛇头移动方向)、图形界面(如绘制游戏画面)。通过编写贪吃蛇游戏,可以锻炼编程思维和解决问题的能力。
- 可扩展性:贪吃蛇游戏作为一个相对简单的项目,在实现基本功能后,还可以进行扩展。学习者可以尝试增加难度、增加特殊道具或规则,添加多玩家功能等等,以此进一步提升对编程语言的理解和应用能力。
设计要点
为了实现简单的贪吃蛇游戏,需要以下设计:
point
:表示游戏区域中的点坐标,具有 x 和 y 两个整型字段。initGame()
:初始化游戏状态,包括蛇的初始位置、移动方向、食物的位置、分数等。placeFood()
:随机放置食物,确保食物不与蛇身重叠。contains(points []point, p point) bool
:检查点 p 是否在点集 points 中。updateGame(dir string)
:根据输入的方向更新游戏状态,包括移动蛇、判断碰撞墙壁或自身、吃到食物等。clearScreen()
:清屏,用于在终端中清除之前的游戏画面。drawGame()
:绘制游戏画面,包括游戏区域、蛇、食物、得分等信息。gameLoop()
:游戏主循环,不断接受用户输入方向并更新游戏状态,直到游戏结束。main()
:初始化游戏并启动游戏循环。
设计过程
-
包的导入和使用:使用了
fmt
、math/rand
、os
、os/exec
和time
包。 -
变量声明和赋值:使用
const
声明常量,使用var
声明变量,并进行赋值
const (
width = 20
height = 10
)
var (
snake []point
food point
)
- 结构体的定义和使用:定义
point
结构体来表示游戏区域中的点坐标,并使用结构体字段来访问和操作。
type point struct {
x, y int
}
...
p := point{2, 4}
fmt.Println(p.x, p.y)
- 切片的使用:使用切片
snake
来表示蛇的身体,使用切片操作对蛇的身体进行增加和删除。
snake := []point{{2, 4}, {2, 5}, {2, 6}}
...
snake = append(snake, point{2, 7})
snake = snake[1:]
- 控制流程语句 - switch-case:
switch direction {
case "up":
// 向上移动逻辑
case "down":
// 向下移动逻辑
case "left":
// 向左移动逻辑
case "right":
// 向右移动逻辑
default:
fmt.Println("Invalid direction")
}
- 函数的定义和调用:
func placeFood() {
// 放置食物逻辑
}
...
placeFood()
- 随机数生成:使用
math/rand
包生成随机数,通过种子初始化随机数生成器。
rand.Seed(time.Now().UnixNano())
x := rand.Intn(width)
y := rand.Intn(height)
- 用户输入的处理:
var direction string
fmt.Scanln(&direction)
...
updateGame(direction)
- 外部命令执行:使用
os/exec
包中的Command
结构体和相关方法,调用系统命令实现清屏功能。
cmd := exec.Command("clear") // for macOS and Linux
// cmd := exec.Command("cmd", "/c", "cls") // for Windows
cmd.Stdout = os.Stdout
_ = cmd.Run()
- 时间延迟:使用
time.Sleep()
函数来控制游戏画面的刷新速度。
time.Sleep(200 * time.Millisecond)
- 字符串格式化输出:使用
fmt.Printf()
函数来格式化输出游戏得分信息。
score := 10
fmt.Printf("Your score: %d\n", score)
代码实现和进阶
简单实现
package main
import (
"fmt"
"math/rand"
"os"
"os/exec"
"time"
)
const (
width = 20
height = 20
)
var (
snake []point
direction point
food point
score int
gameOver bool
gameWin bool
scoreNeeded int
needRedraw bool = true // 添加全局变量needRedraw
)
// 游戏区域点坐标
type point struct {
x, y int
}
func initGame() {
snake = []point{
{x: width / 2, y: height / 2},
}
direction = point{x: 1, y: 0}
placeFood()
score = 0
gameOver = false
gameWin = false
scoreNeeded = 30
}
// 随机放置食物
func placeFood() {
rand.Seed(time.Now().UnixNano())
for {
food = point{
x: rand.Intn(width),
y: rand.Intn(height),
}
// 检查食物是否与蛇身重叠
if !contains(snake, food) {
break
}
}
}
// 检查点是否在切片中
func contains(points []point, p point) bool {
for _, pt := range points {
if pt == p {
return true
}
}
return false
}
// 更新游戏状态
func updateGame(dir string) {
switch dir {
case "w":
if direction.y != 1 {
direction = point{x: 0, y: -1}
}
case "s":
if direction.y != -1 {
direction = point{x: 0, y: 1}
}
case "a":
if direction.x != 1 {
direction = point{x: -1, y: 0}
}
case "d":
if direction.x != -1 {
direction = point{x: 1, y: 0}
}
case "q":
gameOver = true
}
head := snake[len(snake)-1]
newHead := point{
x: head.x + direction.x,
y: head.y + direction.y,
}
// 撞墙或撞到自身,游戏结束
if newHead.x < 0 || newHead.x >= width || newHead.y < 0 || newHead.y >= height || contains(snake, newHead) {
gameOver = true
return
}
snake = append(snake, newHead)
// 吃到食物,增加分数,放置新食物
if newHead == food {
score++
if score == scoreNeeded {
gameWin = true
gameOver = true
return
}
placeFood()
} else {
snake = snake[1:]
}
needRedraw = true // 设置needRedraw为true,需要重新绘制
}
// 清屏
func clearScreen() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
}
// 绘制游戏画面
func drawGame() {
clearScreen()
// 绘制游戏区域
fmt.Print(" ")
for i := 0; i < width; i++ {
fmt.Print("-")
}
fmt.Println()
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if x == 0 || x == width-1 {
fmt.Print("|")
} else if snake[len(snake)-1].x == x && snake[len(snake)-1].y == y {
fmt.Print("O")
} else if contains(snake[:len(snake)-1], point{x: x, y: y}) {
fmt.Print("o")
} else if food.x == x && food.y == y {
fmt.Print("@")
} else {
fmt.Print(" ")
}
}
// 打印换行符
fmt.Println()
}
fmt.Print(" ")
for i := 0; i < width; i++ {
fmt.Print("-")
}
fmt.Println()
fmt.Printf("Score: %d/%d\n", score, scoreNeeded)
needRedraw = false // 设置needRedraw为false,不需要重新绘制
}
// 游戏主循环
func gameLoop() {
for !gameOver {
if needRedraw {
drawGame()
}
var dir string
fmt.Scanln(&dir)
updateGame(dir)
time.Sleep(200 * time.Millisecond)
}
drawGame()
if gameWin {
fmt.Println("Congratulations! You win!")
} else {
fmt.Println("Game Over!")
}
}
func main() {
initGame()
gameLoop()
}
运行过的朋友们可以明显看出,上面的代码运行出来可以看得出的简单粗暴。在运行控制台窗口一遍又一遍的输出当前蛇和食物的状态。那么,可以有哪些优化呢?
Ebiten游戏库
Ebiten
是一个用于创建2D游戏和图形应用程序的轻量级、简单易用的Go语言游戏库。Ebiten提供了高性能的图形渲染和输入处理,使开发者可以快速构建交互式的2D
游戏。
以下是Ebiten库的一些主要特点和功能:
- 简单易用:Ebiten提供简洁而直观的API,使开发者能够快速入门并开始构建游戏。它的API设计得非常类似于HTML5的Canvas API,因此对于有Web开发经验的人来说更加容易上手。
- 高性能:Ebiten充分利用了现代硬件的GPU加速,通过OpenGL实现了快速的图形渲染。此外,Ebiten还针对性能进行了优化,使得游戏能够在各种平台上运行流畅。
- 跨平台支持:Ebiten支持在多个平台上运行,包括Windows、macOS、Linux和Web浏览器等。你可以使用相同的代码构建可在不同平台上运行的游戏,并且无需太多额外的工作。
- 输入处理:Ebiten提供了简单而灵活的输入处理功能,包括键盘、鼠标和触摸屏输入。开发者可以轻松地监听输入事件,以响应用户的交互。
- 声音和音效:Ebiten还支持加载和播放声音文件,使开发者可以为游戏添加音效或背景音乐。
- 动画和特效:Ebiten提供了一些用于实现动画和特效的工具和函数,例如帧动画、平滑移动和淡入淡出等。
- 轻量级:Ebiten是一个非常轻量级的库,没有过多的依赖和复杂的配置。这使得它适合用于开发小型游戏和原型,同时也减少了学习和使用的难度。
贪吃蛇中的Ebiten帮助创建游戏窗口
-
创建游戏窗口和画布大小:
const ( screenWidth = 400 screenHeight = 400 cellSize = 20 gridWidth = screenWidth / cellSize gridHeight = screenHeight / cellSize ) func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 400, 600 // 可根据需要更改窗口大小 } func main() { ebiten.SetWindowSize(400, 600) ebiten.SetWindowTitle("Snake Game") // ... }
-
绘制图形:
type Cell struct { x, y int } func (s *Snake) draw(screen *ebiten.Image) { for _, cell := range s.body { ebitenutil.DrawRect(screen, float64(cell.x*cellSize), float64(cell.y*cellSize), float64(cellSize), float64(cellSize), color.White) } } func (g *Game) Draw(screen *ebiten.Image) { snake.draw(screen) if gameOver { ebitenutil.DebugPrint(screen, "Game Over") } }
-
键盘输入检测:
func update() error { if ebiten.IsKeyPressed(ebiten.KeyArrowUp) && snake.direction != Down && !snake.movedThisFrame { snake.direction = Up snake.movedThisFrame = true } // 其他键盘输入检测的逻辑... return nil }
-
游戏循环:
func (g *Game) Update() error { return update() } func main() { // ... if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }
由于能力有限,目前的相关代码还没解决完bug就先不放上来了。
syscall包
syscall
包是 Go 语言标准库中的一个包,它提供了和操作系统底层交互的功能,可以用来调用底层的系统调用(system calls)。
通过 syscall
包,我们可以执行一些底层操作,如创建和控制进程、读写文件、网络通信等。这个包针对不同操作系统提供了不同的实现,因此可以在不同平台上进行系统级编程。
syscall
包提供了一系列的函数和常量,用于与操作系统进行交互。例如,syscall.Syscall()
函数可以用来调用特定的系统调用,syscall.Open()
函数用于打开文件,syscall.Read()
函数用于读取文件内容等。
需要注意的是,由于 syscall
包操作底层资源,并且与操作系统密切相关,使用时需要小心谨慎,确保正确处理错误和资源释放,以保证程序的稳定性和安全性。
贪吃蛇中的syscall.NewLazyDLL()函数来加载 Windows 平台上的动态链接库
我在网上搜索如何解决简单版本贪吃蛇窗口不断在运行控制台窗口一遍又一遍的输出当前蛇和食物的状态的问题时,看见了一位大佬的博客Golang 控制台百行代码贪吃蛇小游戏,Golang不适合在前端工作,缺少强大的图形图像包和硬件加速包,更适合做成后台服务程序。所以他的贪吃蛇小游戏运行在控制台上,其中调用了Window系统kernel32 dI中控制台相关的函数。
我简单看了一下,大概内容如下:
首先,代码通过NewLazyDLL
函数加载了名为kernel32.dll
的动态链接库。NewLazyDLL
函数返回一个*LazyDLL
类型的值,表示已加载的动态链接库。kernel32.dll
是Windows操作系统中一个非常重要的动态链接库,其中包含了很多常用的系统函数。
然后,代码使用NewProc
函数从加载的kernel32.dll
中获取API函数的地址。NewProc
函数接受一个字符串参数,表示要获取的函数名称。它返回一个*Proc
类型的值,表示指定函数的地址。
通过*Proc
类型的值,我们可以来调用相应的API函数。在代码中,使用了proc_get_console_screen_buffer_info
、proc_set_console_cursor_position
、proc_read_console_input
和proc_wait_for_multiple_objects
等变量来保存相应函数的地址。
当我们需要调用这些API函数时,可以使用Call
方法来实现。Call
方法接受函数的参数,并将其转换为对应的指针类型进行传递。根据函数的返回值类型,我们可以通过Call
方法获取返回值。
所以,通过syscall
包的NewLazyDLL
和NewProc
函数,我们可以加载动态链接库并获取API函数的地址,然后使用Call
方法调用这些API函数来完成各种系统操作。
总结
贪吃蛇游戏是一款经典的电子游戏,玩家需要控制一条蛇在有障碍物的区域内移动,并吃到食物来增长自己的长度。所以有很多前辈都尝试用各种语言去实现和优化这个游戏。我这只是简单作为一个熟悉GO的练习,肯定存在许多不足,希望大家批评指正。