c/3d/snake3d.c · 27092 字节 · 下载原文
/* snake3d.c —— **3D 贪吃蛇**:用「写一个真游戏」来验 3D 接口好不好用。
* snake3d.c — **3D Snake**: a real game written to test whether the 3D API is pleasant to use.
*
* ## 这个例子存在的理由(与 `solar3d.c` 分工不同)
* ## Why this example exists (a different job from `solar3d.c`)
*
* `solar3d` 压的是**声明式运动**(`ui_3d_obj_set_spin` / `ui_3d_obj_set_orbit`,主循环一个几何调用都不发),
* `solar3d` stresses **declarative motion** (`ui_3d_obj_set_spin` / `ui_3d_obj_set_orbit`; the main loop issues no geometry calls at all),
* `g3_all.c` 压的是**号段逐号验收**(每个号一条断言)。两者都没压到**游戏真正的主循环**:
* `g3_all.c` stresses **per-syscall acceptance** (one assertion per syscall number). Neither exercises a **real game main loop**:
*
* · **近 200 个物体**同时存在(`ui_3d_obj_create` 建 196 个 + 4 面墙;`VML3D_MAX_OBJECTS` 自 2026-10-08 起是 1024,
* 从前 256 时这叫"逼近上限",现在离得开)
* - **~200 objects alive at once** (`ui_3d_obj_create` builds 196 plus 4 walls; `VML3D_MAX_OBJECTS` has been 1024
* since 2026-10-08, so "close to the cap" only held back when it was 256)
* · **每帧改一批物体的位置/可见性/颜色**(`ui_3d_obj_set_pos` × 蛇长 + 食物)
* - **Mutate a batch of objects every frame**: position / visibility / color (`ui_3d_obj_set_pos` x snake length + food)
* · **用 `ui_3d_obj_set_visible` 当"对象池"**(一次建满、之后只切可见性,不再建/删)
* - **Use `ui_3d_obj_set_visible` as an object pool** (build all once, then only toggle visibility; never create/destroy)
* · 两种网格 + `ui_3d_obj_set_rot`(四面墙复用同一张网格,两侧转 90°)
* - Two meshes + `ui_3d_obj_set_rot` (the four walls share one mesh; two of them rotated 90 degrees)
* · 3D 物体与 2D HUD **同屏叠放**(分数、游戏结束)
* - 3D objects and a 2D HUD **overlaid on the same screen** (score, game over)
*
* 这些正是"接口好不好用"的判据:建 196 个物体要几行?改位置要不要重建?可见性切换贵不贵?
* These are exactly the questions that decide whether the API is pleasant: how many lines to build 196 objects? Does moving one require a rebuild? How expensive is a visibility toggle?
*
* ## 判据(能自己响)
* ## Pass criteria (self-checking)
*
* 正常玩:方向键 / WASD / 滑动转向,撞墙或撞自己结束,空格重来。
* Normal play: arrow keys / WASD / swipe to turn; hitting a wall or yourself ends the game; Space restarts.
* 机械化验证:把 `RUN_FRAMES` 改成大于 0,跑完打印
* Automated check: set `RUN_FRAMES` greater than 0; when it finishes it prints
* `SNAKE-DONE frames=… len=… score=…`(行首锚定即可断)。
* `SNAKE-DONE frames=... len=... score=...` (anchor at line start to assert on it).
*
* ## 坐标系与手感
* ## Coordinate system and feel
*
* 棋盘在 **XZ 平面**(y = 0),方块中心 y = 0.5。相机斜俯视、注视棋盘中心。
* The board lies on the **XZ plane** (y = 0); block centers sit at y = 0.5. The camera looks down at an angle, aimed at the board center.
* 斜俯视下 `+X` 朝屏幕右、`+Z` 朝屏幕下 ⇒ `cx+1` 往右、`cy+1` 往下。
* In that view `+X` points screen-right and `+Z` points screen-down => `cx+1` moves right and `cy+1` moves down.
*/
#include <stdio.h>
#include <vml_3d.h>
#include <vml_ui.h>
/* 机械验证用:> 0 时跑满这么多帧就退出(正常玩保持 0)。*/
/* Machine verification: when > 0, exit after this many frames (keep 0 for normal play). */
#define RUN_FRAMES 0
/* ── 棋盘与规则 ──────────────────────────────────────────────────────────── */
/* ── Board and rules ──────────────────────────────────────────────────────── */
#define GW 14 /* 宽(X 方向格数) */
/* GW: board width in cells along X */
#define GH 14 /* 高(Z 方向格数) */
/* GH: board height in cells along Z */
#define CELLS (GW * GH) /* 196 —— 全部预算成物体(旧上限 256 下是"逼近",新上限 1024 下已经很宽裕) */
/* 196 -- all preallocated as objects ("close to the cap" under the old 256; comfortable under the new 1024) */
#define STEP_MS 240 /* 每 240ms 走一步(= 老写法的 6 帧 @25fps,约 4.2 步/秒) */
/* One step every 240 ms (= the old 6 frames at 25fps, about 4.2 steps/sec) */
#define MAX_SNAKE 180 /* 蛇最长到这里就"通关"(不超物体上限) */
/* Snake "wins" at this length (stays under the object cap) */
#define CELL_PITCH 1.15f /* 格距:留缝,看得出是一格一格 */
/* Cell pitch: leave a gap so single cells are visible */
/* ── 颜色(0xAARRGGBB)──────────────────────────────────────────────────── */
/* ── Colors (0xAARRGGBB) ────────────────────────────────────────────────── */
#define C_BG 0xFF0B1020
#define C_HEAD 0xFF7CFF4A
#define C_BODY 0xFF2FA84A
#define C_FOOD 0xFFFF4A3D
#define C_WALL 0xFF1B2440
/* ══ 网格装配(与 `solar3d.c` 同一套写法)══════════════════════════════════
/* ══ Mesh assembly (same style as `solar3d.c`) ═════════════════════════════════
* 顶点 = 8 个 float32(pos3 + nrm3 + uv2,共 32 字节),紧随其后是 int32 索引。
* Vertex = 8 float32 (pos3 + nrm3 + uv2, 32 bytes total), immediately followed by int32 indices.
* ⚠ 顶点与索引**在同一块内存里**,索引按 int 指针写 —— 这是位模式搬运,不是数值转换。
* ⚠ Vertices and indices live **in the same memory block**; indices are written through an int pointer -- a bit-pattern move, not a numeric conversion. */
float MB[1200];
int g_vn;
int g_in;
void mb_vertex(int i, float px, float py, float pz, float nx, float ny, float nz) {
int b;
b = i * 8;
MB[b + 0] = px; MB[b + 1] = py; MB[b + 2] = pz;
MB[b + 3] = nx; MB[b + 4] = ny; MB[b + 5] = nz;
MB[b + 6] = 0.0f; MB[b + 7] = 0.0f;
}
void mb_index(int k, int v) {
int* ip;
ip = (int*)&MB[0];
ip[g_vn * 8 + k] = v;
}
int mb_ptr(void) { return (int)&MB[0]; }
/* 轴对齐长方体(以原点为中心,half = 半边长):6 面 × 4 顶点 = 24 顶点、36 索引。
/* Axis-aligned box (centered at the origin, half = half-extent): 6 faces x 4 vertices = 24 vertices, 36 indices.
* 法线**按面给常量** —— 立方体上"归一化位置当法线"是错的(角上会朝 45°,棱边发糊)。
* Normals are **constants per face** -- "normalize the position to get a normal" is wrong on a cube (corners would point 45 degrees, edges blur).
*
* ⚠ 绕序刻意**不保证**:后端不剔除背面,片元的双面光照走 `abs(dot(n, L))`
* ⚠ Winding order is deliberately **not guaranteed**: the backend does no back-face culling and the fragment shader uses two-sided lighting `abs(dot(n, L))`
* ⇒ 绕序反了也不会变黑(只会两面一样亮)。需要"正面亮、背面暗"时才要管绕序。
* => a reversed winding never goes black (both sides just light the same). Only care about winding when you need "front bright, back dark". */
void gen_box(float hx, float hy, float hz) {
int f; int v; int k;
float nx[6]; float ny[6]; float nz[6];
nx[0] = 1.0f; ny[0] = 0.0f; nz[0] = 0.0f;
nx[1] = -1.0f; ny[1] = 0.0f; nz[1] = 0.0f;
nx[2] = 0.0f; ny[2] = 1.0f; nz[2] = 0.0f;
nx[3] = 0.0f; ny[3] = -1.0f; nz[3] = 0.0f;
nx[4] = 0.0f; ny[4] = 0.0f; nz[4] = 1.0f;
nx[5] = 0.0f; ny[5] = 0.0f; nz[5] = -1.0f;
v = 0;
for (f = 0; f < 6; f = f + 1) {
int u; int w; int q;
if (f == 0 || f == 1) { u = 1; w = 2; } /* ±X 面:另两轴是 Y/Z */
/* ±X face: the other two axes are Y/Z */
else if (f == 2 || f == 3) { u = 0; w = 2; } /* ±Y 面:X/Z */
/* ±Y face: X/Z */
else { u = 0; w = 1; } /* ±Z 面:X/Y */
/* ±Z face: X/Y */
for (q = 0; q < 4; q = q + 1) {
float a; float b; float px; float py; float pz;
a = ((q == 0 || q == 3) ? 1.0f : -1.0f) * ((u == 0) ? hx : ((u == 1) ? hy : hz));
b = ((q == 0 || q == 1) ? 1.0f : -1.0f) * ((w == 0) ? hx : ((w == 1) ? hy : hz));
if (f == 0) { px = hx; py = a; pz = b; }
else if (f == 1) { px = -hx; py = a; pz = b; }
else if (f == 2) { px = a; py = hy; pz = b; }
else if (f == 3) { px = a; py = -hy; pz = b; }
else if (f == 4) { px = a; py = b; pz = hz; }
else { px = a; py = b; pz = -hz; }
mb_vertex(v, px, py, pz, nx[f], ny[f], nz[f]);
v = v + 1;
}
}
g_vn = v;
k = 0;
for (f = 0; f < 6; f = f + 1) {
int a;
a = f * 4;
mb_index(k, a); k = k + 1;
mb_index(k, a + 1); k = k + 1;
mb_index(k, a + 2); k = k + 1;
mb_index(k, a); k = k + 1;
mb_index(k, a + 2); k = k + 1;
mb_index(k, a + 3); k = k + 1;
}
g_in = k;
}
/* ══ 游戏状态 ════════════════════════════════════════════════════════════ */
/* ══ Game state ═════════════════════════════════════════════════════════ */
int obj_of_cell[CELLS]; /* 每格一个物体(**对象池**:只切可见性,不建不删) */
/* One object per cell (**object pool**: only toggle visibility, never create/destroy) */
int vp;
int snake[CELLS][2]; /* 蛇身格子坐标,[0] 是头 */
/* Snake body cell coordinates; [0] is the head */
int snake_len;
int nextx; int nexty; /* 下一帧生效的方向(见主循环里的说明) */
/* Direction to take effect next frame (see the note in the main loop) */
int dirx; int diry; /* 已经生效的方向 */
/* Direction already in effect */
int foodx; int foody;
int score;
int gameover;
int frame;
int lastMs; int stepAccMs; /* 墙钟步进(见主循环里那段的说明) */
/* Wall-clock stepping (see that section in the main loop) */
int rnd;
int rnd_next(void) {
rnd = rnd * 1103515245 + 12345;
rnd = (rnd >> 16) & 32767;
return rnd;
}
int cell_index(int cx, int cy) { return cy * GW + cx; }
float cell_x(int cx) { return ((float)cx - (GW - 1) * 0.5f) * CELL_PITCH; }
float cell_z(int cy) { return ((float)cy - (GH - 1) * 0.5f) * CELL_PITCH; }
void place_cell(int cx, int cy, int obj, int argb) {
ui_3d_obj_set_pos(obj, cell_x(cx), 0.5f, cell_z(cy));
ui_3d_obj_set_color(obj, argb);
ui_3d_obj_set_visible(obj, 1);
}
int cell_has_snake(int cx, int cy) {
int i;
for (i = 0; i < snake_len; i = i + 1) {
if (snake[i][0] == cx && snake[i][1] == cy) return 1;
}
return 0;
}
void spawn_food(void) {
int i; int cx; int cy;
for (i = 0; i < 500; i = i + 1) {
cx = rnd_next() % GW;
cy = rnd_next() % GH;
if (cell_has_snake(cx, cy) == 0) { foodx = cx; foody = cy; return; }
}
}
void reset_game(void) {
int i;
for (i = 0; i < CELLS; i = i + 1) ui_3d_obj_set_visible(obj_of_cell[i], 0);
snake_len = 3;
snake[0][0] = GW / 2; snake[0][1] = GH / 2;
snake[1][0] = GW / 2 - 1; snake[1][1] = GH / 2;
snake[2][0] = GW / 2 - 2; snake[2][1] = GH / 2;
nextx = 1; nexty = 0;
dirx = 1; diry = 0;
score = 0;
gameover = 0;
spawn_food();
}
/* 把蛇身与食物摆出来(**每帧重摆**:位置、颜色、可见性都要跟上) */
/* Place the snake body and the food (**re-placed every frame**: position, color and visibility must all keep up) */
void draw_board(void) {
int i;
for (i = 0; i < snake_len; i = i + 1) {
int o;
o = obj_of_cell[cell_index(snake[i][0], snake[i][1])];
if (i == 0) place_cell(snake[i][0], snake[i][1], o, C_HEAD);
else place_cell(snake[i][0], snake[i][1], o, C_BODY);
}
place_cell(foodx, foody, obj_of_cell[cell_index(foodx, foody)], C_FOOD);
}
void step_game(void) {
int nx; int ny; int i; int grew;
if (gameover) return;
nx = snake[0][0] + nextx;
ny = snake[0][1] + nexty;
dirx = nextx; diry = nexty;
if (nx < 0 || ny < 0 || nx >= GW || ny >= GH) { gameover = 1; return; }
/* 撞自己:**尾巴那一格会让开**,所以只查到 snake_len-1(差一位就是"贴着尾巴走也被判死") */
/* Self-collision: **the tail cell vacates**, so only scan up to snake_len-1 (off by one = "walking right behind your tail also kills you") */
for (i = 0; i < snake_len - 1; i = i + 1) {
if (snake[i][0] == nx && snake[i][1] == ny) { gameover = 1; return; }
}
grew = (nx == foodx && ny == foody);
if (grew == 0) {
ui_3d_obj_set_visible(obj_of_cell[cell_index(snake[snake_len - 1][0], snake[snake_len - 1][1])], 0);
} else {
if (snake_len < MAX_SNAKE && snake_len < CELLS) snake_len = snake_len + 1;
score = score + 1;
}
for (i = snake_len - 1; i > 0; i = i - 1) {
snake[i][0] = snake[i - 1][0];
snake[i][1] = snake[i - 1][1];
}
snake[0][0] = nx;
snake[0][1] = ny;
if (grew) spawn_food();
}
/* 转向:**只改"下一帧的方向"**。一帧内连按两次(先上后左)若直接改生效方向,
/* Turning: **only changes "the direction for the next frame"**. If two presses in one frame (up then left) changed the live direction directly,
* 会让蛇当场掉头撞自己,而玩家只按了两下键 —— 这是键盘版贪吃蛇的经典坑。
* the snake would reverse on the spot into itself after only two key presses -- the classic pitfall of keyboard Snake. */
void turn(int nx, int ny) { nextx = nx; nexty = ny; }
int main(void) {
int sw; int sh; int w; int h;
int mesh_box; int mesh_wall;
int i;
int touch[3];
int dragging; int lastTx; int lastTy;
/* ⚠ 问尺寸要用 **_ex**:下面开的窗是「不要手柄」(gamepad=0),而 `ui_scr_get_w()` 按
/* ⚠ Use **_ex** when querying the size: the window opened below is "no gamepad" (gamepad=0), but `ui_scr_get_w()` estimates with the
* **默认** chrome(要手柄)估算 ⇒ 问到的比实际画布矮一截,视口就会贴左上角
* **default** chrome (gamepad present) => it reports less than the real canvas and the viewport sticks to the top-left
* (`solar3d.c` 里记过这个坑:用户实测报的「太阳不在窗口中心」)。
* (`solar3d.c` records this pitfall: a user measured and reported "the sun is not in the center of the window"). */
sw = ui_scr_get_w_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
sh = ui_scr_get_h_ex(ui_scr_get_orientation(), VML_CHROME_NONE);
if (sw <= 0) sw = 640;
if (sh <= 0) sh = 640;
/* 棋盘是方的 ⇒ 开方窗,画面里不留大黑边(窗口宽高就是画布的坐标空间) */
/* The board is square => open a square window so no big black bars appear (the window size is the canvas coordinate space) */
if (sw > sh) { w = sh; h = sh; } else { w = sw; h = sw; }
ui_win_open_ex("3D 贪吃蛇 Snake 3D", w, h, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
vp = ui_3d_viewport_create(0, 0, w, h, 0);
if (vp <= 0) {
/* 拿不到 3D 设备(后端缺失 / 驱动不给上下文)—— 如实报,不装作画出来了 */
/* Could not get a 3D device (backend missing / driver gave no context) -- say so honestly, never pretend it drew something */
printf("NO_3D_DEVICE=1\n");
return 0;
}
ui_3d_viewport_set_bg(vp, C_BG);
/* 相机:斜俯视,**注视棋盘中心(原点)**。方向由 eye 定,距离由 `vp_fit` 按半径推。 */
/* Camera: looking down at an angle, **aimed at the board center (origin)**. Direction is set by eye, distance inferred by `vp_fit` from the radius. */
ui_3d_viewport_set_camera(vp, 0.0f, 1.35f, 1.0f, 0.0f, 0.0f, 0.0f);
ui_3d_viewport_set_fov(vp, 55.0f, 0.5f, 4000.0f);
ui_3d_viewport_fit(vp, 13.5f); /* 棋盘半对角 ≈ 11.4,留点余量 */
/* Board half-diagonal is about 11.4; leave some margin */
ui_3d_viewport_set_light(vp, 0.45f, 1.0f, 0.35f, 0xFFFFFFFF, 0.42f);
/* ── 两张网格:蛇身/食物的方块,与四面墙用的长条 ─────────────────────── */
/* ── Two meshes: the box for the snake/food, and the long bar used by the four walls ─── */
gen_box(0.44f, 0.44f, 0.44f);
mesh_box = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box((GW * CELL_PITCH) * 0.5f + 0.5f, 0.18f, 0.25f);
mesh_wall = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
printf("SNAKE-MESH box=%d wall=%d\n", mesh_box, mesh_wall);
if (mesh_box <= 0 || mesh_wall <= 0) { printf("NO_MESH=1\n"); return 0; }
/* ── 对象池:**一次建满 196 个**,之后全程只切可见性/改位置 ─────────────
/* ── Object pool: **build all 196 up front**; from then on only toggle visibility / change positions ────────────
* 这正是本轮要验的核心用法 —— 建 196 个物体就是下面这 4 行。
* This is the core usage being tested here -- building 196 objects is just the 4 lines below. */
for (i = 0; i < CELLS; i = i + 1) {
int o;
o = ui_3d_obj_create(vp, mesh_box);
if (o <= 0) { printf("SNAKE-OBJ-FAIL=%d\n", i); return 0; }
obj_of_cell[i] = o;
ui_3d_obj_set_visible(o, 0);
}
printf("SNAKE-OBJS=%d\n", CELLS);
/* 四面墙:**同一张网格 + `ui_3d_obj_set_rot` 转 90°**(顺带压一下旋转这条路) */
/* Four walls: **one shared mesh + `ui_3d_obj_set_rot` rotated 90 degrees** (also exercises the rotation path) */
{
float half; float edge;
int k;
half = (GW * CELL_PITCH) * 0.5f + 0.5f;
edge = (GW - 1) * 0.5f * CELL_PITCH + 0.9f;
for (k = 0; k < 4; k = k + 1) {
int o;
o = ui_3d_obj_create(vp, mesh_wall);
if (o <= 0) { printf("SNAKE-WALL-FAIL=%d\n", k); return 0; }
if (k == 0) { ui_3d_obj_set_pos(o, 0.0f, 0.1f, -edge); }
else if (k == 1) { ui_3d_obj_set_pos(o, 0.0f, 0.1f, edge); }
else if (k == 2) { ui_3d_obj_set_pos(o, -edge, 0.1f, 0.0f); ui_3d_obj_set_rot(o, 0.0f, 90.0f, 0.0f); }
else { ui_3d_obj_set_pos(o, edge, 0.1f, 0.0f); ui_3d_obj_set_rot(o, 0.0f, 90.0f, 0.0f); }
ui_3d_obj_set_color(o, C_WALL);
ui_3d_obj_set_visible(o, 1);
}
(void)half;
}
rnd = 20261001;
reset_game();
draw_board();
dragging = 0; lastTx = 0; lastTy = 0;
frame = 0;
lastMs = ui_time_get_ms(); stepAccMs = 0;
while (1) {
/* ── 输入:方向键 / WASD ──────────────────────────────────────────
/* ── Input: arrow keys / WASD ──────────────────────────────────────────
* "不能掉头"的判定用**已生效**的 `dir`,不是 `next` —— 否则连按上下会自杀。
* The "cannot reverse" test uses the **live** `dir`, not `next` -- otherwise mashing up then down is suicide. */
if (ui_key_is_down(VML_KEY_LEFT)) { if (dirx != 1) turn(-1, 0); }
if (ui_key_is_down(VML_KEY_RIGHT)) { if (dirx != -1) turn(1, 0); }
if (ui_key_is_down(VML_KEY_UP)) { if (diry != 1) turn(0, -1); }
if (ui_key_is_down(VML_KEY_DOWN)) { if (diry != -1) turn(0, 1); }
if (ui_key_is_down(65)) { if (dirx != 1) turn(-1, 0); } /* 'A' */
if (ui_key_is_down(68)) { if (dirx != -1) turn(1, 0); } /* 'D' */
if (ui_key_is_down(87)) { if (diry != 1) turn(0, -1); } /* 'W' */
if (ui_key_is_down(83)) { if (diry != -1) turn(0, 1); } /* 'S' */
if (ui_key_is_down(VML_KEY_ESCAPE)) break;
if (gameover && ui_key_is_down(VML_KEY_SPACE)) { reset_game(); draw_board(); }
/* ── 触摸:滑动转向(用**增量**,手指从边上重新落下时才不会跳一大截)── */
/* ── Touch: swipe to turn (use **deltas**, so a finger re-landing near the edge does not jump a big distance) ── */
if (ui_touch_read(0, touch)) {
if (touch[2]) {
if (dragging) {
int dx; int dy; int ax; int ay;
dx = touch[0] - lastTx;
dy = touch[1] - lastTy;
ax = (dx < 0) ? -dx : dx;
ay = (dy < 0) ? -dy : dy;
if ((ax > 16 || ay > 16)) {
if (ax >= ay) { if (dx > 0) turn(1, 0); else turn(-1, 0); }
else { if (dy > 0) turn(0, 1); else turn(0, -1); }
/* ⚠ 判完**立刻重置基准**:不清的话一次长滑会在每帧触发一次转向,
/* ⚠ **Reset the baseline right after a turn**: without it one long swipe triggers a turn every frame,
* 手指还没抬就已经转了好几次(本仓 E106 同族)。
* and the snake turns several times before the finger is even lifted (same family as E106 in this repo). */
lastTx = touch[0]; lastTy = touch[1];
}
} else {
lastTx = touch[0]; lastTy = touch[1];
}
dragging = 1;
} else {
dragging = 0;
}
} else {
dragging = 0;
}
/* ── 步进:**按墙钟**每 STEP_MS 毫秒走一步(用户 2026-10-04 定的口径:
/* ── Stepping: **by wall clock**, one step every STEP_MS ms (the convention the user set on 2026-10-04:
* **帧率不该是玩法的一部分**)。老写法数帧(每 STEP_FRAMES 帧一步,
* **frame rate should not be part of the gameplay**). The old version counted frames (one step per STEP_FRAMES frames,
* 注释里自认"25fps ⇒ 约 4.2 步/秒"),宿主改成跟着显示走之后会快 2.4 倍。
* its comment admitting "25fps => about 4.2 steps/sec"), which runs 2.4x faster once the host follows the display.
* ⚠ 用"欠账累加"而不是"取模":掉帧时能把欠的步数补回来,不会悄悄变慢。
* ⚠ Uses an accumulating deficit rather than a modulo, so dropped frames are caught up instead of the game quietly slowing down. */
{
int now2 = ui_time_get_ms();
stepAccMs = stepAccMs + (now2 - lastMs);
lastMs = now2;
}
if (stepAccMs >= STEP_MS) {
stepAccMs = stepAccMs - STEP_MS;
if (stepAccMs > STEP_MS * 4) stepAccMs = 0; /* 卡太久就别一次补一大串 */
/* stalled too long: do not catch up a whole burst at once */
step_game();
draw_board();
}
/* ── HUD:**2D 叠在 3D 之上**(`ui_2d_clear(0)` = 全透明打底)────────── */
/* ── HUD: **2D drawn on top of 3D** (`ui_2d_clear(0)` = fully transparent base) ── */
ui_2d_clear(0);
ui_2d_text(12, 10, "3D 贪吃蛇 Snake 3D", -1, 15, VML_ANCHOR_LEFT);
{
char buf[64];
sprintf(buf, "len %d score %d", snake_len, score);
ui_2d_text(12, 30, buf, -8355712, 12, VML_ANCHOR_LEFT);
}
if (gameover) {
ui_2d_text(12, 52, "撞了!按空格重来 GAME OVER - SPACE", -65536, 14, VML_ANCHOR_LEFT);
}
ui_2d_present();
/* ⚠ **不是帧节流**(2026-10-04 起**上屏节奏由宿主管**:`ui_2d_present()` 之后宿主按显示预算走,并把这个超时夹到「距下一帧还有多久」)。这里只是「等输入最多等多久」。
/* ⚠ **This is not frame throttling** (since 2026-10-04 **the host owns on-screen pacing**: after `ui_2d_present()` the host paces by the display budget and clamps this timeout to "time left until the next frame"). Here it only means "how long to wait for input at most".
* (当年那句"没有它主循环会在零点几秒内烧完几百帧、屏幕一闪而过",说的是**宿主还没有
* (the old line "without it the main loop burns hundreds of frames in a fraction of a second and the screen flashes by" described an era when **the host had no
* 节流**的年代;现在 `PaceFrame` 已经兜住了。)
* throttling**; nowadays `PaceFrame` covers it.) */
ui_msg_fetch_wait(40);
frame = frame + 1;
if (RUN_FRAMES > 0) { if (frame >= RUN_FRAMES) break; }
}
printf("SNAKE-DONE frames=%d len=%d score=%d\n", frame, snake_len, score);
ui_3d_mesh_free(mesh_box);
ui_3d_mesh_free(mesh_wall);
ui_3d_viewport_free(vp);
return 0;
}