2021SC@SDUSC Application and practice of software engineering in Software College of Shandong University -- Ebiten code analysis and source code analysis

2021SC@SDUSC

1, Overview

This article will focus on ebiten's clock system and Filter texture Filter in image rendering options. Firstly, the clock system is used to record the timeline during the game, including obtaining the current time, initializing parameters, obtaining the current FPS (frames transmitted per second), TPS (transactions processed per second), obtaining the calculation count of TPS, updating the current FPS and TPS, updating the system time, etc. then Filter is the parameter in the image rendering option, Set the texture Filter in the painted image, which will be analyzed one by one below.

2, Code analysis

1. Clock system

var (
	lastNow int64

	//lastSystemTime is the last system time in the last update.
	//lastSystemTime represents the logical time in the game, which can be greater than Curren time
	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
}

First, define the last update time of the global variable lastNow, the last system time, the current FPS, TPS, the last update, the number of frames and transactions, and a mutex.
Some of the above variables are initialized in the init() method.
The definition of the now function is in the now.go file under the peer folder:

var initTime = time.Now()

func now() int64 {
	// Time.Since() returns the monotone timer difference (#875):
	// https://golang.org/pkg/time/#hdr-Monotonic_Clocks
	return int64(time.Since(initTime))
}

The function is to obtain the current system time.
Then obtain the current FPS and TPS on the premise of mutual exclusion lock protection:

func CurrentFPS() float64 {
	m.Lock()
	v := currentFPS
	m.Unlock()
	return v
}

func CurrentTPS() float64 {
	m.Lock()
	v := currentTPS
	m.Unlock()
	return v
}

Here are the counts based on 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

	//Check whether the last time is too old.
	//If the TPS is too large, such as 300 (#1444), use 5 scale or 5 / 60 second
	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))
	}

	//Steady count.
	//If this adjustment is not made, the count may be unstable, such as 0, 2, 0, 2.
	//TODO: refresh this logic to make it applicable to any FPS. Now, this is only valid when 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
}

To prevent the number of transactions processed per second from being too large due to long-term non update time, these values need to be continuously calculated and updated. The implementation method is to pass in the current time and the number of TPS and calculate the difference between the current time and the last update time. If this value is too large, a stable count of TPS will be made.

Then update FPS and TPS methods:

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
}

Pass in the current time and the TPS stability count just obtained, add one to the number of frames, and add the stability count to the number of transactions, and then judge the exception. The current time cannot be earlier than the last update time, and the value of time.Second is smaller than now lastupdate. Then calculate the FPS value, which is the number of frames increased in this period multiplied by Second divided by the time from the last update, Then update the value of the last update time and reset the number of frames and transactions to zero.

Here is the update method:

const SyncWithFPS = -1

//Update updates the internal clock status and returns an integer value.
//Indicates how many times the game needs to be updated according to a given TPS.
//TPS stands for TPS (scale per second).
//If the TPS is SyncWithFPS, Update always returns 1.
//If TPS < = 0 and not SyncWithFPS, Update always returns 0.
//. 
//Update is expected to be called every frame
func Update(tps int) int {
	m.Lock()
	defer m.Unlock()

	n := now()
	if lastNow > n {
		//This ensures that now() must be monotonous (#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
}

Call every frame, call the now() method to obtain the current time, calculate the current TPS number and return, and add one to the FPS number.

2.Filter texture filter

The filter of ebiten is defined in the internal/driver/filter.go file, and the code is as follows:

type Filter int

const (
	FilterNearest Filter = iota
	FilterLinear
	FilterScreen
)

Where FilterNearest represents the nearest (clear edge) filter, FilterLine represents the linear filter, and filterScreen represents a special Screen filter. For internal use only, and when filterScreen is used, parameters such as color matrix or color vertex value can be ignored.

One of the very important purposes of texture filter is image scaling. Taking image scaling as an example, when using the nearest filter, the texture unit closest to the pixel center is used to zoom in and out, which is more efficient, but the effect is not good and the sawtooth is serious.
When a linear filter is used, the weighted average value is taken for the texture unit close to the center of the pixel for zooming in and out. The effect is better and the efficiency is slightly lower.
However, there is a problem with the linear method, that is, edge processing. Generally, there are two methods, one is to take the elements outside the edge as ordinary points for weighted calculation, and the other is not to take.
The former means "use the color of the pixel with the closest coordinates in the texture as the pixel color to be drawn". After simple comparison, it requires less operation and may be faster
The latter means "using several colors with the closest coordinates in the texture to obtain the pixel color to be drawn through the weighted average algorithm". It needs to be calculated by weighted average, which involves division operation, which may be slow.
The specific call is in the graphics.go file in the root directory

//Filter indicates the type of texture filter used when zooming in or out of an image
type Filter int

const (
	//FilterNeest indicates the nearest (clear edge) filter
	FilterNearest Filter = Filter(driver.FilterNearest)

	//FilterLine represents a linear filter
	FilterLinear Filter = Filter(driver.FilterLinear)

	//filterScreen represents a special Screen filter. For internal use only.
	//. 
	//When using filterScreen, parameters such as color matrix or color vertex value can be ignored
	filterScreen Filter = Filter(driver.FilterScreen)
)

Keywords: Go

Added by jthomp7 on Sat, 27 Nov 2021 00:33:42 +0200