This article summarizes the meaning and behavior differences between FPS (Frames Per Second) and TPS (Ticks Per Second) in Ebitengine.
Introduction
Ebitengine provides two concepts for game updates:
- FPS (Frames Per Second)
- TPS (Ticks Per Second)
At first, I couldn’t understand the difference between them, so I wrote down their behavior as a memo.
Note: This article was translated from my original post.
The Differences Between FPS and TPS in Ebitengine
Overview
First, let’s summarize their characteristics.
| FPS | TPS | |
|---|---|---|
| Overview | Frequency of rendering updates per second | Frequency of game logic updates per second |
| Implementation | Draw() is called based on FPS |
Update() is called based on TPS |
| Control | Determined by the display settings of the runtime environment | Determined by Ebitengine settings (e.g., SetTPS()) |
It’s easier to understand if you think of FPS as the drawing update frequency (Draw()), and TPS as the game logic update frequency (Update()).
Another important point is that FPS depends on the player’s display refresh rate settings and is not controlled by Ebitengine.
In contrast, TPS/Update() is controlled inside Ebitengine (default: 60 tps, configurable via SetTPS()), so game logic that depends on update frequency should generally be implemented in Update().
Testing
Now let’s adjust FPS and TPS and check how many times Draw() and Update() are called.
I prepared a simple test code like the one below.
Using this, we check the obtained TPS/FPS values and the actual call counts of Update()/Draw() under different TPS settings and display refresh‑rate conditions.
package main import ( "log" "time" "github.com/hajimehoshi/ebiten/v2" ) const ( TPS = 60 ) type Game struct { updateCount int drawCount int perSec time.Time } func (g *Game) Update() error { now := time.Now() g.updateCount++ // Debug print per sec if now.Sub(g.perSec) >= time.Second { log.Printf("TPS: %.2f, FPS: %.2f", ebiten.ActualTPS(), ebiten.ActualFPS()) log.Printf("Update() was called in this sec: %d times", g.updateCount) log.Printf("Draw() was called in this sec: %d times\n\n", g.drawCount) g.updateCount = 0 g.drawCount = 0 g.perSec = now } return nil } func (g *Game) Draw(screen *ebiten.Image) { g.drawCount++ } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return 640, 480 } func main() { game := &Game{ perSec: time.Now(), } ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Show FPS and TPS with Update()/Draw()") ebiten.SetTPS(TPS) if err := ebiten.RunGame(game); err != nil { log.Fatal(err) } }
TPS 60, Display Refresh Rate 60
Here are the results when TPS is set to 60 and the display refresh rate is also 60:
2024/11/25 14:41:54 TPS: 59.92, FPS: 59.92 2024/11/25 14:41:54 Update() was called in this sec: 60 times 2024/11/25 14:41:54 Draw() was called in this sec: 60 times 2024/11/25 14:41:55 TPS: 60.09, FPS: 60.09 2024/11/25 14:41:55 Update() was called in this sec: 61 times 2024/11/25 14:41:55 Draw() was called in this sec: 61 times 2024/11/25 14:41:56 TPS: 59.98, FPS: 59.98 2024/11/25 14:41:56 Update() was called in this sec: 61 times 2024/11/25 14:41:56 Draw() was called in this sec: 61 times
Both TPS and FPS are around 60, and Update() and Draw() are each called about 60 times per second.
TPS 100, Display Refresh Rate 60
Next, TPS is set to 100 while the display refresh rate stays at 60:
2024/11/25 14:46:47 TPS: 100.32, FPS: 60.00 2024/11/25 14:46:47 Update() was called in this sec: 100 times 2024/11/25 14:46:47 Draw() was called in this sec: 60 times 2024/11/25 14:46:48 TPS: 100.41, FPS: 60.05 2024/11/25 14:46:48 Update() was called in this sec: 100 times 2024/11/25 14:46:48 Draw() was called in this sec: 60 times 2024/11/25 14:46:49 TPS: 99.92, FPS: 59.95 2024/11/25 14:46:49 Update() was called in this sec: 100 times 2024/11/25 14:46:49 Draw() was called in this sec: 60 times
TPS becomes 100, and Update() is called 100 times per second.
Meanwhile, because the display refresh rate is still 60, FPS and the Draw() call count remain at 60.
TPS 60, Display Refresh Rate 50
Finally, TPS remains 60 but the display refresh rate is set to 50.
2024/11/25 15:04:59 TPS: 60.00, FPS: 50.00 2024/11/25 15:04:59 Update() was called in this sec: 60 times 2024/11/25 15:04:59 Draw() was called in this sec: 50 times 2024/11/25 15:05:00 TPS: 59.86, FPS: 50.05 2024/11/25 15:05:00 Update() was called in this sec: 61 times 2024/11/25 15:05:00 Draw() was called in this sec: 51 times 2024/11/25 15:05:01 TPS: 59.94, FPS: 49.95 2024/11/25 15:05:01 Update() was called in this sec: 60 times 2024/11/25 15:05:01 Draw() was called in this sec: 50 times
FPS and Draw() call frequency drop to 50/sec.
This shows that the player’s display settings affect FPS and Draw() frequency.
TPS/Update() call frequency remains at the configured 60, unaffected by FPS.
The characteristics described above are confirmed again through this test.
Conclusion
In this article, I summarized the characteristics of TPS and FPS in Ebitengine and verified them with simple tests.
These were concepts I had only understood vaguely, so this was a good learning experience.
I hope it helps someone!
[Related Articles]