Go Ebiten小游戏开发:井字棋
https://i-blog.csdnimg.cn/img_convert/f0caf87ea16a502ee872fed129d2cf07.png本日我将分享怎样利用 Go 语言和 Ebiten 游戏库开发一个简单的井字棋游戏。Ebiten 是一个轻量级的 2D 游戏库,非常适适用来开发小型游戏。通过这个项目,我们可以学习到怎样利用 Ebiten 处置惩罚输入、渲染图形以及管理游戏状态。
项目概述
井字棋是一个经典的两人对战游戏,玩家轮番在 3x3 的棋盘上放置自己的标记(通常是“圈”和“叉”),先连成一条线的玩家获胜。我们的目的是实现一个简单的井字棋游戏,支持以下功能:
[*]玩家轮番下棋
[*]检测游戏是否竣事(胜利或平局)
[*]游戏竣事后的重新开始功能
[*]简单的动画效果
代码结构
我们的代码重要分为以下几个部分:
[*]游戏状态管理:包括棋盘状态、当前玩家回合、游戏是否竣事等。
[*]输入处置惩罚:处置惩罚鼠标点击和键盘输入。
[*]渲染逻辑:绘制棋盘、棋子和游戏竣事动画。
[*]游戏逻辑:查抄胜利条件、平局条件等。
1. 游戏状态管理
我们利用一个 Game 结构体来管理游戏的状态:
type Game struct {
Turn bool // 当前玩家回合(true: 玩家1,false: 玩家2)
Board 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 == 1 {
DrawCircle(screen, i, j) // 画圈
} else if game.Board == 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 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} // 白色,用于画线条 GameOverTextstring // 游戏竣事时的提示文本 RestartButton bool // 是否显示重新开始按钮(未利用) GameOverTimer int // 游戏竣事动画计时器)type Game struct {
Turn bool // 当前玩家回合(true: 玩家1,false: 玩家2)
Board 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 == 0 { // 如果点击的格子为空 if game.Turn { game.Board = 1 // 玩家1下棋 } else { game.Board = 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 = 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 == 1 {
DrawCircle(screen, i, j) // 画圈
} else if game.Board == 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 int) bool { // 查抄行 for i := 0; i < 3; i++ { if board != 0 && board == board && board == board { return true } } // 查抄列 for i := 0; i < 3; i++ { if board != 0 && board == board && board == board { return true } } // 查抄对角线 if board != 0 && board == board && board == board { return true } if board != 0 && board == board && board == board { return true } return false}// IsBoardFull 查抄棋盘是否已满func IsBoardFull(board int) bool { for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if board == 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企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]