【本文正在参加金石计划附加挑战赛——第二期命题】
认识一下ebitengine
Ebitengine (Eh-Bee-Ten-Gin)(原名 Ebiten)是一款基于Go 编程语言的开源游戏引擎。Ebitengine 简单的 API 让您能够快速轻松地开发可跨多个平台部署的 2D 游戏。
在 Ebitengine 中,一切都是图像:屏幕、图像文件中的数据,甚至屏幕外的项目都表示为图像对象。大多数渲染操作都是将一幅图像绘制在另一幅图像之上。
Ebitengine 游戏可在桌面(Windows、macOS、Linux 和 FreeBSD)、Web 浏览器(通过 WebAssembly)甚至移动设备(Android 和 iOS)上运行!此外,Ebitengine 在 Windows 上采用纯 Go 实现,因此 Windows 开发人员无需安装 C 编译器。还支持 Nintendo Switch™!
尽管 Ebitengine 的绘图 API 非常简单,但 Ebitengine 游戏借助 GPU 的强大功能运行速度非常快。内部将多幅图像集成到纹理图集中,并在可能的情况下自动批量执行绘图操作。
Ebitengine 已用于开发生产级游戏。其中一个例子是Bear's Restaurant,这是一款下载量超过 1,500,000 次的移动应用程序。有关使用 Ebitengine 制作的更多生产游戏,请参阅展示页面。有关 Ebitengine 的其他商业游戏的信息,请访问展示页面。
设计一款简单的五子棋游戏
在对 ebitengine 有了初步了解之后,可以尝试着做一些简单的小玩具。下面带着大家做一个非常简单的五子棋小游戏。
创建一个工程并安装依赖
首先用 Goland 创建一个 Go 应用程序工程,然后安装一下 ebitengine 库:
go get github.com/hajimehoshi/ebiten/v2
绘制出一个窗口
首先我们需要绘制一个窗口,定义 Game 结构体如下:
const (
boardSize = 15 // 棋盘大小
cellSize = 40 // 每个格子的大小
screenWidth = cellSize * boardSize
screenHeight = cellSize * boardSize
)
type Player int
const (
Player1 Player = 1 // 黑子
Player2 Player = 2 // 白子
)
type GameWindow struct {
board [boardSize][boardSize]Player // 棋盘,存储每个位置的棋子
currentPlayer Player // 当前玩家
gameOver bool // 游戏是否结束
winnerMessage string // 胜利信息
resetPending bool // 是否等待鼠标释放
}
上面的一些常用用于定义棋盘大小还有该谁落子。
声明构造函数
func NewGame() *GameWindow {
return &GameWindow{
board: [boardSize][boardSize]Player{},
currentPlayer: Player1,
gameOver: false,
winnerMessage: "",
resetPending: false,
}
}
ebitengine 规定游戏窗口需要实现 ebiten.Game 接口:
type Go interface {
// 刷新窗口
Update() error
// 绘制窗口的每一帧
Draw(screen *Image)
// 窗口布局,设置窗口的大小
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
实现一下这个接口:
// 更新游戏逻辑
func (g *GameWindow) Update() error {
return nil
}
// 绘制棋盘和棋子
func (g *GameWindow) Draw(screen *ebiten.Image) {
}
// 设置游戏布局
func (g *GameWindow) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
编写主函数运行窗口:
func main() {
game := NewGame()
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Gobang GameWindow")
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}
可以看到已经成功绘制出了一个窗口,只不过是黑乎乎的啥也没有:
绘制棋盘格子
为了能看清棋盘,这里我们设置棋盘的底色是黄色,然后格子都是黑色的线条:
func (g *GameWindow) Draw(screen *ebiten.Image) {
// 设置背景为黄色
screen.Fill(color.RGBA{255, 255, 0, 255}) // 黄色背景
// 绘制棋盘格子
for i := 0; i < boardSize; i++ {
ebitenutil.DrawLine(screen, float64(i*cellSize), 0, float64(i*cellSize), screenHeight, color.Black)
ebitenutil.DrawLine(screen, 0, float64(i*cellSize), screenWidth, float64(i*cellSize), color.Black)
}
// 绘制棋子在交叉点上
for i := 0; i < boardSize; i++ {
for j := 0; j < boardSize; j++ {
if g.board[i][j] == Player1 {
ebitenutil.DrawCircle(screen, float64(i*cellSize), float64(j*cellSize), cellSize/3, color.Black)
} else if g.board[i][j] == Player2 {
ebitenutil.DrawCircle(screen, float64(i*cellSize), float64(j*cellSize), cellSize/3, color.White)
}
}
}
// 如果游戏结束,显示胜利信息在窗口顶部
if g.gameOver {
text.Draw(screen, g.winnerMessage, basicfont.Face7x13, screenWidth/2-50, 20, color.Black)
// 绘制重新开始按钮
ebitenutil.DrawRect(screen, 10, screenHeight-50, 100, 40, color.White)
text.Draw(screen, "Restart", basicfont.Face7x13, 20, screenHeight-30, color.Black)
}
}
可以看到棋盘的格子已经成功地被绘制出来了:
处理点击事件实现落子
下一步我们需要实现落子的逻辑,棋子要落在格子的交叉点上,重写 Update 方法:
func (g *GameWindow) Update() error {
// 检测是否要等待鼠标释放
if g.resetPending {
if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
g.resetPending = false
}
return nil
}
if g.gameOver {
// 检查按钮点击
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
mouseX, mouseY := ebiten.CursorPosition()
if mouseX >= 10 && mouseX <= 110 && mouseY >= screenHeight-50 && mouseY <= screenHeight-10 {
g.ResetGame() // 重新开始游戏
g.resetPending = true
}
}
return nil
}
// 处理正常的落子逻辑
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
// 将鼠标坐标转换为棋盘交叉点的格子坐标
gridX, gridY := (x+cellSize/2)/cellSize, (y+cellSize/2)/cellSize
// 确保点击位置在棋盘范围内,且该位置没有棋子
if gridX >= 0 && gridY >= 0 && gridX < boardSize && gridY < boardSize && g.board[gridX][gridY] == 0 {
// 放置棋子
g.board[gridX][gridY] = g.currentPlayer
// 检查是否有玩家获胜
if g.checkWin(gridX, gridY) {
g.gameOver = true
if g.currentPlayer == Player1 {
g.winnerMessage = "Black Wins!"
} else {
g.winnerMessage = "White Wins!"
}
} else {
// 切换玩家
if g.currentPlayer == Player1 {
g.currentPlayer = Player2
} else {
g.currentPlayer = Player1
}
}
}
}
return nil
}
同时,我们也需要完善判负和重新开始游戏的逻辑:
// 检查是否有玩家获胜
func (g *GameWindow) checkWin(x, y int) bool {
directions := [][]int{
{1, 0}, {0, 1}, {1, 1}, {1, -1}, // 四个方向
}
for _, dir := range directions {
count := 1
for i := 1; i < 5; i++ {
nx, ny := x+dir[0]*i, y+dir[1]*i
if nx < 0 || ny < 0 || nx >= boardSize || ny >= boardSize || g.board[nx][ny] != g.currentPlayer {
break
}
count++
}
for i := 1; i < 5; i++ {
nx, ny := x-dir[0]*i, y-dir[1]*i
if nx < 0 || ny < 0 || nx >= boardSize || ny >= boardSize || g.board[nx][ny] != g.currentPlayer {
break
}
count++
}
if count >= 5 {
return true
}
}
return false
}
// 重置游戏状态
func (g *GameWindow) ResetGame() {
g.board = [boardSize][boardSize]Player{}
g.currentPlayer = Player1
g.gameOver = false
g.winnerMessage = ""
}
来看一下最终的实现效果:
到这一步为止,整个小项目已经算是完成了,但是可能还有许多不完善的地方,读者有兴趣可以自行完善。
技术总结
通过本篇文章,你可以初步了解到 Go 语言在 2D 游戏领域的技术以及 ebitengine 游戏引擎的基本使用,还有游戏窗口、刷新帧的概念。想要更深入地了解 ebitengine 可以去官方网站查看技术文档。