贪吃蛇是一款经典的小游戏,玩法简单却充满乐趣。本文将介绍如何使用 Go 语言和 Ebiten 游戏引擎开发一个简单的贪吃蛇游戏。通过这个项目,你可以学习到游戏开发的基本流程、Ebiten 的使用方法以及如何用 Go 实现游戏逻辑。
项目简介
贪吃蛇的核心玩法是控制一条蛇在网格中移动,吃掉随机生成的食物,每吃一个食物蛇身会变长,同时得分增加。如果蛇撞到墙壁或自己的身体,游戏结束。
本项目使用 Go 语言和 Ebiten 游戏引擎实现。Ebiten 是一个轻量级的 2D 游戏引擎,非常适合开发小游戏。
开发环境
- Go 版本:1.20+
- Ebiten 版本:v2.5.0+
- 开发工具:VS Code 或 GoLand
安装 Ebiten:
go mod init snake
go get -u github.com/hajimehoshi/ebiten/v2
游戏设计
游戏元素
- 蛇:由头部和身体组成,头部控制移动方向,身体跟随头部移动。
- 食物:随机出现在网格中,蛇吃到食物后身体变长。
- 网格:游戏区域被划分为固定大小的网格,蛇和食物都位于网格中。
游戏规则
- 蛇每移动一格,身体跟随头部移动。
- 吃到食物后,蛇身变长,食物重新生成。
- 如果蛇撞到墙壁或自己的身体,游戏结束。
实现细节
游戏状态
游戏的核心状态由 Game
结构体管理,包括蛇的位置、食物位置、当前方向、分数等。
type Game struct {Head Pos // 蛇头位置Body []Pos // 蛇身位置列表Food Pos // 食物位置Dir int // 当前移动方向Score int // 当前分数GameOver bool // 游戏是否结束Paused bool // 游戏是否暂停TickCount int // 更新计数器
}
游戏循环
Ebiten 的游戏循环由 Update
和 Draw
方法实现:
- Update:处理游戏逻辑更新,如蛇的移动、碰撞检测、输入处理等。
- Draw:绘制游戏画面,包括蛇、食物和分数。
蛇的移动
蛇的移动通过更新头部位置,并将身体各部分依次移动到前一个部分的位置实现。
func (g *Game) Next() {// 移动蛇身for i := len(g.Body) - 1; i > 0; i-- {g.Body[i] = g.Body[i-1]}g.Body[0] = g.Head// 移动蛇头g.Head.X += Direction[g.Dir].Xg.Head.Y += Direction[g.Dir].Y
}
碰撞检测
碰撞检测分为两种情况:
- 撞墙:蛇头超出网格范围。
- 撞自己:蛇头与身体任何部分重合。
func (g *Game) IsDead() bool {// 检查是否撞墙if g.Head.X < 0 || g.Head.X >= GridSize || g.Head.Y < 0 || g.Head.Y >= GridSize {return true}// 检查是否撞到自己for _, pos := range g.Body {if g.Head == pos {return true}}return false
}
食物生成
食物需要随机生成在网格中,且不能与蛇的身体重合。
func (g *Game) SpawnFood() {for {x := rand.IntN(GridSize)y := rand.IntN(GridSize)if !g.IsOccupied(x, y) {g.Food = Pos{x, y}break}}
}
输入处理
通过检测键盘输入来控制蛇的移动方向,并支持暂停和重置游戏。
func (g *Game) HandleInput() {if ebiten.IsKeyPressed(ebiten.KeyEscape) {os.Exit(0) // 按下 Esc 键退出游戏}if ebiten.IsKeyPressed(ebiten.KeyP) {g.Paused = !g.Paused // 按下 P 键切换暂停状态}if !g.Paused && !g.GameOver {// 处理方向键输入if ebiten.IsKeyPressed(ebiten.KeyLeft) && g.Dir != RIGHT {g.Dir = LEFT}if ebiten.IsKeyPressed(ebiten.KeyRight) && g.Dir != LEFT {g.Dir = RIGHT}if ebiten.IsKeyPressed(ebiten.KeyUp) && g.Dir != DOWN {g.Dir = UP}if ebiten.IsKeyPressed(ebiten.KeyDown) && g.Dir != UP {g.Dir = DOWN}}
}
运行效果
运行游戏后,你会看到一个简单的贪吃蛇界面:
- 使用方向键控制蛇的移动。
- 吃到食物后,蛇身变长,分数增加。
- 如果蛇撞到墙壁或自己的身体,游戏结束,按下
R
键可以重新开始。
完整代码
package mainimport ("fmt""image/color""math/rand/v2""os""github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/ebitenutil""github.com/hajimehoshi/ebiten/v2/vector"
)const (GridSize int = 40 // 网格大小(每个格子的大小)BlockSize float32 = 20 // 每个格子的像素大小WindowWidth int = GridSize * int(BlockSize) // 窗口宽度WindowHeight int = GridSize * int(BlockSize) // 窗口高度InitialTPS int = 5 // 初始每秒更新次数(游戏速度)ScorePerFood int = 10 // 每吃一个食物增加的分数
)const (RIGHT = iota // 右方向DOWN // 下方向UP // 上方向LEFT // 左方向
)var (HeadColor color.Color = color.NRGBA{0, 0, 255, 255} // 蛇头颜色BodyColor color.Color = color.NRGBA{255, 255, 255, 255} // 蛇身颜色FoodColor color.Color = color.NRGBA{255, 0, 0, 255} // 食物颜色
)// Pos 表示一个二维坐标
type Pos struct {X, Y int
}// Direction 表示四个方向的移动向量
var Direction [4]Pos = [4]Pos{{1, 0}, {0, 1}, {0, -1}, {-1, 0}}// Game 表示游戏的状态
type Game struct {Head Pos // 蛇头位置Body []Pos // 蛇身位置列表Food Pos // 食物位置Dir int // 当前移动方向Score int // 当前分数GameOver bool // 游戏是否结束Paused bool // 游戏是否暂停TickCount int // 更新计数器
}// Update 是游戏的主更新逻辑
func (g *Game) Update() error {if g.GameOver {// 如果游戏结束,检测是否按下 R 键来重置游戏if ebiten.IsKeyPressed(ebiten.KeyR) {g.Reset()}return nil}g.TickCount++if g.TickCount >= 60/InitialTPS {g.TickCount = 0if !g.Paused {g.Next() // 更新游戏状态}}g.HandleInput() // 处理玩家输入return nil
}// Draw 是游戏的主绘制逻辑
func (g *Game) Draw(screen *ebiten.Image) {DrawGameState(screen, g)
}// Layout 设置游戏窗口的布局
func (g *Game) Layout(outerWidth, outerHeight int) (int, int) {return WindowWidth, WindowHeight
}func main() {ebiten.SetWindowTitle("Snake") // 设置窗口标题ebiten.SetWindowSize(WindowWidth, WindowHeight) // 设置窗口大小game := &Game{}game.Reset() // 初始化游戏状态if err := ebiten.RunGame(game); err != nil {panic(err)}
}// DrawGameState 绘制游戏状态
func DrawGameState(screen *ebiten.Image, g *Game) {// 绘制食物vector.DrawFilledRect(screen, float32(g.Food.X)*BlockSize, float32(g.Food.Y)*BlockSize, BlockSize, BlockSize, FoodColor, true)// 绘制蛇头vector.DrawFilledRect(screen, float32(g.Head.X)*BlockSize, float32(g.Head.Y)*BlockSize, BlockSize, BlockSize, HeadColor, true)// 绘制蛇身for _, pos := range g.Body {vector.DrawFilledRect(screen, float32(pos.X)*BlockSize, float32(pos.Y)*BlockSize, BlockSize, BlockSize, BodyColor, true)}// 绘制分数scoreText := fmt.Sprintf("Score: %d", g.Score)ebitenutil.DebugPrint(screen, scoreText)// 如果游戏结束,显示游戏结束信息if g.GameOver {ebitenutil.DebugPrintAt(screen, "Game Over! Press R to restart.", WindowWidth/2-100, WindowHeight/2)}
}// Next 更新游戏状态
func (g *Game) Next() {// 检查蛇是否吃到食物if g.Head == g.Food {g.Body = append(g.Body, g.Body[len(g.Body)-1]) // 增加蛇身长度g.Score += ScorePerFood // 增加分数g.SpawnFood() // 生成新的食物}// 移动蛇身for i := len(g.Body) - 1; i > 0; i-- {g.Body[i] = g.Body[i-1]}g.Body[0] = g.Head// 移动蛇头g.Head.X += Direction[g.Dir].Xg.Head.Y += Direction[g.Dir].Y// 检查是否碰撞if g.IsDead() {g.GameOver = true}
}// SpawnFood 生成新的食物
func (g *Game) SpawnFood() {for {x := rand.IntN(GridSize)y := rand.IntN(GridSize)if !g.IsOccupied(x, y) {g.Food = Pos{x, y}break}}
}// IsOccupied 检查某个位置是否被蛇占据
func (g *Game) IsOccupied(x, y int) bool {if g.Head.X == x && g.Head.Y == y {return true}for _, pos := range g.Body {if pos.X == x && pos.Y == y {return true}}return false
}// IsDead 检查蛇是否死亡(撞墙或撞到自己)
func (g *Game) IsDead() bool {// 检查是否撞墙if g.Head.X < 0 || g.Head.X >= GridSize || g.Head.Y < 0 || g.Head.Y >= GridSize {return true}// 检查是否撞到自己for _, pos := range g.Body {if g.Head == pos {return true}}return false
}// HandleInput 处理玩家输入
func (g *Game) HandleInput() {if ebiten.IsKeyPressed(ebiten.KeyEscape) {os.Exit(0) // 按下 Esc 键退出游戏}if ebiten.IsKeyPressed(ebiten.KeyP) {g.Paused = !g.Paused // 按下 P 键切换暂停状态}if !g.Paused && !g.GameOver {// 处理方向键输入if ebiten.IsKeyPressed(ebiten.KeyLeft) && g.Dir != RIGHT {g.Dir = LEFT}if ebiten.IsKeyPressed(ebiten.KeyRight) && g.Dir != LEFT {g.Dir = RIGHT}if ebiten.IsKeyPressed(ebiten.KeyUp) && g.Dir != DOWN {g.Dir = UP}if ebiten.IsKeyPressed(ebiten.KeyDown) && g.Dir != UP {g.Dir = DOWN}}
}// Reset 重置游戏状态
func (g *Game) Reset() {g.Head = Pos{2, 0} // 初始化蛇头位置g.Body = []Pos{{1, 0}, {0, 0}} // 初始化蛇身g.Food = Pos{rand.IntN(GridSize), rand.IntN(GridSize)} // 初始化食物位置g.Dir = RIGHT // 初始方向向右g.Score = 0 // 重置分数g.GameOver = false // 重置游戏结束状态g.Paused = false // 重置暂停状态g.TickCount = 0 // 重置计数器
}