引子
最近偶然看到一个Go语言库,口号喊出“一个超级简单(dead simple)的2D游戏引擎”,好奇点开了它的官网。
官网上已经有很多可以在线体验的小游戏了(利用WASM技术)。例如曾经风靡一时的2048:

当然只要安装了Go,我们也键入下面的命令本地运行这个游戏:
$ go run -tags=example github.com/hajimehoshi/ebiten/v2/examples/2048@latest
还有童年《俄罗斯方块》:
有14年左右让无数人疯狂的《Flappy Bird》(或许称为Flappy Gopher更贴切一点😀):

这些瞬间让我产生了极大的兴趣。简单浏览一下文档,整体感觉下来,虽然与成熟的游戏引擎(如Cocos2dx,DirectX,Unity3d等)相比,ebiten功能还不算丰富。但是麻雀虽小,五脏俱全。ebiten的API设计比较简单,使用也很方便,即使对于新手也可以在1-2个小时内掌握,并开发出一款简单的游戏。更妙的是,Go语言让ebitengine实现了跨平台!
接下来的3篇文章,我会介绍ebitengine这个库。对于游戏引擎来说,只介绍它的API用法似乎有点纸上谈兵。恰好我想起之前看到一个《外星人入侵》的小游戏,刚好可以拿来练手。那请大家坐稳扶好,我们出发咯。
安装
ebitengine 要求Go版本 >= 1.15。使用go module下载这个包:
$ go get -u github.com/hajimehoshi/ebiten/v2
显示窗口
游戏开发第一步是将游戏窗口显示出来,并且能在窗口上显示一些文字。先看代码:
package main
import (
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type Game struct{}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Hello, World")
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 320, 240
}
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("外星人入侵")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
使用命令go run
运行该程序:
$ go run main.go
我们会看到一个窗口,标题为外星人入侵,并且左上角显示了文字Hello,World:

现在我们来分析使用ebiten开发的游戏程序的结构。
首先,ebiten引擎运行时要求传入一个游戏对象,该对象的必须实现ebiten.Game
这个接口:
// Game defines necessary functions for a game.
type Game interface {
Update() error
Draw(screen *Image)
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
ebiten.Game
接口定义了ebiten游戏需要的3个方法:Update
,Draw
和Layout
。
Update
:每个tick都会被调用。tick是引擎更新的一个时间单位,默认为1/60s。tick的倒数我们一般称为帧,即游戏的更新频率。默认ebiten游戏是60帧,即每秒更新60次。该方法主要用来更新游戏的逻辑状态,例如子弹位置更新。上面的例子中,游戏对象没有任何状态,故Update
方法为空。注意到Update
方法的返回值为error
类型,当Update
方法返回一个非空的error
值时,游戏停止。在上面的例子中,我们一直返回nil,故只有关闭窗口时游戏才停止。Draw
:每帧(frame)调用。帧是渲染使用的一个时间单位,依赖显示器的刷新率。如果显示器的刷新率为60Hz,Draw
将会每秒被调用60次。Draw
接受一个类型为*ebiten.Image
的screen对象。ebiten引擎每帧会渲染这个screen。在上