games: 60 fps rendering loop + smoother Snake controls

Both games now render at 60 fps independent of the game-logic step rate:
the main loop caps select() at one frame (16 ms) so the screen updates
smoothly even when gravity/snake steps are slow, and renders every frame
or whenever state changed.

Snake: replaced the turn queue with a single buffered direction (latest
keypress wins) so rapid key presses don't queue up and misfire. Base speed
set to 100 px/sec (~90 ms per step) for controllable, responsive play.
Progressive difficulty (+1 px/sec per 5 food) and edge wrap are unchanged.
This commit is contained in:
Luxferre
2026-10-24 08:43:39 +03:00
parent 0abf305dff
commit 9d4ca2e3a6
6 changed files with 52 additions and 43 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32 -35
View File
@@ -102,13 +102,12 @@ static State state = PLAYING;
static int snake[COLS * ROWS][2]; // x,y per segment (0 = tail)
static int snakelen = 4;
static int dirx = 1, diry = 0; // current direction
static int tqx[4], tqy[4]; // queued pending turns (rapid presses not lost)
static int tqh = 0, tqt = 0, tqn = 0;
static int ndirx = 1, ndiry = 0; // buffered next direction (latest keypress wins)
static int foodx, foody;
static int score = 0;
static int best = 0;
static int speed = 90; // ms per step (derived from pps below)
static int pps = 130; // base speed px/sec; +1 px/sec per 5 food eaten
static int pps = 100; // base speed px/sec; +1 px/sec per 5 food eaten
static unsigned long nextTick = 0;
static bool running = true;
static unsigned long now_ms() {
@@ -164,33 +163,19 @@ static void reset_game() {
snake[i][1] = cy;
}
dirx = 1; diry = 0;
tqh = tqt = tqn = 0;
ndirx = 1; ndiry = 0;
score = 0;
pps = 130;
pps = 100;
update_speed();
place_food();
state = PLAYING;
nextTick = now_ms() + speed;
}
// Queue a pending direction change; rapid key presses are stored so none are lost.
static void tq_push(int dx, int dy) {
if (tqn >= 4) return; // queue full: drop the new turn
tqx[(tqh + tqn) % 4] = dx;
tqy[(tqh + tqn) % 4] = dy;
tqn++;
}
static void step() {
// apply the oldest queued turn (no reversing); rapid presses are not lost
int tdx = 0, tdy = 0;
if (tqn > 0) {
tdx = tqx[tqt]; tdy = tqy[tqt];
tqt = (tqt + 1) % 4;
tqn--;
}
if (!(tdx == -dirx && tdy == -diry) && !(tdx == 0 && tdy == 0)) {
dirx = tdx; diry = tdy;
// apply buffered direction (no reversing); latest keypress wins
if (!(ndirx == -dirx && ndiry == -diry) && !(ndirx == 0 && ndiry == 0)) {
dirx = ndirx; diry = ndiry;
}
int hx = snake[snakelen - 1][0] + dirx;
int hy = snake[snakelen - 1][1] + diry;
@@ -217,7 +202,7 @@ static void step() {
score += 10;
if (score > best) best = score;
int food = score / 10; // food items eaten so far
pps = 130 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
pps = 100 + food / 5; // +1 px/sec per 5 food eaten (progressive difficulty)
update_speed();
place_food();
}
@@ -288,13 +273,13 @@ static void render() {
static void handle_key(KeySym ks) {
switch (ks) {
case XK_Up: case XK_w: case XK_KP_Up: case XK_2:
tq_push(0, -1); break;
ndirx = 0; ndiry = -1; break;
case XK_Down: case XK_s: case XK_KP_Down: case XK_8: case XK_x:
tq_push(0, 1); break;
ndirx = 0; ndiry = 1; break;
case XK_Left: case XK_a: case XK_KP_Left: case XK_4:
tq_push(-1, 0); break;
ndirx = -1; ndiry = 0; break;
case XK_Right: case XK_d: case XK_KP_Right: case XK_6:
tq_push(1, 0); break;
ndirx = 1; ndiry = 0; break;
case XK_5: case XK_Return: case XK_KP_Enter: case XK_space: case XK_p:
if (state == PLAYING) state = PAUSED;
else if (state == PAUSED) { state = PLAYING; nextTick = now_ms() + speed; }
@@ -390,12 +375,19 @@ int main() {
render();
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 60; // 60 fps rendering
unsigned long lastFrame = now_ms();
while (running) {
long until = (long)nextTick - (long)now_ms();
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 = until / 1000;
tv.tv_usec = (until % 1000) * 1000;
tv.tv_sec = wait / 1000;
tv.tv_usec = (wait % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
@@ -407,17 +399,22 @@ int main() {
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
dirty = true;
}
if (!running) break;
dirty = true;
}
if (running && now_ms() >= nextTick) {
now = now_ms();
if (running && now >= nextTick) {
if (state == PLAYING) step();
if (state == PLAYING) nextTick = now_ms() + speed;
else nextTick = now_ms() + 80; // keep loop responsive while paused/over
if (state == PLAYING) nextTick = now + speed;
else nextTick = now + 80; // keep loop responsive while paused/over
dirty = true;
}
if (dirty) render();
// Render at 60 fps or whenever state changed
if (dirty || now - lastFrame >= FRAME_MS) {
render();
lastFrame = now;
}
}
XFreeGC(dpy, gc);
+20 -8
View File
@@ -499,12 +499,19 @@ int main() {
render();
int xfd = ConnectionNumber(dpy);
const unsigned long FRAME_MS = 1000 / 60; // 60 fps rendering
unsigned long lastFrame = now_ms();
while (running) {
long until = (long)nextTick - (long)now_ms();
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 = until / 1000;
tv.tv_usec = (until % 1000) * 1000;
tv.tv_sec = wait / 1000;
tv.tv_usec = (wait % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(xfd, &fds);
@@ -516,17 +523,22 @@ int main() {
XNextEvent(dpy, &e);
handle_event(e);
if (!running) break;
dirty = true;
}
if (!running) break;
dirty = true;
}
if (running && now_ms() >= nextTick) {
now = now_ms();
if (running && now >= nextTick) {
if (state == PLAYING) step();
if (state == PLAYING) nextTick = now_ms() + speed;
else nextTick = now_ms() + 80;
if (state == PLAYING) nextTick = now + speed;
else nextTick = now + 80;
dirty = true;
}
if (dirty) render();
// Render at 60 fps or whenever state changed
if (dirty || now - lastFrame >= FRAME_MS) {
render();
lastFrame = now;
}
}
XFreeGC(dpy, gc);