2021SC@SDUSC
本文将重点介绍ebiten的时钟系统以及图像绘制选项中的Filter纹理过滤器。首先时钟系统是用来在游戏的进行过程中对时间线进行一个记录,有获取当前时间,初始化参数,获取当前FPS(每秒传输帧数),TPS(每秒处理事务数),获取TPS的计算计数,更新当前FPS和TPS,更新系统时间等方法,然后Filter是图像绘制选项中的参数,设置绘制图像中的纹理滤镜,下面将逐一分析。
var ( lastNow int64 //lastSystemTime是上次更新中的最后一个系统时间。 //lastSystemTime表示游戏中的逻辑时间,可以大于Curren时间 lastSystemTime int64 currentFPS float64 currentTPS float64 lastUpdated int64 fpsCount = 0 tpsCount = 0 m sync.Mutex ) func init() { n := now() lastNow = n lastSystemTime = n lastUpdated = n }
首先定义全局变量上次更新的时间lastNow,上次系统时间,当前FPS,TPS,最后一次更新,帧数和事务数,以及一个互斥锁。
上述部分变量在init()方法中完成初始化。
其中now函数的定义在同级文件夹下的now.go文件内:
var initTime = time.Now() func now() int64 { // Time.Since()返回单调计时器差(#875): // https://golang.org/pkg/time/#hdr-Monotonic_Clocks return int64(time.Since(initTime)) }
作用是获取当前的系统时间。
然后是在有互斥锁保护的前提下获取当前FPS和TPS:
func CurrentFPS() float64 { m.Lock() v := currentFPS m.Unlock() return v } func CurrentTPS() float64 { m.Lock() v := currentTPS m.Unlock() return v }
下面是根据TPS计数:
func calcCountFromTPS(tps int64, now int64) int { if tps == 0 { return 0 } if tps < 0 { panic("clock: tps must >= 0") } diff := now - lastSystemTime if diff < 0 { return 0 } count := 0 syncWithSystemClock := false //检测上一次时间是否太旧。 //如果TPS太大,如300(#1444),则使用5刻度或5/60秒 if diff > max(int64(time.Second)*5/tps, int64(time.Second)*5/60) { // The previous time is too old. // Let's force to sync the game time with the system clock. syncWithSystemClock = true } else { count = int(diff * tps / int64(time.Second)) } //稳定计数。 //如果不进行此调整,计数可能会不稳定,如0、2、0、2、...。 //TODO:刷新此逻辑,以使其适用于任何FPS。现在,这仅在FPS=TPS时有效。 if count == 0 && (int64(time.Second)/tps/2) < diff { count = 1 } if count == 2 && (int64(time.Second)/tps*3/2) > diff { count = 1 } if syncWithSystemClock { lastSystemTime = now } else { lastSystemTime += int64(count) * int64(time.Second) / tps } return count }
防止出现长时间不更新时间导致此时的每秒处理事务数目过大,所以需要不断的计算并更新这些数值,实现方法是传入当前时间和TPS数,计算当前时间和上次更新时间的差值,如果这个值过大,那么就将TPS进行一个稳定计数。
然后是更新FPS和TPS方法:
func updateFPSAndTPS(now int64, count int) { fpsCount++ tpsCount += count if now < lastUpdated { panic("clock: lastUpdated must be older than now") } if time.Second > time.Duration(now-lastUpdated) { return } currentFPS = float64(fpsCount) * float64(time.Second) / float64(now-lastUpdated) currentTPS = float64(tpsCount) * float64(time.Second) / float64(now-lastUpdated) lastUpdated = now fpsCount = 0 tpsCount = 0 }
传入当前时间和刚刚得到的TPS稳定计数,将帧数加一,事务数加上稳定计数,然后进行异常判断,当前时间不能比上一次更新时间还前且time.Second的值要比now-lastUpdate要小,接着计算FPS的值就是这一段时间内增加的帧数乘以Second除以距离上次更新的时间,然后更新上次更新时间的值,并将帧数和事务数归零。
下面是更新方法:
const SyncWithFPS = -1 //Update更新内部时钟状态,返回整数值。 //表示游戏根据给定的TPS需要更新多少次。 //TPS表示TPS(每秒刻度)。 //如果TPS为SyncWithFPS,则Update始终返回1。 //如果TPS<=0且不是SyncWithFPS,则Update始终返回0。 //。 //预计每帧都会调用Update func Update(tps int) int { m.Lock() defer m.Unlock() n := now() if lastNow > n { //这确保now()必须是单调的(#875)。 panic("clock: lastNow must be older than n") } lastNow = n c := 0 if tps == SyncWithFPS { c = 1 } else if tps > 0 { c = calcCountFromTPS(int64(tps), n) } updateFPSAndTPS(n, c) return c }
每帧都调用,调用now()方法获取当前时间,计算当前TPS数并返回且将FPS数加一。
ebiten的过滤器定义在internal/driver/filter.go文件内,代码如下:
type Filter int const ( FilterNearest Filter = iota FilterLinear FilterScreen )
其中FilterNearest表示最近(边缘清晰)的过滤器,FilterLine表示线性滤镜,filterScreen代表一种特殊的Screen筛选器。仅限内部使用且当使用filterScreen时,可以忽略颜色矩阵或颜色顶点值等参数。
纹理过滤器的其中一个很重要的目的就是图像的缩放,以图像缩放为例,采用最近的过滤器时,用最靠近像素中心的那个纹理单元进行放大和缩小,效率更高,但效果不好,锯齿严重。
采用线性过滤器时,是对靠近像素中心的纹理单元,取加权平均值,用于放大和缩小。效果更好,效率稍低。
但是线性方式存在问题,那就是边缘处理,通常情况下有两种方法,一种是取边缘外元素作为普通点进行加权计算,一种是不取。
前者表示“使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色”,只经过简单比较,需要运算较少,可能速度较快
后者表示“使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色”,需要经过加权平均计算,其中涉及除法运算,可能速度较慢。
具体的调用在根目录下的graphics.go文件中
//Filter表示放大或缩小图像时使用的纹理滤镜类型 type Filter int const ( //FilterNeest表示最近(边缘清晰)的过滤器 FilterNearest Filter = Filter(driver.FilterNearest) //FilterLine表示线性滤镜 FilterLinear Filter = Filter(driver.FilterLinear) //filterScreen代表一种特殊的Screen筛选器。仅限内部使用。 //。 //当使用filterScreen时,可以忽略颜色矩阵或颜色顶点值等参数 filterScreen Filter = Filter(driver.FilterScreen) )