c/3d/pool3d.c · 26482 字节 · 下载原文
/* pool3d.c —— **3D 桌球**:第二个"用真游戏验接口"的探针。
* pool3d.c — **3D pool**: the second "write a real game to test the API" probe.
*
* ## 它压的是 `snake3d.c` 压不到的那一半
* ## It stresses the half that `snake3d.c` cannot
*
* `snake3d` 是**格子对齐**的(位置只会落在 196 个格点上),而桌球要的是
* `snake3d` is **grid-aligned** (positions only ever land on 196 lattice points), while pool needs
* **连续坐标 + 精确到小数的位置/半径**:
* **continuous coordinates with fractional positions and radii**:
*
* · 球的位置是**浮点**、每帧都在变(不是格子)—— 检验 `ui_3d_obj_set_pos` 的精度与调用频率
* - Ball positions are **floats** changing every frame (not a grid) -- exercises `ui_3d_obj_set_pos` precision and call frequency
* · 球半径靠 `ui_3d_obj_set_scale`(单位球缩放),**大小必须一致**(不同缩放会让碰撞判定失真)
* - Ball radius comes from `ui_3d_obj_set_scale` (scaling a unit sphere); **the size must be consistent** (a different scale would distort collision tests)
* · 球与球、球与库边的**碰撞**(O(n²) 的成对循环,每帧跑)
* - Ball-to-ball and ball-to-cushion **collisions** (an O(n^2) pairwise loop, run every frame)
* · 相机**斜俯视** + 拖动旋转 + 开合缩放(`ui_3d_viewport_set_orbit` / `ui_3d_viewport_set_zoom`)
* - Camera **looking down at an angle** + drag to rotate + pinch to zoom (`ui_3d_viewport_set_orbit` / `ui_3d_viewport_set_zoom`)
* · 3D→2D 投影做 HUD(`ui_3d_obj_get_screen` 标出瞄准线)
* - 3D-to-2D projection to build the HUD (`ui_3d_obj_get_screen` marks the aiming line)
*
* ## 玩法
* ## How to play
*
* 按住往**想打的方向**滑一下(滑多长 = 多大力),白球被打出去;球进袋就消失。
* Hold and swipe in **the direction you want to hit** (swipe length = force); the cue ball shoots off; a ball drops out of play once potted.
* 全部打完或按空格重开。Esc 退出。
* Clear them all, or press Space to restart. Esc quits.
*
* ## 判据
* ## Pass criteria
*
* 机械化验证:把 `RUN_FRAMES` 改成大于 0,跑完打印
* Automated check: set `RUN_FRAMES` greater than 0; when it finishes it prints
* `POOL-DONE frames=… alive=… potted=…`(行首锚定即可断)。
* `POOL-DONE frames=... alive=... potted=...` (anchor at line start to assert on it).
*/
#include <stdio.h>
#include <vml_3d.h>
#include <vml_ui.h>
/* ⚠⚠ **必须包含 math.h**(2026-10-01 实测踩到,卡了一整轮):
/* ⚠⚠ **math.h must be included** (hit on 2026-10-01, cost a whole round of debugging):
* 少了它,`sin`/`cos`/`sqrt` 就成了**隐式声明** —— 前端照样编过、照样链接,
* without it `sin`/`cos`/`sqrt` become **implicit declarations** -- the frontend still compiles and links them,
* 但落到一个**把实参原样返回**的实现上:`sqrt(4.0f)` 得 `4.0`、`sin(0.5f)` 得 `0.5`。
* but they land on an implementation that **returns the argument unchanged**: `sqrt(4.0f)` gives `4.0`, `sin(0.5f)` gives `0.5`.
* 症状是"球体的顶点全是 0 / 全相等" ⇒ **球画不出来,而编译链接运行全绿、不报任何错**。
* The symptom is "every sphere vertex is 0 / all equal" => **nothing draws the sphere, while compile, link and run are all green and report no error at all**.
* `Lib/c/math.h` 才是那条把名字接对的路(它声明的是 `double sin(double)`)。
* `Lib/c/math.h` is the path that wires the names up correctly (it declares `double sin(double)`).
* ⚠ 同一族的既有风险见 `docs/开发教训.md` E132(跨库同名裸标签"后链的赢")——
* ⚠ For the same family of risk see the dev-lessons doc, E132 (a bare label with the same name across libraries: "the last one linked wins") --
* `sin`/`cos`/`sqrt` 在 `fixed` / `mathd` / `mathf` **三个库里都有定义**。
* `sin`/`cos`/`sqrt` are **defined in all three** of the `fixed` / `mathd` / `mathf` libraries. */
#include <math.h>
#define RUN_FRAMES 0
/* ── 台面尺寸(世界单位)──────────────────────────────────────────────── */
/* ── Table dimensions (world units) ──────────────────────────────────────── */
#define TABLE_W 20.0f /* X 方向长 */
/* TABLE_W: length along X */
#define TABLE_D 10.0f /* Z 方向宽 */
/* TABLE_D: width along Z */
#define WALL_H 0.7f /* 库边高 */
/* Cushion height */
#define BALL_R 0.45f /* 球半径 */
/* Ball radius */
#define POCKET_R 0.85f /* 袋口半径(判定用) */
/* Pocket radius (used for the check) */
#define NBALL 10 /* 球数(1 白球 + 9 彩球)—— 够压接口,又看得清 */
/* Ball count (1 cue + 9 colored) -- enough to stress the API while still readable */
#define C_BG 0xFF101418
#define C_CLOTH 0xFF1E6B3C
#define C_CUSH 0xFF6B3A1E
#define C_CUE 0xFFFFFFFF
#define C_BALL1 0xFFFFD54A
#define C_BALL2 0xFF4AA8FF
#define C_BALL3 0xFFFF6B6B
#define C_BALL4 0xFFB07CFF
#define C_BALL5 0xFF6BFFB0
/* ══ 网格装配(与另外两个 3D 例子同一套写法)══ */
/* ══ Mesh assembly (same style as the other two 3D examples) ══ */
float MB[4400];
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]; }
/* 单位球(半径 1):法线就是归一化位置。球类统一用这一张网格 + `ui_3d_obj_set_scale(BALL_R)`。 */
/* Unit sphere (radius 1): the normal is just the normalized position. All balls share this mesh plus `ui_3d_obj_set_scale(BALL_R)`. */
void gen_sphere(int rings, int segs) {
int i; int j; int k; int v;
v = 0;
for (i = 0; i <= rings; i = i + 1) {
float phi = 3.14159265f * i / rings;
float sp = sin(phi);
float cp = cos(phi);
for (j = 0; j <= segs; j = j + 1) {
float th = 6.28318531f * j / segs;
float nx = sp * cos(th);
float ny = cp;
float nz = sp * sin(th);
mb_vertex(v, nx, ny, nz, nx, ny, nz);
v = v + 1;
}
}
g_vn = v;
k = 0;
for (i = 0; i < rings; i = i + 1) {
for (j = 0; j < segs; j = j + 1) {
int a = i * (segs + 1) + j;
int b = a + segs + 1;
mb_index(k, a); k = k + 1;
mb_index(k, b); k = k + 1;
mb_index(k, a + 1); k = k + 1;
mb_index(k, a + 1); k = k + 1;
mb_index(k, b); k = k + 1;
mb_index(k, b + 1); k = k + 1;
}
}
g_in = k;
}
/* 长方体(以原点为中心)—— 台面与库边共用,靠不同的 half 参数区分。 */
/* Box (centered at the origin) -- shared by the table and the cushions, told apart by the different half-extents. */
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 axisU; int axisW; int q;
if (f == 0 || f == 1) { axisU = 1; axisW = 2; }
else if (f == 2 || f == 3) { axisU = 0; axisW = 2; }
else { axisU = 0; axisW = 1; }
for (q = 0; q < 4; q = q + 1) {
float halfU; float halfW; float cx; float cy; float cz;
halfU = ((q == 0 || q == 3) ? 1.0f : -1.0f) *
((axisU == 0) ? hx : ((axisU == 1) ? hy : hz));
halfW = ((q == 0 || q == 1) ? 1.0f : -1.0f) *
((axisW == 0) ? hx : ((axisW == 1) ? hy : hz));
if (f == 0) { cx = hx; cy = halfU; cz = halfW; }
else if (f == 1) { cx = -hx; cy = halfU; cz = halfW; }
else if (f == 2) { cx = halfU; cy = hy; cz = halfW; }
else if (f == 3) { cx = halfU; cy = -hy; cz = halfW; }
else if (f == 4) { cx = halfU; cy = halfW; cz = hz; }
else { cx = halfU; cy = halfW; cz = -hz; }
mb_vertex(v, cx, cy, cz, nx[f], ny[f], nz[f]);
v = v + 1;
}
}
g_vn = v;
k = 0;
for (f = 0; f < 6; f = f + 1) {
int base;
base = f * 4;
mb_index(k, base); k = k + 1;
mb_index(k, base + 1); k = k + 1;
mb_index(k, base + 2); k = k + 1;
mb_index(k, base); k = k + 1;
mb_index(k, base + 2); k = k + 1;
mb_index(k, base + 3); k = k + 1;
}
g_in = k;
}
/* ══ 球的状态 ═══════════════════════════════════════════════════════════ */
/* ══ Ball state ═════════════════════════════════════════════════════════ */
int vp;
int ball_obj[NBALL]; /* 物体句柄 */
/* Object handles */
float bx[NBALL]; /* 位置 x */
/* Position x */
float bz[NBALL]; /* 位置 z */
/* Position z */
float vx[NBALL]; /* 速度 x */
/* Velocity x */
float vz[NBALL]; /* 速度 z */
/* Velocity z */
int alive[NBALL];
int alive_count;
int potted;
int frame;
int lastMs; int physAcc; /* 墙钟步进(见主循环里物理那段) */
/* Wall-clock stepping (see the physics section in the main loop) */
int rnd;
int rnd_next(void) {
rnd = rnd * 1103515245 + 12345;
rnd = (rnd >> 16) & 32767;
return rnd;
}
void sync_ball(int i) {
ui_3d_obj_set_pos(ball_obj[i], bx[i], BALL_R, bz[i]);
ui_3d_obj_set_visible(ball_obj[i], alive[i]);
}
/* 开球摆位:白球在左边,彩球挤在右边一小片(够产生碰撞,又不至于一杆全散) */
/* Rack layout: cue ball on the left, colored balls packed into a small patch on the right (enough to collide, yet not all scattered by one shot) */
void reset_game(void) {
int i;
for (i = 0; i < NBALL; i = i + 1) {
alive[i] = 1;
vx[i] = 0.0f;
vz[i] = 0.0f;
if (i == 0) {
bx[0] = -TABLE_W * 0.32f;
bz[0] = 0.0f;
} else {
int col = (i - 1) / 3;
int row = (i - 1) % 3;
bx[i] = TABLE_W * 0.20f + (float)col * (BALL_R * 2.2f);
bz[i] = ((float)row - 1.0f) * (BALL_R * 2.2f);
}
ui_3d_obj_set_color(ball_obj[i], (i == 0) ? C_CUE :
((i == 1) ? C_BALL1 : ((i == 2) ? C_BALL2 :
((i == 3) ? C_BALL3 : ((i == 4) ? C_BALL4 : C_BALL5)))));
sync_ball(i);
}
alive_count = NBALL;
potted = 0;
}
/* 库边反弹:把球钳回台内并反向速度(带 0.9 的损耗,不然会永远弹) */
/* Cushion bounce: clamp the ball back inside the table and reverse its velocity (with 0.9 damping, or it would bounce forever) */
void hit_cushion(int i) {
float lx = TABLE_W * 0.5f - BALL_R;
float lz = TABLE_D * 0.5f - BALL_R;
if (bx[i] < -lx) { bx[i] = -lx; vx[i] = -vx[i] * 0.9f; }
if (bx[i] > lx) { bx[i] = lx; vx[i] = -vx[i] * 0.9f; }
if (bz[i] < -lz) { bz[i] = -lz; vz[i] = -vz[i] * 0.9f; }
if (bz[i] > lz) { bz[i] = lz; vz[i] = -vz[i] * 0.9f; }
}
/* 落袋:四个角袋 */
/* Pocketing: the four corner pockets */
void check_pocket(int i) {
float px[4]; float pz[4];
int k;
px[0] = -TABLE_W * 0.5f; pz[0] = -TABLE_D * 0.5f;
px[1] = TABLE_W * 0.5f; pz[1] = -TABLE_D * 0.5f;
px[2] = -TABLE_W * 0.5f; pz[2] = TABLE_D * 0.5f;
px[3] = TABLE_W * 0.5f; pz[3] = TABLE_D * 0.5f;
for (k = 0; k < 4; k = k + 1) {
float dx = bx[i] - px[k];
float dz = bz[i] - pz[k];
if (dx * dx + dz * dz < POCKET_R * POCKET_R) {
/* ⚠ 白球落袋**也要消失**(否则它会卡在袋里反复触发);
/* ⚠ The cue ball **also disappears** when potted (otherwise it gets stuck in the pocket and re-triggers it);
* 真实规则是"白球回位",这里从简,并在 HUD 里说清。
* the real rule is "cue ball returns to the baulk"; here we simplify and spell it out in the HUD. */
alive[i] = 0;
alive_count = alive_count - 1;
vx[i] = 0.0f;
vz[i] = 0.0f;
if (i != 0) potted = potted + 1;
sync_ball(i);
return;
}
}
}
/* 一步物理:成对碰撞 + 库边 + 落袋 + 摩擦。dt 固定(每帧一步),保证可复现。 */
/* One physics step: pairwise collisions + cushions + pockets + friction. dt is fixed (one step per frame) for reproducibility. */
void physics(void) {
int i;
int j;
float dt = 0.055f;
float friction = 0.988f;
for (i = 0; i < NBALL; i = i + 1) {
if (alive[i] == 0) continue;
bx[i] = bx[i] + vx[i] * dt;
bz[i] = bz[i] + vz[i] * dt;
vx[i] = vx[i] * friction;
vz[i] = vz[i] * friction;
/* 速度小到看不见就停住 —— 不清零的话球会永远以毫米级速度飘,还每帧重算碰撞 */
/* Stop once the speed is too small to see -- without zeroing it the ball drifts at millimeter-per-second speed forever and recomputes collisions every frame */
if (vx[i] > -0.02f && vx[i] < 0.02f) vx[i] = 0.0f;
if (vz[i] > -0.02f && vz[i] < 0.02f) vz[i] = 0.0f;
hit_cushion(i);
}
/* 球球碰撞:等质量、正碰(沿法线交换法向速度分量)—— 台球的标准近似 */
/* Ball-ball collision: equal mass, head-on (exchange the normal velocity components along the normal) -- the standard billiards approximation */
for (i = 0; i < NBALL; i = i + 1) {
if (alive[i] == 0) continue;
for (j = i + 1; j < NBALL; j = j + 1) {
float dx; float dz; float d2; float d;
float nx; float nz; float rel;
if (alive[j] == 0) continue;
dx = bx[j] - bx[i];
dz = bz[j] - bz[i];
d2 = dx * dx + dz * dz;
if (d2 > (BALL_R * 2.0f) * (BALL_R * 2.0f)) continue;
if (d2 < 1e-6f) { dx = 0.001f; dz = 0.0f; d2 = 1e-6f; }
d = sqrt(d2);
nx = dx / d;
nz = dz / d;
rel = (vx[j] - vx[i]) * nx + (vz[j] - vz[i]) * nz;
if (rel > 0.0f) continue; /* 正在分开,不处理 */
/* separating already, skip */
vx[i] = vx[i] + rel * nx * 0.98f;
vz[i] = vz[i] + rel * nz * 0.98f;
vx[j] = vx[j] - rel * nx * 0.98f;
vz[j] = vz[j] - rel * nz * 0.98f;
/* 位置分离:不推开的话两球会重叠、每帧都判成碰撞("粘在一起抖") */
/* Position separation: without pushing them apart the two balls overlap and register a collision every frame ("stuck together, jittering") */
{
float push = (BALL_R * 2.0f - d) * 0.5f;
bx[i] = bx[i] - nx * push;
bz[i] = bz[i] - nz * push;
bx[j] = bx[j] + nx * push;
bz[j] = bz[j] + nz * push;
}
}
}
for (i = 0; i < NBALL; i = i + 1) {
if (alive[i] == 0) continue;
check_pocket(i);
if (alive[i]) sync_ball(i);
}
}
int main(void) {
int sw; int sh; int w; int h;
int mesh_ball; int mesh_table; int mesh_cushion;
int i;
int touch[3];
int dragging; int lastTx; int lastTy; int downX; int downY;
float camYaw; float camPitch; float camZoom;
int o_table; int o_cush[4];
float lw; float ld;
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 table is horizontal => open a landscape window so no big black bars appear */
if (sw > sh) { w = sw; h = (sw * 3) / 5; if (h > sh) h = sh; }
else { h = sh; w = sw; }
ui_win_open_ex("3D 桌球 Pool 3D", w, h, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
vp = ui_3d_viewport_create(0, 0, w, h, 0);
if (vp <= 0) { printf("NO_3D_DEVICE=1\n"); return 0; }
ui_3d_viewport_set_bg(vp, C_BG);
ui_3d_viewport_set_camera(vp, 0.0f, 1.05f, 0.72f, 0.0f, 0.0f, 0.0f);
ui_3d_viewport_set_fov(vp, 52.0f, 0.5f, 4000.0f);
ui_3d_viewport_fit(vp, 13.0f);
ui_3d_viewport_set_light(vp, 0.40f, 1.0f, 0.30f, 0xFFFFFFFF, 0.46f);
/* ── 网格:球(单位球)+ 台面 + 库边 ───────────────────────────────── */
/* ── Meshes: ball (unit sphere) + table + cushions ───────────────────────── */
gen_sphere(10, 16);
mesh_ball = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(TABLE_W * 0.5f, 0.35f, TABLE_D * 0.5f);
mesh_table = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(TABLE_W * 0.5f + 0.5f, WALL_H * 0.5f, 0.5f);
mesh_cushion = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
printf("POOL-MESH ball=%d table=%d cush=%d\n", mesh_ball, mesh_table, mesh_cushion);
if (mesh_ball <= 0 || mesh_table <= 0 || mesh_cushion <= 0) { printf("NO_MESH=1\n"); return 0; }
/* 台面 */
/* Table surface */
o_table = ui_3d_obj_create(vp, mesh_table);
ui_3d_obj_set_pos(o_table, 0.0f, -0.35f, 0.0f);
ui_3d_obj_set_color(o_table, C_CLOTH);
/* 四面库边:长边两条用原网格,短边两条转 90° */
/* Four cushions: the two long sides use the mesh as-is, the two short sides are rotated 90 degrees */
lw = TABLE_W * 0.5f + 0.5f;
ld = TABLE_D * 0.5f + 0.5f;
for (i = 0; i < 4; i = i + 1) {
int o;
o = ui_3d_obj_create(vp, mesh_cushion);
o_cush[i] = o;
if (i == 0) { ui_3d_obj_set_pos(o, 0.0f, WALL_H * 0.5f, -ld); }
else if (i == 1) { ui_3d_obj_set_pos(o, 0.0f, WALL_H * 0.5f, ld); }
else if (i == 2) { ui_3d_obj_set_pos(o, -lw, WALL_H * 0.5f, 0.0f); ui_3d_obj_set_rot(o, 0.0f, 90.0f, 0.0f); }
else { ui_3d_obj_set_pos(o, lw, WALL_H * 0.5f, 0.0f); ui_3d_obj_set_rot(o, 0.0f, 90.0f, 0.0f); }
ui_3d_obj_set_color(o, C_CUSH);
}
/* 球:单位球缩放到 BALL_R。**缩放只用这一个数** —— 碰撞判定也用它,两处同源。 */
/* Balls: scale the unit sphere to BALL_R. **That single number is the only scale** -- the collision test uses it too, so both sites share one source. */
for (i = 0; i < NBALL; i = i + 1) {
int o;
o = ui_3d_obj_create(vp, mesh_ball);
if (o <= 0) { printf("POOL-OBJ-FAIL=%d\n", i); return 0; }
ball_obj[i] = o;
ui_3d_obj_set_scale(o, BALL_R);
}
printf("POOL-OBJS=%d\n", NBALL);
rnd = 20261002;
reset_game();
dragging = 0; lastTx = 0; lastTy = 0; downX = 0; downY = 0;
camYaw = 0.0f; camPitch = 34.0f; camZoom = 1.0f;
frame = 0;
lastMs = ui_time_get_ms(); physAcc = 0; /* 墙钟步进(见下面物理那段) */
/* Wall-clock stepping (see the physics section below) */
while (1) {
/* ── 触摸:拖动 = 转相机;**按下-滑动-松开** = 出杆 ─────────────────
/* ── Touch: drag = orbit the camera; **press-slide-release** = take a shot ──────
* 同一套触摸要同时表达"转视角"和"打球",所以分两段:
* One touch dialect has to express both "look around" and "hit the ball", so it is split into two phases:
* 按住不动 → 这一段只当"瞄准/转视角",松开时若位移够大就出一杆。
* holding still -> this phase is only "aiming / orbiting"; on release, a large enough displacement fires a shot.
* ⚠ 横轴的符号与 `solar3d` 同一条(`ui_3d_viewport_set_orbit` 的 yaw 是球坐标标准式)。
* ⚠ The horizontal-axis sign follows the same convention as `solar3d` (the yaw of `ui_3d_viewport_set_orbit` is the standard spherical-coordinate form). */
if (ui_touch_read(0, touch)) {
if (touch[2]) {
if (dragging) {
int dx; int dy;
dx = touch[0] - lastTx;
dy = touch[1] - lastTy;
camYaw = camYaw - (float)dx * 0.35f;
camPitch = camPitch + (float)dy * 0.30f;
if (camPitch > 86.0f) camPitch = 86.0f;
if (camPitch < 8.0f) camPitch = 8.0f;
lastTx = touch[0]; lastTy = touch[1];
} else {
downX = touch[0]; downY = touch[1];
lastTx = touch[0]; lastTy = touch[1];
}
dragging = 1;
} else {
if (dragging) {
/* 松开:位移够大就出杆(屏幕方向 → 台面方向:右 = +X,下 = +Z) */
/* Release: fire if the displacement is big enough (screen direction -> table direction: right = +X, down = +Z) */
float fx = (float)(lastTx - downX);
float fy = (float)(lastTy - downY);
float len = sqrt(fx * fx + fy * fy);
if (len > 24.0f && alive[0]) {
float p = len * 0.055f;
if (p > 22.0f) p = 22.0f;
vx[0] = fx / len * p;
vz[0] = fy / len * p;
}
}
dragging = 0;
}
} else {
dragging = 0;
}
if (ui_key_is_down(VML_KEY_SPACE)) reset_game();
if (ui_key_is_down(VML_KEY_ESCAPE)) break;
ui_3d_viewport_set_orbit(vp, camYaw, camPitch);
ui_3d_viewport_set_zoom(vp, camZoom);
/* 物理**步长不变**(原作者要的可复现性,保留),但**每秒跑几步由墙钟决定** ——
/* The physics **step size stays the same** (the reproducibility the original author wanted, kept), but **how many steps run per second is decided by the wall clock** --
* 这才是"帧率不该是玩法的一部分"(用户 2026-10-04)。
* that is what "frame rate should not be part of the gameplay" means (user, 2026-10-04).
* 老写法"每帧恰好一步",而主循环是 ui_msg_fetch_wait(40) ⇒ 25 步/秒;
* The old version did "exactly one step per frame", and the main loop is ui_msg_fetch_wait(40) => 25 steps/sec;
* 宿主改成跟着显示走之后会变成 60 步/秒 ⇒ 球快 2.4 倍。这里按 40ms 一步补回来。
* once the host follows the display it would become 60 steps/sec => balls 2.4x faster. Here it is restored to one step per 40 ms. */
{
int now2 = ui_time_get_ms();
physAcc = physAcc + (now2 - lastMs);
lastMs = now2;
if (physAcc > 40 * 8) physAcc = 0; /* 卡太久别一次补一大串 */
/* stalled too long: do not catch up a whole burst at once */
while (physAcc >= 40) { physAcc = physAcc - 40; physics(); }
}
/* ── HUD ──────────────────────────────────────────────────────── */
ui_2d_clear(0);
ui_2d_text(12, 10, "3D 桌球 Pool 3D", -1, 15, VML_ANCHOR_LEFT);
{
char buf[64];
sprintf(buf, "进球 %d 剩 %d", potted, alive_count);
ui_2d_text(12, 30, buf, -8355712, 12, VML_ANCHOR_LEFT);
}
ui_2d_text(12, 48, "按住滑动松手出杆(往想打的方向)", -8355712, 11, VML_ANCHOR_LEFT);
ui_2d_present();
ui_msg_fetch_wait(40); /* 只是「等输入最多等一会儿」(2026-10-04 起**上屏节奏由宿主管**:`ui_2d_present()` 之后宿主按显示预算走,并把这个超时夹到「距下一帧还有多久」) */
/* Only "wait for input at most a little while" (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") */
frame = frame + 1;
if (RUN_FRAMES > 0) { if (frame >= RUN_FRAMES) break; }
}
printf("POOL-DONE frames=%d alive=%d potted=%d\n", frame, alive_count, potted);
ui_3d_mesh_free(mesh_ball);
ui_3d_mesh_free(mesh_table);
ui_3d_mesh_free(mesh_cushion);
ui_3d_viewport_free(vp);
return 0;
}