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

Go Ebiten小游戏开发:井字棋

image-20250113024452832

今天我将分享如何使用 Go 语言和 Ebiten 游戏库开发一个简单的井字棋游戏。Ebiten 是一个轻量级的 2D 游戏库,非常适合用来开发小型游戏。通过这个项目,我们可以学习到如何使用 Ebiten 处理输入、渲染图形以及管理游戏状态。

项目概述

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

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

代码结构

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

  1. 游戏状态管理:包括棋盘状态、当前玩家回合、游戏是否结束等。
  2. 输入处理:处理鼠标点击和键盘输入。
  3. 渲染逻辑:绘制棋盘、棋子和游戏结束动画。
  4. 游戏逻辑:检查胜利条件、平局条件等。

1. 游戏状态管理

我们使用一个 Game 结构体来管理游戏的状态:

type Game struct {
	Turn       bool      // 当前玩家回合(true: 玩家1,false: 玩家2)
	Board      [3][3]int // 3x3 的棋盘,0: 空,1: 玩家1,2: 玩家2
	IsGameOver bool      // 游戏是否结束
}

2. 输入处理

我们通过 HandleInput 函数来处理玩家的输入。玩家可以通过鼠标点击来下棋,按下 R 键重新开始游戏,按下 ESC 键退出游戏。

func (game *Game) HandleInput() {
	if ebiten.IsKeyPressed(ebiten.KeyEscape) {
		game.Exit() // 按下 ESC 键退出游戏
	}
	if !game.IsGameOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		game.HandleMouseClick() // 如果游戏未结束且按下鼠标左键,处理点击
	}
	if game.IsGameOver && ebiten.IsKeyPressed(ebiten.KeyR) {
		game.Restart() // 如果游戏结束且按下 R 键,重新开始游戏
	}
}

3. 渲染逻辑

我们使用 DrawBoard 函数来绘制棋盘和棋子。棋盘由两条垂直线和两条水平线组成,棋子则根据棋盘状态绘制“圈”或“叉”。

func DrawBoard(screen *ebiten.Image, game *Game) {
	// 绘制棋盘线条
	for i := 1; i <= 2; i++ {
		vector.DrawFilledRect(screen, float32(i)*BlockSize, 0, LineWidth, 3*BlockSize+LineWidth, WHITE, true)
		vector.DrawFilledRect(screen, 0, float32(i)*BlockSize, 3*BlockSize+LineWidth, LineWidth, WHITE, true)
	}
	// 绘制棋子的圈和叉
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if game.Board[i][j] == 1 {
				DrawCircle(screen, i, j) // 画圈
			} else if game.Board[i][j] == 2 {
				DrawCross(screen, i, j) // 画叉
			}
		}
	}
}

4. 游戏逻辑

我们通过 CheckGameOver 函数来检查游戏是否结束。如果棋盘已满且没有玩家获胜,则为平局;否则,检查是否有玩家连成一条线。

func (game *Game) CheckGameOver() {
	if IsBoardFull(game.Board) { // 检查是否平局
		game.IsGameOver = true
		GameOverText = "It's a Draw!"
	} else if CheckWin(game.Board) { // 检查是否有玩家获胜
		game.IsGameOver = true
		if game.Turn { // 当前回合是 O,说明 X 赢了
			GameOverText = "Player X Wins!"
		} else { // 当前回合是 X,说明 O 赢了
			GameOverText = "Player O Wins!"
		}
	}
}

完整代码

package main

import (
	"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 {
	Turn       bool      // 当前玩家回合(true: 玩家1,false: 玩家2)
	Board      [3][3]int // 3x3 的棋盘,0: 空,1: 玩家1,2: 玩家2
	IsGameOver bool      // 游戏是否结束
}

// 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() {
	if ebiten.IsKeyPressed(ebiten.KeyEscape) {
		game.Exit() // 按下 ESC 键退出游戏
	}
	if !game.IsGameOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		game.HandleMouseClick() // 如果游戏未结束且按下鼠标左键,处理点击
	}
	if game.IsGameOver && ebiten.IsKeyPressed(ebiten.KeyR) {
		game.Restart() // 如果游戏结束且按下 R 键,重新开始游戏
	}
}

// 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() {
	if IsBoardFull(game.Board) { // 检查是否平局
		game.IsGameOver = true
		GameOverText = "It's a Draw!"
	} else if CheckWin(game.Board) { // 检查是否有玩家获胜
		game.IsGameOver = true
		if game.Turn { // 当前回合是 O,说明 X 赢了
			GameOverText = "Player X Wins!"
		} else { // 当前回合是 X,说明 O 赢了
			GameOverText = "Player O Wins!"
		}
	}
}

// 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) {
	// 绘制棋盘线条
	for i := 1; i <= 2; i++ {
		vector.DrawFilledRect(screen, float32(i)*BlockSize, 0, LineWidth, 3*BlockSize+LineWidth, WHITE, true)
		vector.DrawFilledRect(screen, 0, float32(i)*BlockSize, 3*BlockSize+LineWidth, LineWidth, WHITE, true)
	}
	// 绘制棋子的圈和叉
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if game.Board[i][j] == 1 {
				DrawCircle(screen, i, j) // 画圈
			} else if game.Board[i][j] == 2 {
				DrawCross(screen, i, j) // 画叉
			}
		}
	}
}

// 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)
	}
}


http://www.kler.cn/a/502679.html

相关文章:

  • SQL Server 查看数据库表使用空间
  • .NET framework、Core和Standard都是什么?
  • uniapp 之 uni-forms校验提示【提交的字段[‘xxx‘]在数据库中并不存在】解决方案
  • 《探索鸿蒙Next上开发人工智能游戏应用的技术难点》
  • QT Quick QML 实例之椭圆投影,旋转
  • vue2修改表单只提交被修改的数据的字段传给后端接口
  • Postgres14.4(Docker安装)
  • 【数据分析】一、初探 Numpy
  • 服务器引导异常,Grub报错: error: ../../grub-core/fs/fshelp.c:258:file xxxx.img not found.
  • 行业案例:高德服务单元化方案和架构实践
  • 【开源免费】基于SpringBoot+Vue.JS企业级工位管理系统(JAVA毕业设计)
  • C++ 的 pair 和 tuple
  • 【江协STM32】11-1 SPI通信协议
  • UE5 打包项目
  • 【源码解析】Java NIO 包中的 Buffer
  • 新型物联网智能断路器功能参数介绍
  • Spring Boot3 配合ProxySQL实现对 MySQL 主从同步的读写分离和负载均衡
  • 【2024年华为OD机试】 (C卷,100分)- 工号不够用了怎么办?(Java JS PythonC/C++)
  • 【机器学习】数学知识:指数函数(exp)
  • 大语言模型的分层架构:高效建模的全新探索
  • Vue.js组件开发,AI时代的前端新玩法
  • LabVIEW自动扫描与图像清晰度检测
  • kalilinux - msf和永恒之蓝漏洞
  • C#学习笔记 --- 简单应用
  • B树——C++
  • 6. NLP自然语言处理(Natural Language Processing)