当前位置: 首页 > news >正文

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

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

项目结构

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

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

游戏状态管理

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

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

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

type Piece struct {shape [][]intx, y  intcolor 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 = 0g.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 + xnewY := g.currentPiece.y + yif 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 := 0for y := boardHeight - 1; y >= 0; y-- {isFull := truefor x := 0; x < boardWidth; x++ {if g.board[y][x] == 0 {isFull = falsebreak}}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 += linesClearedg.score += []int{100, 300, 500, 800}[linesCleared-1] * g.levelg.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 mainimport ("fmt""image/color""log""math/rand""github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/ebitenutil""github.com/hajimehoshi/ebiten/v2/inpututil""github.com/hajimehoshi/ebiten/v2/vector"
)const (screenWidth  = 320screenHeight = 640blockSize    = 32boardWidth   = 10boardHeight  = 20
)// Game 表示游戏状态
type Game struct {board        [][]intcurrentPiece *PiecenextPiece    *Piece // 下一个方块gameOver     booldropTimer    intscore        int  // 得分level        int  // 当前等级lines        int  // 已消除的行数paused       bool // 暂停状态// 添加动画相关字段clearingLines  []int // 正在消除的行clearAnimation int   // 动画计时器isClearing     bool  // 是否正在播放消除动画
}// Piece 表示俄罗斯方块的一个方块
type Piece struct {shape [][]intx, y  intcolor 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.pausedreturn nil}if g.gameOver || g.paused {return nil}// 更新消除动画if g.isClearing {g.updateClearAnimation()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 = 0g.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 {// 检查是否是正在消除的行isClearing := falsefor _, clearY := range g.clearingLines {if y == clearY {isClearing = truebreak}}if isClearing {// 闪烁效果if (g.clearAnimation/3)%2 == 0 {// 绘制发光效果drawGlowingBlock(screen, x, y, g.board[y][x])}} else {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) {vector.DrawFilledRect(screen,float32(x*blockSize),float32(y*blockSize),float32(blockSize-1),float32(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),},false)
}// drawGlowingBlock 绘制发光的方块
func drawGlowingBlock(screen *ebiten.Image, x, y, colorIndex int) {vector.DrawFilledRect(screen,float32(x*blockSize-2),float32(y*blockSize-2),float32(blockSize+3),float32(blockSize+3),color.RGBA{255, 255, 255, 128},false)drawBlock(screen, x, y, colorIndex)
}// 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 + xnewY := g.currentPiece.y + yif 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.shapeheight := 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.shapeg.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 + yboardX := g.currentPiece.x + xg.board[boardY][boardX] = g.currentPiece.color}}}// 检查并清除完整的行g.clearLines()// 生成新的方块g.spawnNewPiece()// 检查游戏是否结束if !g.isValidPosition() {g.gameOver = true}
}// clearLines 检查完整的行
func (g *Game) clearLines() {if g.isClearing {return}// 检查完整的行g.clearingLines = nilfor y := boardHeight - 1; y >= 0; y-- {isFull := truefor x := 0; x < boardWidth; x++ {if g.board[y][x] == 0 {isFull = falsebreak}}if isFull {g.clearingLines = append(g.clearingLines, y)}}// 如果有要消除的行,开始动画if len(g.clearingLines) > 0 {g.isClearing = trueg.clearAnimation = 0}
}// updateClearAnimation 更新消除动画
func (g *Game) updateClearAnimation() {if !g.isClearing {return}g.clearAnimation++// 动画结束后执行实际的消除if g.clearAnimation >= 30 { // 0.5秒动画(30帧)// 执行实际的消除for _, y := range g.clearingLines {// 从当前行开始,将每一行都复制为上一行的内容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 := len(g.clearingLines)g.lines += linesClearedg.score += []int{100, 300, 500, 800}[linesCleared-1] * g.levelg.level = g.lines/10 + 1// 重置动画状态g.isClearing = falseg.clearingLines = nil}
}// 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.nextPieceg.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 mainimport ("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, // 橙色
}

http://www.mrgr.cn/news/94172.html

相关文章:

  • halcon机器人视觉(四)calibrate_hand_eye_stationary_3d_sensor
  • JAVA 基础语法备忘录 -
  • 01 | Go 项目开发极速入门课介绍
  • 如何搭建一个适配微信小程序,h5,app的工程
  • VSCode集成C语言开发环境
  • 要登录的设备ip未知时的处理方法
  • 17 | 实现简洁架构的 Biz 层
  • 【大模型】WPS 接入 DeepSeek-R1详解,打造全能AI办公助手
  • 编程助手学Python--Deepseek对OpenAI的Python库调用GPT-4模型生成对话回复理解
  • Future<V>接口 和 CompletableFuture<T>类 介绍
  • BLDC直流无刷电机转速电流双闭环调速MATLAB仿真
  • 21 | 全面测试项目功能
  • 12 | 给应用添加优雅关停功能
  • 完整项目案例:基于Django的毕业设计选题管理系统(包含源码结构、核心代码及设计文档框架)
  • 【Linux】在VMWare中安装Ubuntu操作系统(2025最新_Ubuntu 24.04.2)#VMware安装Ubuntu实战分享#
  • Node-RED基础1
  • 【Godot4.3】RenderingServer总结
  • Quantum Computing:量子计算如何改变世界
  • 深入探索 Java Stream
  • 使用 OptiSLang 和 MotorCAD 构建一个强大的电机优化元模型