Go Ebiten小游戏开发:井字棋

一给  金牌会员 | 2025-1-14 16:52:26 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 994|帖子 994|积分 2982


本日我将分享怎样利用 Go 语言和 Ebiten 游戏库开发一个简单的井字棋游戏。Ebiten 是一个轻量级的 2D 游戏库,非常适适用来开发小型游戏。通过这个项目,我们可以学习到怎样利用 Ebiten 处置惩罚输入、渲染图形以及管理游戏状态。
项目概述

井字棋是一个经典的两人对战游戏,玩家轮番在 3x3 的棋盘上放置自己的标记(通常是“圈”和“叉”),先连成一条线的玩家获胜。我们的目的是实现一个简单的井字棋游戏,支持以下功能:


  • 玩家轮番下棋
  • 检测游戏是否竣事(胜利或平局)
  • 游戏竣事后的重新开始功能
  • 简单的动画效果
代码结构

我们的代码重要分为以下几个部分:

  • 游戏状态管理:包括棋盘状态、当前玩家回合、游戏是否竣事等。
  • 输入处置惩罚:处置惩罚鼠标点击和键盘输入。
  • 渲染逻辑:绘制棋盘、棋子和游戏竣事动画。
  • 游戏逻辑:查抄胜利条件、平局条件等。
1. 游戏状态管理

我们利用一个 Game 结构体来管理游戏的状态:
  1. type Game struct {
  2.         Turn       bool      // 当前玩家回合(true: 玩家1,false: 玩家2)
  3.         Board      [3][3]int // 3x3 的棋盘,0: 空,1: 玩家1,2: 玩家2
  4.         IsGameOver bool      // 游戏是否结束
  5. }
复制代码
2. 输入处置惩罚

我们通过 HandleInput 函数来处置惩罚玩家的输入。玩家可以通过鼠标点击来下棋,按下 R 键重新开始游戏,按下 ESC 键退出游戏。
  1. func (game *Game) HandleInput() {
  2.         if ebiten.IsKeyPressed(ebiten.KeyEscape) {
  3.                 game.Exit() // 按下 ESC 键退出游戏
  4.         }
  5.         if !game.IsGameOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
  6.                 game.HandleMouseClick() // 如果游戏未结束且按下鼠标左键,处理点击
  7.         }
  8.         if game.IsGameOver && ebiten.IsKeyPressed(ebiten.KeyR) {
  9.                 game.Restart() // 如果游戏结束且按下 R 键,重新开始游戏
  10.         }
  11. }
复制代码
3. 渲染逻辑

我们利用 DrawBoard 函数来绘制棋盘和棋子。棋盘由两条垂直线和两条程度线组成,棋子则根据棋盘状态绘制“圈”或“叉”。
  1. func DrawBoard(screen *ebiten.Image, game *Game) {
  2.         // 绘制棋盘线条
  3.         for i := 1; i <= 2; i++ {
  4.                 vector.DrawFilledRect(screen, float32(i)*BlockSize, 0, LineWidth, 3*BlockSize+LineWidth, WHITE, true)
  5.                 vector.DrawFilledRect(screen, 0, float32(i)*BlockSize, 3*BlockSize+LineWidth, LineWidth, WHITE, true)
  6.         }
  7.         // 绘制棋子的圈和叉
  8.         for i := 0; i < 3; i++ {
  9.                 for j := 0; j < 3; j++ {
  10.                         if game.Board[i][j] == 1 {
  11.                                 DrawCircle(screen, i, j) // 画圈
  12.                         } else if game.Board[i][j] == 2 {
  13.                                 DrawCross(screen, i, j) // 画叉
  14.                         }
  15.                 }
  16.         }
  17. }
复制代码
4. 游戏逻辑

我们通过 CheckGameOver 函数来查抄游戏是否竣事。如果棋盘已满且没有玩家获胜,则为平局;否则,查抄是否有玩家连成一条线。
  1. func (game *Game) CheckGameOver() {
  2.         if IsBoardFull(game.Board) { // 检查是否平局
  3.                 game.IsGameOver = true
  4.                 GameOverText = "It's a Draw!"
  5.         } else if CheckWin(game.Board) { // 检查是否有玩家获胜
  6.                 game.IsGameOver = true
  7.                 if game.Turn { // 当前回合是 O,说明 X 赢了
  8.                         GameOverText = "Player X Wins!"
  9.                 } else { // 当前回合是 X,说明 O 赢了
  10.                         GameOverText = "Player O Wins!"
  11.                 }
  12.         }
  13. }
复制代码
完整代码

  1. package mainimport (        "image"        "image/color"        "log"        "math"        "os"        "github.com/hajimehoshi/ebiten/v2"        "github.com/hajimehoshi/ebiten/v2/vector"        "golang.org/x/image/font"        "golang.org/x/image/font/basicfont"        "golang.org/x/image/math/fixed")const (        BlockSize       float32 = 200                               // 每个格子的巨细        WindowWidth     int     = 3*int(BlockSize) + int(LineWidth) // 窗口宽度        WindowHeight    int     = 3*int(BlockSize) + int(LineWidth) // 窗口高度        LineWidth       float32 = 20                                // 线条宽度        LineOffsetRatio float32 = LineWidth / BlockSize / 2         // 线条偏移比例)var (        BLUE          color.Color = color.NRGBA{0, 0, 255, 255}     // 蓝色,用于画圈        RED           color.Color = color.NRGBA{255, 0, 0, 255}     // 红色,用于画叉        WHITE         color.Color = color.NRGBA{255, 255, 255, 255} // 白色,用于画线条        GameOverText  string                                        // 游戏竣事时的提示文本        RestartButton bool                                          // 是否显示重新开始按钮(未利用)        GameOverTimer int                                           // 游戏竣事动画计时器)type Game struct {
  2.         Turn       bool      // 当前玩家回合(true: 玩家1,false: 玩家2)
  3.         Board      [3][3]int // 3x3 的棋盘,0: 空,1: 玩家1,2: 玩家2
  4.         IsGameOver bool      // 游戏是否结束
  5. }
  6. // Update 是 Ebiten 的主循环函数,每一帧调用一次func (game *Game) Update() error {        game.HandleInput() // 处置惩罚输入        if game.IsGameOver {                GameOverTimer++ // 游戏竣事时,计时器增长        }        return nil}// Draw 是 Ebiten 的渲染函数,每一帧调用一次func (game *Game) Draw(screen *ebiten.Image) {        DrawBoard(screen, game) // 绘制棋盘        if game.IsGameOver {                DrawGameOver(screen) // 如果游戏竣事,绘制竣事动画        }}// Layout 设置窗口的结构func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {        return outsideWidth, outsideHeight}// HandleInput 处置惩罚用户输入func (game *Game) HandleInput() {
  7.         if ebiten.IsKeyPressed(ebiten.KeyEscape) {
  8.                 game.Exit() // 按下 ESC 键退出游戏
  9.         }
  10.         if !game.IsGameOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
  11.                 game.HandleMouseClick() // 如果游戏未结束且按下鼠标左键,处理点击
  12.         }
  13.         if game.IsGameOver && ebiten.IsKeyPressed(ebiten.KeyR) {
  14.                 game.Restart() // 如果游戏结束且按下 R 键,重新开始游戏
  15.         }
  16. }
  17. // HandleMouseClick 处置惩罚鼠标点击变乱func (game *Game) HandleMouseClick() {        mouseX, mouseY := ebiten.CursorPosition()                        // 获取鼠标位置        x, y := mouseX/int(BlockSize), mouseY/int(BlockSize)             // 计算点击的格子坐标        if x >= 0 && x < 3 && y >= 0 && y < 3 && game.Board[x][y] == 0 { // 如果点击的格子为空                if game.Turn {                        game.Board[x][y] = 1 // 玩家1下棋                } else {                        game.Board[x][y] = 2 // 玩家2下棋                }                game.Turn = !game.Turn // 切换玩家回合                game.CheckGameOver()   // 查抄游戏是否竣事        }}// CheckGameOver 查抄游戏是否竣事func (game *Game) CheckGameOver() {
  18.         if IsBoardFull(game.Board) { // 检查是否平局
  19.                 game.IsGameOver = true
  20.                 GameOverText = "It's a Draw!"
  21.         } else if CheckWin(game.Board) { // 检查是否有玩家获胜
  22.                 game.IsGameOver = true
  23.                 if game.Turn { // 当前回合是 O,说明 X 赢了
  24.                         GameOverText = "Player X Wins!"
  25.                 } else { // 当前回合是 X,说明 O 赢了
  26.                         GameOverText = "Player O Wins!"
  27.                 }
  28.         }
  29. }
  30. // Restart 重新开始游戏func (game *Game) Restart() {        game.Board = [3][3]int{} // 重置棋盘        game.Turn = false        // 重置回合        game.IsGameOver = false  // 重置游戏状态        GameOverText = ""        // 清空竣事文本        GameOverTimer = 0        // 重置计时器}// Exit 退出游戏func (game *Game) Exit() {        os.Exit(0)}// DrawBoard 绘制棋盘func DrawBoard(screen *ebiten.Image, game *Game) {
  31.         // 绘制棋盘线条
  32.         for i := 1; i <= 2; i++ {
  33.                 vector.DrawFilledRect(screen, float32(i)*BlockSize, 0, LineWidth, 3*BlockSize+LineWidth, WHITE, true)
  34.                 vector.DrawFilledRect(screen, 0, float32(i)*BlockSize, 3*BlockSize+LineWidth, LineWidth, WHITE, true)
  35.         }
  36.         // 绘制棋子的圈和叉
  37.         for i := 0; i < 3; i++ {
  38.                 for j := 0; j < 3; j++ {
  39.                         if game.Board[i][j] == 1 {
  40.                                 DrawCircle(screen, i, j) // 画圈
  41.                         } else if game.Board[i][j] == 2 {
  42.                                 DrawCross(screen, i, j) // 画叉
  43.                         }
  44.                 }
  45.         }
  46. }
  47. // DrawCircle 绘制圈func DrawCircle(screen *ebiten.Image, x, y int) {        x0, y0 := ((1+LineOffsetRatio)*float32(x)+0.5)*BlockSize, ((1+LineOffsetRatio)*float32(y)+0.5)*BlockSize        vector.StrokeCircle(screen, x0, y0, BlockSize/3, LineWidth, BLUE, true)}// DrawCross 绘制叉func DrawCross(screen *ebiten.Image, x, y int) {        L := BlockSize / 4        x1, y1 := ((1+LineOffsetRatio)*float32(x)+0.5)*BlockSize-L, ((1+LineOffsetRatio)*float32(y)+0.5)*BlockSize-L        x2, y2 := ((1+LineOffsetRatio)*float32(x)+0.5)*BlockSize+L, ((1+LineOffsetRatio)*float32(y)+0.5)*BlockSize+L        vector.StrokeLine(screen, x1, y1, x2, y2, LineWidth, RED, true)        x3, y3 := ((1+LineOffsetRatio)*float32(x)+0.5)*BlockSize+L, ((1+LineOffsetRatio)*float32(y)+0.5)*BlockSize-L        x4, y4 := ((1+LineOffsetRatio)*float32(x)+0.5)*BlockSize-L, ((1+LineOffsetRatio)*float32(y)+0.5)*BlockSize+L        vector.StrokeLine(screen, x3, y3, x4, y4, LineWidth, RED, true)}// DrawGameOver 绘制游戏竣事动画func DrawGameOver(screen *ebiten.Image) {        // 背景渐变动画        alpha := uint8(math.Min(float64(GameOverTimer)*2, 255))        bgColor := color.NRGBA{0, 0, 0, alpha}        vector.DrawFilledRect(screen, 0, 0, float32(WindowWidth), float32(WindowHeight), bgColor, true)        // 绘制游戏竣事文本        if GameOverText != "" {                textColor := color.NRGBA{255, 255, 255, 255}                text := GameOverText + " Press R to Restart"                DrawText(screen, text, WindowWidth/4, WindowHeight/2, textColor)        }}// DrawText 绘制文本func DrawText(screen *ebiten.Image, text string, x, y int, clr color.Color) {        f := basicfont.Face7x13        textWidth := font.MeasureString(f, text).Ceil()        textHeight := f.Metrics().Height.Ceil() + 100        textX := x - textWidth/2        textY := y - textHeight/2        textImage := ebiten.NewImage(textWidth, textHeight)        textImage.Fill(color.Transparent)        d := &font.Drawer{                Dst:  textImage,                Src:  image.NewUniform(clr),                Face: f,                Dot:  fixed.Point26_6{X: fixed.I(20), Y: fixed.I(20)},        }        d.DrawString(text)        op := &ebiten.DrawImageOptions{}        op.GeoM.Scale(2, 2)                               // 缩放文本        op.GeoM.Translate(float64(textX), float64(textY)) // 定位文本        op.ColorScale.ScaleWithColor(clr)                 // 设置文本颜色        screen.DrawImage(textImage, op)}// CheckWin 查抄是否有玩家获胜func CheckWin(board [3][3]int) bool {        // 查抄行        for i := 0; i < 3; i++ {                if board[i][0] != 0 && board[i][0] == board[i][1] && board[i][0] == board[i][2] {                        return true                }        }        // 查抄列        for i := 0; i < 3; i++ {                if board[0][i] != 0 && board[0][i] == board[1][i] && board[0][i] == board[2][i] {                        return true                }        }        // 查抄对角线        if board[0][0] != 0 && board[0][0] == board[1][1] && board[0][0] == board[2][2] {                return true        }        if board[2][0] != 0 && board[2][0] == board[1][1] && board[2][0] == board[0][2] {                return true        }        return false}// IsBoardFull 查抄棋盘是否已满func IsBoardFull(board [3][3]int) bool {        for i := 0; i < 3; i++ {                for j := 0; j < 3; j++ {                        if board[i][j] == 0 {                                return false                        }                }        }        return true}// main 是步调入口func main() {        ebiten.SetWindowTitle("Tic-Tac-Toe")            // 设置窗口标题        ebiten.SetWindowSize(WindowWidth, WindowHeight) // 设置窗口巨细        game := &Game{}        if err := ebiten.RunGame(game); err != nil {                log.Fatal(err)        }}
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

一给

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表