Skip to content

Commit d6a59f3

Browse files
committed
Add a framerate handling example.
1 parent 1304109 commit d6a59f3

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

examples/framerate.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
# To the extent possible under law, the libtcod maintainers have waived all
3+
# copyright and related or neighboring rights for this example. This work is
4+
# published from: United States.
5+
# https://creativecommons.org/publicdomain/zero/1.0/
6+
"""A system to control time since the original libtcod tools are deprecated.
7+
"""
8+
import statistics
9+
import time
10+
from collections import deque
11+
from typing import Deque, List, Optional, Tuple
12+
13+
import tcod
14+
15+
WIDTH, HEIGHT = 720, 480
16+
17+
18+
class Clock:
19+
"""Measure framerate performance and sync to a given framerate.
20+
21+
Everything important is handled by `Clock.sync`. You can use the fps
22+
properties to track the performance of an application.
23+
"""
24+
25+
def __init__(self) -> None:
26+
self.last_time = time.perf_counter() # Last time this was synced.
27+
self.time_samples: Deque[float] = deque() # Delta time samples.
28+
self.max_samples = 64 # Number of fps samples to log. Can be changed.
29+
self.drift_time = 0.0 # Tracks how much the last frame was overshot.
30+
31+
def sync(self, fps: Optional[float] = None) -> float:
32+
"""Sync to a given framerate and return the delta time.
33+
34+
`fps` is the desired framerate in frames-per-second. If None is given
35+
then this function will track the time and framerate without waiting.
36+
37+
`fps` must be above zero when given.
38+
"""
39+
if fps is not None:
40+
# Wait until a target time based on the last time and framerate.
41+
desired_frame_time = 1 / fps
42+
target_time = self.last_time + desired_frame_time - self.drift_time
43+
# Sleep might take slightly longer than asked.
44+
sleep_time = max(0, target_time - self.last_time - 0.001)
45+
if sleep_time:
46+
time.sleep(sleep_time)
47+
# Busy wait until the target_time is reached.
48+
while (drift_time := time.perf_counter() - target_time) < 0:
49+
pass
50+
self.drift_time = min(drift_time, desired_frame_time)
51+
52+
# Get the delta time.
53+
current_time = time.perf_counter()
54+
delta_time = max(0, current_time - self.last_time)
55+
self.last_time = current_time
56+
57+
# Record the performance of the current frame.
58+
self.time_samples.append(delta_time)
59+
while len(self.time_samples) > self.max_samples:
60+
self.time_samples.popleft()
61+
62+
return delta_time
63+
64+
@property
65+
def min_fps(self) -> float:
66+
"""The FPS of the slowest frame."""
67+
try:
68+
return 1 / max(self.time_samples)
69+
except (ValueError, ZeroDivisionError):
70+
return 0
71+
72+
@property
73+
def max_fps(self) -> float:
74+
"""The FPS of the fastest frame."""
75+
try:
76+
return 1 / min(self.time_samples)
77+
except (ValueError, ZeroDivisionError):
78+
return 0
79+
80+
@property
81+
def mean_fps(self) -> float:
82+
"""The FPS of the sampled frames overall."""
83+
if not self.time_samples:
84+
return 0
85+
try:
86+
return 1 / statistics.fmean(self.time_samples)
87+
except ZeroDivisionError:
88+
return 0
89+
90+
@property
91+
def median_fps(self) -> float:
92+
"""The FPS of the median frame."""
93+
if not self.time_samples:
94+
return 0
95+
try:
96+
return 1 / statistics.median(self.time_samples)
97+
except ZeroDivisionError:
98+
return 0
99+
100+
@property
101+
def last_fps(self) -> float:
102+
"""The FPS of the most recent frame."""
103+
if not self.time_samples or self.time_samples[-1] == 0:
104+
return 0
105+
return 1 / self.time_samples[-1]
106+
107+
108+
def main() -> None:
109+
"""Example program for Clock."""
110+
# vsync is False in this example, but you'll want it to be True unless you
111+
# need to benchmark or set framerates above 60 FPS.
112+
with tcod.context.new(width=WIDTH, height=HEIGHT, vsync=False) as context:
113+
line_x = 0 # Highlight a line, helpful to measure frames visually.
114+
clock = Clock()
115+
delta_time = 0.0 # The time passed between frames.
116+
desired_fps = 50
117+
while True:
118+
console = context.new_console(order="F")
119+
console.rgb["bg"][line_x % console.width, :] = (255, 0, 0)
120+
console.print(
121+
1,
122+
1,
123+
f"Current time:{time.perf_counter() * 1000:8.2f}ms"
124+
f"\nDelta time:{delta_time * 1000:8.2f}ms"
125+
f"\nDesired FPS:{desired_fps:3d} (use scroll whell to adjust)"
126+
f"\n last:{clock.last_fps:.2f} fps"
127+
f"\n mean:{clock.mean_fps:.2f} fps"
128+
f"\nmedian:{clock.median_fps:.2f} fps"
129+
f"\n min:{clock.min_fps:.2f} fps"
130+
f"\n max:{clock.max_fps:.2f} fps",
131+
)
132+
context.present(console, integer_scaling=True)
133+
delta_time = clock.sync(fps=desired_fps)
134+
line_x += 1
135+
136+
# Handle events.
137+
for event in tcod.event.get():
138+
context.convert_event(event) # Set tile coordinates for event.
139+
if isinstance(event, tcod.event.Quit):
140+
raise SystemExit()
141+
elif isinstance(event, tcod.event.MouseWheel):
142+
desired_fps = max(1, desired_fps + event.y)
143+
144+
145+
if __name__ == "__main__":
146+
main()

0 commit comments

Comments
 (0)