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.pswp.cn/news/897686.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/897686.shtml
英文地址,請注明出處:http://en.pswp.cn/news/897686.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL中IN關鍵字與EXIST關鍵字的比較

文章目錄 **功能等價性分析****執行計劃分析**&#xff1a; **1. EXISTS 的工作原理****步驟拆解**&#xff1a; **2. 為什么需要“利用索引快速定位”&#xff1f;****索引作用示例**&#xff1a; **3. 與 IN 子查詢的對比****IN 的工作方式**&#xff1a;**關鍵差異**&#x…

## DeepSeek寫水果記憶配對手機小游戲

DeepSeek寫水果記憶配對手機小游戲 提問 根據提的要求&#xff0c;讓DeepSeek整理的需求&#xff0c;進行提問&#xff0c;內容如下&#xff1a; 請生成一個包含以下功能的可運行移動端水果記憶配對小游戲H5文件&#xff1a; 要求 可以重新開始游戲 可以暫停游戲 卡片里的水果…

【含文檔+PPT+源碼】基于Django框架的鄉村綠色農產品交易平臺的設計與實現

項目介紹 本課程演示的是一款基于Django框架的鄉村綠色農產品交易平臺的設計與實現&#xff0c;主要針對計算機相關專業的正在做畢設的學生與需要項目實戰練習的 Python學習者。 1.包含&#xff1a;項目源碼、項目文檔、數據庫腳本、軟件工具等所有資料 2.帶你從零開始部署運…

idea超級AI插件,讓 AI 為 Java 工程師

引言? 用戶可在界面中直接通過輸入自然語言的形式描述接口的需求&#xff0c;系統通過輸入的需求自動分析關鍵的功能點有哪些&#xff0c;并對不確定方案的需求提供多種選擇&#xff0c;以及對需求上下文進行補充&#xff0c;用戶修改確定需求后&#xff0c;系統會根據需求設…

@RestControllerAdvice注解

RestControllerAdvice RestControllerAdvice 是 Spring Framework&#xff08;3.2&#xff09;和 Spring Boot 中用于全局處理控制器層異常和統一響應格式的注解。它結合了 ControllerAdvice 和 ResponseBody 的功能&#xff0c;能夠攔截控制器方法拋出的異常&#xff0c;并以 …

ActiveMQ監聽器在MQ重啟后不再監聽問題

應用的監聽器注解 JmsListener(destination "TopicName",containerFactory "FactoryName")工廠代碼 BeanJmsListenerContainerFactory<?> FactoryName(ConnectionFactory connectionFactory){SimpleJmsListenerContainerFactory factory new S…

大白話 Vue 中的keep - alive組件,它的作用是什么?在什么場景下使用?

大白話 Vue 中的keep - alive組件&#xff0c;它的作用是什么&#xff1f;在什么場景下使用&#xff1f; 什么是 keep-alive 組件 在 Vue 里&#xff0c;keep-alive 是一個內置組件&#xff0c;它就像是一個“保存盒”&#xff0c;能把組件實例保存起來&#xff0c;而不是每次…

考研復試c語言常見問答題匯總2

11. 關鍵字和一般標識符有什么不同&#xff1f; C語言中關鍵字與一般標識符區別&#xff1a; 定義&#xff1a;關鍵字是C語言預定義的特殊單詞&#xff08;如int、for&#xff09;&#xff0c;有固定含義&#xff1b;標識符是自定義的名稱&#xff08;如變量名、函數名&#xf…

Scala編程_實現Rational的基本操作

在Scala中實現一個簡單的有理數&#xff08;Rational&#xff09;類&#xff0c;并對其進行加法、比較等基本操作. 有理數的定義 有理數是可以表示為兩個整數的比值的數&#xff0c;通常形式為 n / d&#xff0c;其中 n 是分子&#xff0c;d 是分母。為了確保我們的有理數始終…

若依框架-給sys_user表添加新字段并獲取當前登錄用戶的該字段值

目錄 添加字段 修改SysUser類 修改SysUserMapper.xml 修改user.js 前端獲取字段值 添加字段 若依框架的sys_user表是沒有age字段的&#xff0c;但由于業務需求&#xff0c;我需要新添加一個age字段&#xff1a; 修改SysUser類 添加age字段后&#xff0c;要在SysUser類 …

霍夫變換法是基于傳統視覺特征的道路車道線檢測算法中的一種經典方法

霍夫變換法是基于傳統視覺特征的道路車道線檢測算法中的一種經典方法&#xff0c;以下是對它的詳細介紹&#xff1a; 基本原理 霍夫變換的基本思想是將圖像空間中的點映射到參數空間中&#xff0c;通過在參數空間中尋找峰值來確定圖像中特定形狀的參數。在車道線檢測中&#…

【論文筆記】Best Practices and Lessons Learned on Synthetic Data for Language Models

論文信息 論文標題&#xff1a;Best Practices and Lessons Learned on Synthetic Data for Language Models 作者信息&#xff1a; Ruibo Liu, Jerry Wei, Fangyu Liu, Chenglei Si, Yanzhe Zhang, Jinmeng Rao, Steven Zheng, Daiyi Peng, Diyi Yang, Denny Zhou1 and Andre…

Android調試工具之ADB

Android Debug Bridge ADB介紹**一、ADB下載****二、ADB安裝****三、ADB基礎使用命令** ADB介紹 ADB&#xff08;Android Debug Bridge&#xff09;是Android開發與調試的必備工具&#xff0c;掌握它能極大提升開發效率。 一、ADB下載 Windows版本&#xff1a;https://dl.goo…

第三篇《RMAN 備份與恢復指南:保障數據庫安全》(RMAN)

《Oracle 數據遷移與備份系列》 第三篇&#xff1a;《RMAN 備份與恢復指南&#xff1a;保障數據庫安全》&#xff08;RMAN&#xff09; 1.概述 RMAN&#xff08;Recovery Manager&#xff09; 是 Oracle 數據庫內置的專用備份與恢復工具&#xff0c;提供高效、安全的物理級數…

【測試框架篇】單元測試框架pytest(4):assert斷言詳解

一、前言 用例三要素之一就是對預期結果的斷言。 何為斷言&#xff1f;簡單來說就是實際結果和期望結果去對比&#xff0c;符合預期就測試pass&#xff0c;不符合預期那就測試 failed。斷言內容就是你要的預期結果。斷言包含對接口響應內容做斷言、也包含對落DB的數據做斷言。…

什么是大模型微調?

在大模型&#xff08;如GPT、BERT、LLaMA等&#xff09;廣泛應用的今天&#xff0c;“微調”&#xff08;Fine-Tuning&#xff09;已成為釋放模型潛力的關鍵技術。它通過針對特定任務調整預訓練模型&#xff0c;使其從“通才”變為“專才”。本文將從概念、原理到實踐&#xff…

C# Channel

核心概念創建Channel無界通道有界通道FullMode選項 生產者-消費者模式生產者寫入數據消費者讀取數據 完整示例高級配置優化選項&#xff1a;取消操作&#xff1a;通過 CancellationToken 取消讀寫。 錯誤處理適用場景Channel的類型創建Channel寫入和讀取消息使用場景示例代碼注…

基于Spring Boot的牙科診所管理系統的設計與實現(LW+源碼+講解)

專注于大學生項目實戰開發,講解,畢業答疑輔導&#xff0c;歡迎高校老師/同行前輩交流合作?。 技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬蟲、數據可視化、安卓app、大數據、物聯網、機器學習等設計與開發。 主要內容&#xff1a;…

upload-labs-靶場(1-19關)通關攻略

文件上傳漏洞是指由于程序員再開發時&#xff0c;未對用戶上傳的文件進行嚴格的驗證和過濾&#xff0c;而導致用戶可以上傳可執行的動態腳本文件 Pass-01&#xff08;前端驗證繞過&#xff09; 上傳111.php文件&#xff0c;發現彈窗顯示不允許&#xff0c;并給出白名單文件類…

使用 pytesseract 進行 OCR 識別:以固定區域經緯度提取為例

引言 在智能交通、地圖定位等應用場景中&#xff0c;經常會遇到需要從圖像中提取經緯度信息的需求。本篇文章將介紹如何利用 Python 的 pytesseract 庫結合 PIL 對圖像進行預處理&#xff0c;通過固定區域裁剪&#xff0c;來有效地識別出圖像上顯示的經緯度信息。 1. OCR 與 …