Home

Awesome

Ebitengine TPS vs FPS

This document aims to help users of the Ebitengine game engine and shed some light on the topic of ticks per second (TPS) vs frames per second (FPS), which has proven to be a common point of confusion both for newcomers and experienced devs alike.

Time in game engines

Let's say we want to create an impressive game where a shrimp appears from the left of the screen and starts moving to the right. Then, at any time, we want to allow the player to press space to make the shrimp change direction!

Ok. ¯\_(ツ)_/¯

How do you update the shrimp's position? At which point? How much? Do you use a physical simulation that considers speed and elapsed time, or do you just move it a fixed amount of pixels on each update?

Well, it depends. Most modern game engines offer two ways to work with time:

For example, in Unity you have Time.deltaTime and Time.fixedDeltaTime. In Godot, instead, you have separate methods for idle processing (frame by frame) and physics processing (fixed timestep).

In Ebitengine, the Update function is part of a fixed timestep loop, and it's called based on the ticks per second (TPS) of your game. By default, Ebitengine's TPS is 60, which means that the Update method will be called 60 times per second. In other words, unless you modify the TPS with SetTPS, the fixed timestep will be 1000/60 = 16.666 milliseconds. To use delta times, instead, you would use the standard Golang time package, but we won't talk about this now.

So... if both approaches can be used, what should you do? Well, in Ebitengine, you almost always want to stick to Update and the fixed timestep loop... but to understand why, we first need to talk about the advantages and disadvantages of each method.

Fixed timestep vs variable deltas

Let's start with fixed timestep loops. Using a fixed timestep and letting the game engine call your game's update method seems like the easiest way to make games:

On the other hand, there are also some downsides:

Let's look at variable time deltas now:

Most modern games are actually using both methods for different parts of the game. This makes sense because big games nowadays end up running in wildly different devices, with many configurable quality levels, with gamers pushing their hardware to run at 240FPS and all that stuff. Despite all this, we still want first-person shooters to be sharply responsive, open world games to keep up the pace no matter how crowded the area gets, high-quality animations to play as smoothly as possible... and this means that game developers and modern engines need to use every trick in the book to try to push for the best results.

If you are working with Ebitengine, though, those are almost never your main concerns. Ebitengine is mostly used for 2D games, which will often feature low resolution assets, pixel art, shrimps and choppy animations. Here, keeping it simple and preserving determinism are typically more important than smoothness under astringent performance constraints. You should have enough headroom to make your games work properly even on lower end devices while using only the fixed timestep Update method.

Back to Ebitengine: Update vs Draw

Ebitengine has two main methods that you have to implement to make your game work: Update and Draw. As we have seen, Update is called regularly based on the ticks per second (TPS) configured for your game. On the other hand, Draw will be called based on the refresh rate of your screen. If your screen has a refresh rate of 60Hz, by default Ebitengine will try to call Draw 60 times each second (60FPS).

Your main logic should be processed on Update, using the fixed timestep. If you need smoother visual effects (maybe related to shaders), you may compute delta times by yourself in the Draw method... but as a rule of thumb I'd suggest to not complicate your own life unless you have a good reason for it.

In some special cases one may decide to use only time deltas and process all the logic inside Draw itself, but this would be the exception in Ebitengine and is beyond the scope of this document.

If the game lags, Ebitengine will prioritize TPS over FPS in order to avoid the game slowing down. Some people get really concerned about this, but if your game lags what you should be doing is profiling and optimizing your code, not worrying about time deltas.

Other common concerns

But TPS are not fixed? ActualTPS can return different values? How is that reliable?

If your game is not lagging, the average rate at which Update is called may vary slightly, but TPS will be stable and compensated in the long run1. You won't be losing time or advancing in time unless the game starts lagging a lot (and in that case, you should start profiling and optimizing instead).

ActualTPS is mostly a debug method that you can use while developing to keep track of the game performance. That said, a good method to keep track of your game's performance is setting FPSModeVsyncOffMaximum (only for development, not releases!) and displaying the ActualFPS value on the screen or the title bar:

ebiten.SetWindowTitle(fmt.Sprintf("Game Title | %.2ffps", ebiten.ActualFPS())

FPS will start fluctuating earlier than TPS if something is lagging.

But I learned that using time deltas is the way to do things right!

It's the main method to manage time in most game engines and the main topic of most "game loop implementation" tutorials. That explains why a lot of people is confused when working with Ebitengine, but the "Fixed timestep vs variable deltas" section already discussed the advantages and disadvantages of each method; for a library like Ebitengine, using a fixed timestep loop makes sense as the go-to approach.

But if Update and Draw can be called at different rates, that's... weird?

You may have Update be called multiple times consecutively before Draw, or the other way around. It's good to keep this in mind in order to avoid developing an incorrect mental model of how the main Ebitengine loop works, but once you understand that TPS and FPS can each go at their own pace it is not that surprising.

But is it still reasonable to compute time deltas if I really need them?

In some cases —for example when working with shaders—, if you want some visual effect to be as smooth as possible and have good reason to believe that Update will be called fairly less often than Draw (so, TPS are lower than FPS), computing time deltas may make sense. It can also make sense for some games that want to support high refresh rate displays as smoothly as possible. In most cases, though, worrying about time deltas in Ebitengine causes more harm than good. If you have read this document and understand the differences clearly, do whatever you want. Otherwise, keep your hands out of time.Now() and continue trying to understand.

What you should never do is computing time deltas in the Update method: if computing elapsed times in a method that's part of a fixed timestep loop doesn't trigger any alarms, you probably still don't fully grasp the difference between TPS and FPS, between fixed timesteps and variable time deltas, between Update and Draw.

Can I change TPS during the game?

The API allows it, and Hajime Hoshi mentioned using it to implement a turbo mode for a game. It's really hard to come up with reasonable use-cases for it outside a few tricks like these, though.

But you are wrong about...

Feel free to drop by Ebitengine's discord server and duel ;)

Quick summary

Footnotes

  1. There are some subtleties, actually. Time only passes uniformly between visual updates (graphical frames), not necessarily between multiple Update calls. Internally, Ebitengine calls all the "accumulated" or "pending" updates sequentially in a for loop, without any waits. This means that if your update method does very little work and TPS and FPS differ or get momentarily out of sync, you may receive multiple consecutive calls to Update with virtually no real time elapsed between them. You should not make any assumptions about the real time elapsed between Update calls: updates correspond to "simulation steps", but they don't control the synchronization of their corresponding visuals. That's controlled by the graphical buffer swaps that Ebitengine issues after each Draw. 2