在這篇文章中,我們將一起開發一個簡單的俄羅斯方塊游戲,使用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, // 橙色
}