Go Ebiten小游戏开发:贪吃蛇
贪吃蛇是一款经典的小游戏,玩法简单却充满乐趣。本文将介绍如何使用 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].X
g.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 main
import (
"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 = 0
if !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].X
g.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 // 重置计数器
}
原文地址:https://blog.csdn.net/qq_22328011/article/details/145102364
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!