Go Ebiten小游戏开发:俄罗斯方块

发布于:2025-03-11 ⋅ 阅读:(18) ⋅ 点赞:(0)

在这篇文章中,我们将一起开发一个简单的俄罗斯方块游戏,使用Go语言和Ebiten游戏库。Ebiten是一个轻量级的游戏库,适合快速开发2D游戏。我们将逐步构建游戏的基本功能,包括游戏逻辑、图形绘制和用户输入处理。
在这里插入图片描述

项目结构

我们的项目将包含以下主要部分:

  • 游戏状态管理
  • 方块生成与移动
  • 碰撞检测
  • 行消除与计分
  • 游戏界面绘制

游戏状态管理

首先,我们定义一个 Game 结构体来管理游戏的状态。它包含游戏板、当前方块、下一个方块、分数、等级等信息。

type Game struct {
	board        [][]int
	currentPiece *Piece
	nextPiece    *Piece // 下一个方块
	gameOver     bool
	dropTimer    int
	score        int  // 得分
	level        int  // 当前等级
	lines        int  // 已消除的行数
	paused       bool // 暂停状态
}

我们还需要定义一个 Piece 结构体来表示俄罗斯方块的形状和位置。

type Piece struct {
	shape [][]int
	x, y  int
	color int
}

初始化游戏

NewGame 函数中,我们初始化游戏状态,包括创建游戏板和生成初始方块。

func NewGame() *Game {
	game := &Game{
		board:     make([][]int, boardHeight),
		dropTimer: 0,
		level:     1,
		score:     0,
		lines:     0,
	}

	for i := range game.board {
		game.board[i] = make([]int, boardWidth)
	}

	game.nextPiece = game.generateNewPiece()
	game.spawnNewPiece()

	return game
}

游戏逻辑

Update 方法中,我们处理游戏逻辑,包括用户输入、方块移动和下落。

func (g *Game) Update() error {
	// 处理键盘输入
	if inpututil.IsKeyJustPressed(ebiten.KeyLeft) {
		g.moveLeft()
	}
	if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
		g.moveRight()
	}
	if ebiten.IsKeyPressed(ebiten.KeyDown) {
		g.moveDown()
	}
	if inpututil.IsKeyJustPressed(ebiten.KeyUp) {
		g.rotate()
	}

	// 控制方块下落速度
	g.dropTimer++
	if g.dropTimer >= dropSpeed {
		g.dropTimer = 0
		g.moveDown()
	}

	return nil
}

碰撞检测

我们需要检查方块是否可以移动或旋转,这通过 isValidPosition 方法实现。

func (g *Game) isValidPosition() bool {
	for y := 0; y < len(g.currentPiece.shape); y++ {
		for x := 0; x < len(g.currentPiece.shape[y]); x++ {
			if g.currentPiece.shape[y][x] != 0 {
				newX := g.currentPiece.x + x
				newY := g.currentPiece.y + y

				if newX < 0 || newX >= boardWidth || newY < 0 || newY >= boardHeight {
					return false
				}

				if g.board[newY][newX] != 0 {
					return false
				}
			}
		}
	}
	return true
}

行消除与计分

当方块锁定到游戏板时,我们需要检查是否有完整的行,并进行消除和计分。

func (g *Game) clearLines() {
	linesCleared := 0
	for y := boardHeight - 1; y >= 0; y-- {
		isFull := true
		for x := 0; x < boardWidth; x++ {
			if g.board[y][x] == 0 {
				isFull = false
				break
			}
		}

		if isFull {
			for moveY := y; moveY > 0; moveY-- {
				copy(g.board[moveY], g.board[moveY-1])
			}
			for x := 0; x < boardWidth; x++ {
				g.board[0][x] = 0
			}
			linesCleared++
			y++
		}
	}

	if linesCleared > 0 {
		g.lines += linesCleared
		g.score += []int{100, 300, 500, 800}[linesCleared-1] * g.level
		g.level = g.lines/10 + 1
	}
}

绘制游戏界面

最后,我们在 Draw 方法中绘制游戏界面,包括游戏板、当前方块、下一个方块和游戏信息。

func (g *Game) Draw(screen *ebiten.Image) {
	// 绘制游戏板
	for y := 0; y < boardHeight; y++ {
		for x := 0; x < boardWidth; x++ {
			if g.board[y][x] != 0 {
				drawBlock(screen, x, y, g.board[y][x])
			}
		}
	}

	// 绘制当前方块
	if g.currentPiece != nil {
		for y := 0; y < len(g.currentPiece.shape); y++ {
			for x := 0; x < len(g.currentPiece.shape[y]); x++ {
				if g.currentPiece.shape[y][x] != 0 {
					drawBlock(screen, g.currentPiece.x+x, g.currentPiece.y+y, g.currentPiece.color)
				}
			}
		}
	}

	// 绘制下一个方块预览
	if g.nextPiece != nil {
		for y := 0; y < len(g.nextPiece.shape); y++ {
			for x := 0; x < len(g.nextPiece.shape[y]); x++ {
				if g.nextPiece.shape[y][x] != 0 {
					drawBlock(screen, boardWidth+2+x, 4+y, g.nextPiece.color)
				}
			}
		}
	}

	// 绘制游戏信息
	ebitenutil.DebugPrint(screen, fmt.Sprintf("\nScore: %d\nLevel: %d\nLines: %d", g.score, g.level, g.lines))
}

结论

通过以上步骤,我们已经实现了一个基本的俄罗斯方块游戏。你可以在此基础上添加更多功能,比如音效、菜单、不同的方块形状等。希望这篇文章能帮助你入门Go语言游戏开发,并激发你创造更复杂的游戏项目!

完整代码

main.go

package main

import (
	"fmt"
	"image/color"
	"log"
	"math/rand"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
)

const (
	screenWidth  = 320
	screenHeight = 640
	blockSize    = 32
	boardWidth   = 10
	boardHeight  = 20
)

// Game 表示游戏状态
type Game struct {
	board        [][]int
	currentPiece *Piece
	nextPiece    *Piece // 下一个方块
	gameOver     bool
	dropTimer    int
	score        int  // 得分
	level        int  // 当前等级
	lines        int  // 已消除的行数
	paused       bool // 暂停状态
}

// Piece 表示俄罗斯方块的一个方块
type Piece struct {
	shape [][]int
	x, y  int
	color int
}

// NewGame 创建新游戏实例
func NewGame() *Game {
	game := &Game{
		board:     make([][]int, boardHeight),
		dropTimer: 0,
		level:     1,
		score:     0,
		lines:     0,
	}

	// 初始化游戏板
	for i := range game.board {
		game.board[i] = make([]int, boardWidth)
	}

	// 创建初始方块和下一个方块
	game.nextPiece = game.generateNewPiece()
	game.spawnNewPiece()

	return game
}

// Update 处理游戏逻辑
func (g *Game) Update() error {
	// 重启游戏
	if g.gameOver && inpututil.IsKeyJustPressed(ebiten.KeySpace) {
		*g = *NewGame()
		return nil
	}

	// 暂停/继续
	if inpututil.IsKeyJustPressed(ebiten.KeyP) {
		g.paused = !g.paused
		return nil
	}

	if g.gameOver || g.paused {
		return nil
	}

	// 处理键盘输入
	if inpututil.IsKeyJustPressed(ebiten.KeyLeft) {
		g.moveLeft()
	}
	if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
		g.moveRight()
	}
	if ebiten.IsKeyPressed(ebiten.KeyDown) {
		g.moveDown()
	}
	if inpututil.IsKeyJustPressed(ebiten.KeyUp) {
		g.rotate()
	}

	// 根据等级调整下落速度
	g.dropTimer++
	dropSpeed := 60 - (g.level-1)*5 // 每提升一级,加快5帧
	if dropSpeed < 20 {             // 最快速度限制
		dropSpeed = 20
	}
	if g.dropTimer >= dropSpeed {
		g.dropTimer = 0
		g.moveDown()
	}

	return nil
}

// Draw 绘制游戏画面
func (g *Game) Draw(screen *ebiten.Image) {
	// 绘制游戏板
	for y := 0; y < boardHeight; y++ {
		for x := 0; x < boardWidth; x++ {
			if g.board[y][x] != 0 {
				drawBlock(screen, x, y, g.board[y][x])
			}
		}
	}

	// 绘制当前方块
	if g.currentPiece != nil {
		for y := 0; y < len(g.currentPiece.shape); y++ {
			for x := 0; x < len(g.currentPiece.shape[y]); x++ {
				if g.currentPiece.shape[y][x] != 0 {
					drawBlock(screen,
						g.currentPiece.x+x,
						g.currentPiece.y+y,
						g.currentPiece.color)
				}
			}
		}
	}

	// 绘制下一个方块预览
	if g.nextPiece != nil {
		for y := 0; y < len(g.nextPiece.shape); y++ {
			for x := 0; x < len(g.nextPiece.shape[y]); x++ {
				if g.nextPiece.shape[y][x] != 0 {
					drawBlock(screen,
						boardWidth+2+x,
						4+y,
						g.nextPiece.color)
				}
			}
		}
	}

	// 绘制游戏信息
	ebitenutil.DebugPrint(screen, fmt.Sprintf(
		"\nScore: %d\nLevel: %d\nLines: %d",
		g.score, g.level, g.lines))

	// 绘制游戏状态
	if g.gameOver {
		ebitenutil.DebugPrint(screen,
			"\n\n\n\nGame Over!\nPress SPACE to restart")
	} else if g.paused {
		ebitenutil.DebugPrint(screen,
			"\n\n\n\nPAUSED\nPress P to continue")
	}
}

// drawBlock 绘制单个方块
func drawBlock(screen *ebiten.Image, x, y, colorIndex int) {
	ebitenutil.DrawRect(screen,
		float64(x*blockSize),
		float64(y*blockSize),
		float64(blockSize-1),
		float64(blockSize-1),
		color.RGBA{
			R: uint8((colors[colorIndex] >> 24) & 0xFF),
			G: uint8((colors[colorIndex] >> 16) & 0xFF),
			B: uint8((colors[colorIndex] >> 8) & 0xFF),
			A: uint8(colors[colorIndex] & 0xFF),
		})
}

// moveLeft 向左移动当前方块
func (g *Game) moveLeft() {
	if g.currentPiece == nil {
		return
	}
	g.currentPiece.x--
	if !g.isValidPosition() {
		g.currentPiece.x++
	}
}

// moveRight 向右移动当前方块
func (g *Game) moveRight() {
	if g.currentPiece == nil {
		return
	}
	g.currentPiece.x++
	if !g.isValidPosition() {
		g.currentPiece.x--
	}
}

// moveDown 向下移动当前方块
func (g *Game) moveDown() {
	if g.currentPiece == nil {
		return
	}
	g.currentPiece.y++
	if !g.isValidPosition() {
		g.currentPiece.y--
		g.lockPiece()
	}
}

// isValidPosition 检查当前方块位置是否有效
func (g *Game) isValidPosition() bool {
	for y := 0; y < len(g.currentPiece.shape); y++ {
		for x := 0; x < len(g.currentPiece.shape[y]); x++ {
			if g.currentPiece.shape[y][x] != 0 {
				newX := g.currentPiece.x + x
				newY := g.currentPiece.y + y

				if newX < 0 || newX >= boardWidth ||
					newY < 0 || newY >= boardHeight {
					return false
				}

				if g.board[newY][newX] != 0 {
					return false
				}
			}
		}
	}
	return true
}

// Layout 实现必要的 Ebiten 接口方法
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return screenWidth, screenHeight
}

// rotate 旋转当前方块
func (g *Game) rotate() {
	if g.currentPiece == nil {
		return
	}

	// 创建新的旋转后的形状
	oldShape := g.currentPiece.shape
	height := len(oldShape)
	width := len(oldShape[0])
	newShape := make([][]int, width)
	for i := range newShape {
		newShape[i] = make([]int, height)
	}

	// 执行90度旋转
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			newShape[x][height-1-y] = oldShape[y][x]
		}
	}

	// 保存原来的形状,以便在新位置无效时恢复
	originalShape := g.currentPiece.shape
	g.currentPiece.shape = newShape

	// 如果新位置无效,恢复原来的形状
	if !g.isValidPosition() {
		g.currentPiece.shape = originalShape
	}
}

// lockPiece 将当前方块锁定到游戏板上
func (g *Game) lockPiece() {
	if g.currentPiece == nil {
		return
	}

	// 将方块添加到游戏板
	for y := 0; y < len(g.currentPiece.shape); y++ {
		for x := 0; x < len(g.currentPiece.shape[y]); x++ {
			if g.currentPiece.shape[y][x] != 0 {
				boardY := g.currentPiece.y + y
				boardX := g.currentPiece.x + x
				g.board[boardY][boardX] = g.currentPiece.color
			}
		}
	}

	// 检查并清除完整的行
	g.clearLines()

	// 生成新的方块
	g.spawnNewPiece()

	// 检查游戏是否结束
	if !g.isValidPosition() {
		g.gameOver = true
	}
}

// clearLines 清除完整的行并计分
func (g *Game) clearLines() {
	linesCleared := 0
	for y := boardHeight - 1; y >= 0; y-- {
		// 检查当前行是否已满
		isFull := true
		for x := 0; x < boardWidth; x++ {
			if g.board[y][x] == 0 {
				isFull = false
				break
			}
		}

		// 如果行已满,删除该行并将上面的所有行下移
		if isFull {
			// 从当前行开始,将每一行都复制为上一行的内容
			for moveY := y; moveY > 0; moveY-- {
				copy(g.board[moveY], g.board[moveY-1])
			}
			// 清空最上面的行
			for x := 0; x < boardWidth; x++ {
				g.board[0][x] = 0
			}
			// 由于行已经下移,我们需要重新检查当前行
			linesCleared++
			y++
		}
	}

	// 更新分数和等级
	if linesCleared > 0 {
		g.lines += linesCleared
		// 分数计算:一次消除的行数越多,得分越高
		g.score += []int{100, 300, 500, 800}[linesCleared-1] * g.level
		// 每消除10行提升一个等级
		g.level = g.lines/10 + 1
	}
}

// generateNewPiece 生成一个新的随机方块
func (g *Game) generateNewPiece() *Piece {
	pieceIndex := rand.Intn(len(tetrominoes))
	return &Piece{
		shape: tetrominoes[pieceIndex],
		x:     boardWidth/2 - len(tetrominoes[pieceIndex][0])/2,
		y:     0,
		color: pieceIndex + 1,
	}
}

// spawnNewPiece 生成新的方块
func (g *Game) spawnNewPiece() {
	g.currentPiece = g.nextPiece
	g.nextPiece = g.generateNewPiece()
}

func main() {
	game := NewGame()

	ebiten.SetWindowSize(screenWidth, screenHeight)
	ebiten.SetWindowTitle("俄罗斯方块")

	if err := ebiten.RunGame(game); err != nil {
		log.Fatal(err)
	}
}


piece.go

package main

import (
	"math/rand"
	"time"
)

// 在init函数中初始化随机数种子
func init() {
	rand.Seed(time.Now().UnixNano())
}

// 定义所有可能的方块形状
var tetrominoes = [][][]int{
	{ // I
		{1, 1, 1, 1},
	},
	{ // O
		{1, 1},
		{1, 1},
	},
	{ // T
		{0, 1, 0},
		{1, 1, 1},
	},
	{ // L
		{1, 0, 0},
		{1, 1, 1},
	},
	{ // J
		{0, 0, 1},
		{1, 1, 1},
	},
	{ // S
		{0, 1, 1},
		{1, 1, 0},
	},
	{ // Z
		{1, 1, 0},
		{0, 1, 1},
	},
}

// 方块颜色定义
var colors = []int{
	1: 0xFF0000FF, // 红色
	2: 0x00FF00FF, // 绿色
	3: 0x0000FFFF, // 蓝色
	4: 0xFFFF00FF, // 黄色
	5: 0xFF00FFFF, // 紫色
	6: 0x00FFFFFF, // 青色
	7: 0xFFA500FF, // 橙色
}