games: responsive game loop with non-blocking input polling
Replaced the blocking select()-based main loop with a proper game loop: - Process ALL pending X11 input (XPending) before every frame — no more missed keypresses from select() blocking for up to a full frame - Render every frame at 30 fps (FRAME_MS = 33 ms) consistently - Frame pacing uses select() with a timeout that wakes immediately on input, so keypresses are processed within milliseconds - Game logic (snake steps, tetris gravity) still runs at its own rate via nextTick, decoupled from render rate This eliminates input lag and missed keypresses in both Snake and Tetris.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28
-31
@@ -376,45 +376,42 @@ int main() {
|
||||
|
||||
int xfd = ConnectionNumber(dpy);
|
||||
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
|
||||
unsigned long lastFrame = now_ms();
|
||||
unsigned long frameStart = now_ms();
|
||||
|
||||
while (running) {
|
||||
unsigned long now = now_ms();
|
||||
long until = (long)nextTick - (long)now;
|
||||
if (until < 0) until = 0;
|
||||
// Never block select longer than one frame so the screen stays smooth
|
||||
long wait = (until < (long)FRAME_MS) ? until : (long)FRAME_MS;
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = wait / 1000;
|
||||
tv.tv_usec = (wait % 1000) * 1000;
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
int r = select(xfd + 1, &fds, nullptr, nullptr, &tv);
|
||||
bool dirty = false;
|
||||
if (r > 0) {
|
||||
// Process all pending input immediately (non-blocking) before each frame
|
||||
while (XPending(dpy)) {
|
||||
XEvent e;
|
||||
while (XPending(dpy)) {
|
||||
XNextEvent(dpy, &e);
|
||||
handle_event(e);
|
||||
if (!running) break;
|
||||
dirty = true;
|
||||
}
|
||||
XNextEvent(dpy, &e);
|
||||
handle_event(e);
|
||||
if (!running) break;
|
||||
}
|
||||
now = now_ms();
|
||||
if (running && now >= nextTick) {
|
||||
if (!running) break;
|
||||
|
||||
unsigned long now = now_ms();
|
||||
|
||||
// Game logic step at its own rate
|
||||
if (now >= nextTick) {
|
||||
if (state == PLAYING) step();
|
||||
if (state == PLAYING) nextTick = now + speed;
|
||||
else nextTick = now + 80; // keep loop responsive while paused/over
|
||||
dirty = true;
|
||||
nextTick = now + (state == PLAYING ? speed : 80);
|
||||
}
|
||||
// Render at 60 fps or whenever state changed
|
||||
if (dirty || now - lastFrame >= FRAME_MS) {
|
||||
render();
|
||||
lastFrame = now;
|
||||
|
||||
// Render every frame
|
||||
render();
|
||||
|
||||
// Frame pacing: sleep until next frame, but wake up immediately on input
|
||||
now = now_ms();
|
||||
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
|
||||
if (sleepMs > 0) {
|
||||
struct timeval tv;
|
||||
tv.tv_sec = sleepMs / 1000;
|
||||
tv.tv_usec = (sleepMs % 1000) * 1000;
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
select(xfd + 1, &fds, nullptr, nullptr, &tv);
|
||||
}
|
||||
frameStart = now_ms();
|
||||
}
|
||||
|
||||
XFreeGC(dpy, gc);
|
||||
|
||||
+28
-31
@@ -500,45 +500,42 @@ int main() {
|
||||
|
||||
int xfd = ConnectionNumber(dpy);
|
||||
const unsigned long FRAME_MS = 1000 / 30; // 30 fps rendering
|
||||
unsigned long lastFrame = now_ms();
|
||||
unsigned long frameStart = now_ms();
|
||||
|
||||
while (running) {
|
||||
unsigned long now = now_ms();
|
||||
long until = (long)nextTick - (long)now;
|
||||
if (until < 0) until = 0;
|
||||
// Never block select longer than one frame so the screen stays smooth
|
||||
long wait = (until < (long)FRAME_MS) ? until : (long)FRAME_MS;
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = wait / 1000;
|
||||
tv.tv_usec = (wait % 1000) * 1000;
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
int r = select(xfd + 1, &fds, nullptr, nullptr, &tv);
|
||||
bool dirty = false;
|
||||
if (r > 0) {
|
||||
// Process all pending input immediately (non-blocking) before each frame
|
||||
while (XPending(dpy)) {
|
||||
XEvent e;
|
||||
while (XPending(dpy)) {
|
||||
XNextEvent(dpy, &e);
|
||||
handle_event(e);
|
||||
if (!running) break;
|
||||
dirty = true;
|
||||
}
|
||||
XNextEvent(dpy, &e);
|
||||
handle_event(e);
|
||||
if (!running) break;
|
||||
}
|
||||
now = now_ms();
|
||||
if (running && now >= nextTick) {
|
||||
if (!running) break;
|
||||
|
||||
unsigned long now = now_ms();
|
||||
|
||||
// Game logic step at its own rate
|
||||
if (now >= nextTick) {
|
||||
if (state == PLAYING) step();
|
||||
if (state == PLAYING) nextTick = now + speed;
|
||||
else nextTick = now + 80;
|
||||
dirty = true;
|
||||
nextTick = now + (state == PLAYING ? speed : 80);
|
||||
}
|
||||
// Render at 60 fps or whenever state changed
|
||||
if (dirty || now - lastFrame >= FRAME_MS) {
|
||||
render();
|
||||
lastFrame = now;
|
||||
|
||||
// Render every frame
|
||||
render();
|
||||
|
||||
// Frame pacing: sleep until next frame, but wake up immediately on input
|
||||
now = now_ms();
|
||||
long sleepMs = (long)FRAME_MS - (long)(now - frameStart);
|
||||
if (sleepMs > 0) {
|
||||
struct timeval tv;
|
||||
tv.tv_sec = sleepMs / 1000;
|
||||
tv.tv_usec = (sleepMs % 1000) * 1000;
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(xfd, &fds);
|
||||
select(xfd + 1, &fds, nullptr, nullptr, &tv);
|
||||
}
|
||||
frameStart = now_ms();
|
||||
}
|
||||
|
||||
XFreeGC(dpy, gc);
|
||||
|
||||
Reference in New Issue
Block a user