栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Go语言

2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(三)

Go语言 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(三)

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()就是返回事先人为设置好的屏幕宽高,并根据给定的外部尺寸进行调整。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/990936.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号