Go Ebiten小游戏开发:贪吃蛇

Go Ebiten小游戏开发:贪吃蛇_第1张图片

贪吃蛇是一款经典的小游戏,玩法简单却充满乐趣。本文将介绍如何使用 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 的游戏循环由 UpdateDraw 方法实现:

  • 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                                        // 重置计数器
}

你可能感兴趣的:(小游戏开发,Go语言,golang,开发语言,后端)