2021SC@SDUSC
- 一.概述
- 二.代码分析
- 1.Update()
- 2.Draw()
- 3.Layout()
本文将介绍ebiten在RunGame函数中逐帧执行的Update()方法,Draw()方法和Layout()方法。
首先分析确定这三个方法所具有的功能是什么:
Update()——更新游戏逻辑,几乎每一帧都要调用,Ebiten默认每秒调用Update方法60次,且第一次调用Draw方法之前确保必须先调用一次Update方法初始化。
Draw()——绘制游戏画面,每帧都会调用游戏的Draw函数来绘制屏幕,参数表示一个屏幕图像。采用更新后的内容作为游戏屏幕。
Layout()——接受以设备无关像素为单位的原生外部大小,并返回游戏的逻辑屏幕大小,在台式机上,外部是窗口或显示器(全屏模式)。在浏览器上,外部是一个实体元素;在手机上,外部是视野的大小。即使外部大小和屏幕大小不同,渲染比例也会自动调整为与外部相配。确保在第一次调用Update之前先调用Layout。
下面将逐一分析其实现原理。
二.代码分析上述三个方法接口定义如下:
type Game interface {
Update() error
Draw(screen *Image)
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
1.Update()
更新游戏逻辑,该方法通常是在游戏的开发过程中自行实现上述game类的接口,每个游戏Update()中的内容都不相同,以游戏2048为例,Update()方法如下:
//Update更新当前游戏状态
func (g *Game) Update() error {
g.input.Update()
if err := g.board.Update(g.input); err != nil {
return err
}
return nil
}
其中此时的Game类和g.input.Update(),g.board.Update()都是开发者自己定义的方法,用于更新该游戏的逻辑,此处就不过多赘述了。
2.Draw()还是以2048游戏为例,此时定义的Draw()如下:
func (g *Game) Draw(screen *ebiten.Image) {
if g.boardImage == nil {
w, h := g.board.Size()
g.boardImage = ebiten.NewImage(w, h)
}
screen.Fill(backgroundColor)
g.board.Draw(g.boardImage)
op := &ebiten.DrawImageOptions{}
sw, sh := screen.Size()
bw, bh := g.boardImage.Size()
x := (sw - bw) / 2
y := (sh - bh) / 2
op.GeoM.Translate(float64(x), float64(y))
screen.DrawImage(g.boardImage, op)
}
功能是将当前游戏以 图像 的形式绘制到指定屏幕上。
其中多次调用了ebiten的方法,下面依次分析。
实现判断当前屏幕释放存在可以绘制的背景图像,如果没有,那么调用ebiten的新建图像方法:
//newimage返回空图像。
//。
//如果宽度或高度小于1或大于设备相关的最大尺寸,newimage会死机。
//。
//如果RunGame已经结束,newimage会死机。
func NewImage(width, height int) *Image {
if isRunGameEnded() {
panic(fmt.Sprintf("ebiten: NewImage cannot be called after RunGame finishes"))
}
if width <= 0 {
panic(fmt.Sprintf("ebiten: width at NewImage must be positive but %d", width))
}
if height <= 0 {
panic(fmt.Sprintf("ebiten: height at NewImage must be positive but %d", height))
}
i := &Image{
mipmap: mipmap.New(width, height),
bounds: image.Rect(0, 0, width, height),
}
i.addr = i
return i
}
返回一个初始化的空Image类型的对象。位于根目录下的image.go文件内,Image类即图像类的定义如下:
//Image表示一组矩形像素。
//像素格式为Alpha预乘RGBA。
//Image实现image.Image和dra.Image。
type Image struct {
//addr保持自我检查复制。
//类似的示例请参见string.Builder。
addr *Image
mipmap *mipmap.Mipmap //Mipmap(有时候拼写成mipmap)是一种电脑图形图像技术,用于在三维图像的二维代替物中达到立体感效应。
bounds image.Rectangle //边界
original *Image
screen bool
}
接下来调用Fill方法给这个空图像填充背景颜色。位于根目录下的image.go文件内
Fill方法的实现细节如下:
//Fill用纯色填充图像。
//。
//释放图像时,Fill不执行任何操作
func (i *Image) Fill(clr color.Color) {
//使用原来的大小覆盖整个地域(#1691)。
//DrawImage自动裁剪渲染区域。
orig := i
if i.isSubImage() {
orig = i.original
}
w, h := orig.Size()
op := &DrawImageOptions{}
op.GeoM.Scale(float64(w), float64(h))
r, g, b, a := clr.RGBA()
var rf, gf, bf, af float64
if a > 0 {
rf = float64(r) / float64(a)
gf = float64(g) / float64(a)
bf = float64(b) / float64(a)
af = float64(a) / 0xffff
}
op.ColorM.Scale(rf, gf, bf, af)
op.CompositeMode = CompositeModeCopy
i.DrawImage(emptySubImage, op)
}
先判断i图像是不是子图像,如果是,那么就获取它的源图像,并获取宽高,再设置绘制图像时的选项,即DrawImageOptions。下一步中的GeoM是图像的几何变换选项,该方法的目的是将图像的矩阵按照上面给出的宽高进行缩放,下面的颜色同理,然后再设置图像绘制的合成模式为复制。最后调用DrawImage方法完成绘制。
其中DrawImageOptions的定义如下:
//DrawImageOptions表示DrawImage的选项。
type DrawImageOptions struct {
//Geom是要绘制的几何矩阵。
//默认(零)值为Identity,即在(0,0)处绘制图像
GeoM GeoM
//ColorM是要绘制的颜色矩阵。
//默认(零)值为Identity,不会改变任何颜色。
ColorM ColorM
//CompositeMode是绘制的合成模式。
//默认值(零)为规则的Alpha混合
CompositeMode CompositeMode
//滤镜是纹理滤镜的一种。
//默认(零)值为FilterNeadest。
Filter Filter
}
两个缩放方法的实现即进行相关的矩阵计算,实现如下,分别位于根目录下的geom.go文件内和colorm.go文件内。
//Scale按(x,y)缩放矩阵。
func (g *GeoM) Scale(x, y float64) {
a := (g.a_1 + 1) * x
b := g.b * x
tx := g.tx * x
c := g.c * y
d := (g.d_1 + 1) * y
ty := g.ty * y
g.a_1 = a - 1
g.b = b
g.c = c
g.d_1 = d - 1
g.tx = tx
g.ty = ty
}
//Scale按(r,g,b,a)缩放矩阵
func (c *ColorM) Scale(r, g, b, a float64) {
c.impl = c.affineColorM().Scale(float32(r), float32(g), float32(b), float32(a))
}
最后是DrawImage方法,传入两个参数分别是图像和图像绘制选项,实现细节如下:
func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
i.copyCheck()
if img.isDisposed() {
panic("ebiten: the given image to DrawImage must not be disposed")
}
if i.isDisposed() {
return
}
dstBounds := i.Bounds()
dstRegion := driver.Region{
X: float32(dstBounds.Min.X),
Y: float32(dstBounds.Min.Y),
Width: float32(dstBounds.Dx()),
Height: float32(dstBounds.Dy()),
}
//锁定前计算折点,因为用户可以在。
//options.ImageParts接口没有死锁(例如调用Image函数)。
if options == nil {
options = &DrawImageOptions{}
}
bounds := img.Bounds()
mode := driver.CompositeMode(options.CompositeMode)
filter := driver.Filter(options.Filter)
a, b, c, d, tx, ty := options.GeoM.elements32()
sx0 := float32(bounds.Min.X)
sy0 := float32(bounds.Min.Y)
sx1 := float32(bounds.Max.X)
sy1 := float32(bounds.Max.Y)
vs := graphics.QuadVertices(sx0, sy0, sx1, sy1, a, b, c, d, tx, ty, 1, 1, 1, 1)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageNum]*mipmap.Mipmap{img.mipmap}
i.mipmap.DrawTriangles(srcs, vs, is, options.ColorM.affineColorM(), mode, filter, driver.AddressUnsafe, dstRegion, driver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, canSkipMipmap(options.GeoM, filter))
}
首先是i.copyCheck()方法,检查图像i和i.addr是否相同即进行保持自我复制检查。
再检查i和传入的img参数的mipmap是否为空,均不为空后进入下一步。
i.Bounds()返回目的图像的边界即bounds属性。
driver.Region提取出刚刚获得的边界中的边界起点X,Y值以及边界宽高。
然后判断传入的option是否为空,如果为空则重新初始化。
接下来获取传入图像的边界img.Bounds(),图像绘制选项的合成模式,纹理过滤以及几何变换参数a, b, c, d, tx, ty。
获取边界的宽高的最大值和最小值,并根据上述变量得到一个不会和其他分片重叠的四边形分片。
is是一个四元指数,src是着色器图像数量*一个初始化的Mipmap对象。
最后根据mipmap的DrawTriangles方法完成图像的绘制。
该方法就是Draw()方法的 核心部分 ,上述最后三步的实现细节在此暂时不讨论。
3.Layout()const (
ScreenWidth = 420
ScreenHeight = 600
boardSize = 4
)
//Layout实现ebiten.Game的布局
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return ScreenWidth, ScreenHeight
}
在本例子中,Layout()就是返回事先人为设置好的屏幕宽高,并根据给定的外部尺寸进行调整。



