c/3d/air3d.c · 25353 字节 · 下载原文
/* air3d.c —— **3D 空战**:第四个"用真游戏验接口"的探针。
* air3d.c — **3D air combat**: the fourth "write a real game to test the API" probe.
*
* ## 它压的是前三个都没有的那一环:**自由三维朝向**
* ## What it stresses that the previous three did not: **free 3D orientation**
*
* · `snake3d` 只改位置;`pool3d` 只改位置;`race3d` 只绕 **Y** 转(航向)。
* · `snake3d` and `pool3d` change position only; `race3d` rotates about **Y** only (heading).
* 空战要 **yaw + pitch 两个轴**同时生效 ⇒ 这是 `ui_3d_obj_set_rot(o, ax, ay, az)` 的
* Air combat needs **both yaw and pitch** active at once => this is where the **multi-axis** use of
* **多轴**用法第一次被真游戏压到。
* `ui_3d_obj_set_rot(o, ax, ay, az)` gets stressed by a real game for the first time.
* · 相机要跟着一个**会俯仰的**载具走 —— 相机的 eye 由载具的**前向量**推出来,
* · The camera must follow a vehicle that **pitches** -- the camera eye is derived from the vehicle's **forward vector**,
* 而不只是"在它后面"。上一轮 `race3d` 只压了水平跟随。
* not merely "behind it". The previous `race3d` only stressed horizontal follow.
* · 弹幕:一个**对象池**(子弹反复生灭),外加命中判定的三维距离。
* · Barrage: an **object pool** (bullets spawning and dying repeatedly), plus 3D distance for hit tests.
*
* ## ⚠ 本轮实测钉下的两条接口事实
* ## ⚠ Two API facts nailed down by measurement this round
*
* ① **旋转是 `R = Rz·Ry·Rx`(度)**,模型机头朝 **+Z** ⇒ 飞机用
* (1) **Rotation is `R = Rz*Ry*Rx` (degrees)**; model nose faces **+Z** => for the plane use
* `ui_3d_obj_set_rot(o, -pitch_deg, yaw_deg, 0)`(**X 取负**:正 pitch 表示抬头,
* `ui_3d_obj_set_rot(o, -pitch_deg, yaw_deg, 0)` (**negate X**: positive pitch means nose-up,
* 而绕 X 的正向旋转把 +Z 转向 **-Y**,即低头)。
* while a positive rotation about X turns +Z toward **-Y**, i.e. nose-down).
* ② **前向量 = (cos(pitch)·sin(yaw), sin(pitch), cos(pitch)·cos(yaw))** ——
* (2) **Forward = (cos(pitch)*sin(yaw), sin(pitch), cos(pitch)*cos(yaw))** --
* 位移与相机都由它推,**只留这一个真源**("同一处布局两套算法必然漂")。
* both motion and camera derive from it; **keep this as the single source of truth** ("two algorithms for one layout inevitably drift").
*
* ## 玩法
* ## Gameplay
*
* 飞机自己往前飞:←/→ 转向、↑/↓ 抬头低头,点屏幕 / 空格开火。
* The plane flies forward on its own: left/right turns, up/down pitches; tap the screen / press space to fire.
* 打中敌机得分,敌机会在别处重生。Esc 退出。
* Hitting an enemy scores; enemies respawn elsewhere. Esc quits.
*
* ## 判据
* ## Criteria
*
* 机械化验证:`RUN_FRAMES` > 0 时跑满帧数并打印
* Mechanical check: when `RUN_FRAMES` > 0 it runs the full frame count and prints
* `AIR-DONE frames=… score=… shots=…`(行首锚定即可断)。
* `AIR-DONE frames=... score=... shots=...` (anchored at line start, so a grep can settle it).
*/
#include <stdio.h>
#include <math.h>
#include <vml_3d.h>
#include <vml_ui.h>
#define RUN_FRAMES 0
#define N_ENEMY 6
#define N_BULLET 24
#define SPEED 34.0f /* 飞机前进速度(世界单位/秒) */
/* Plane forward speed (world units/sec) */
#define BULLET_SP 90.0f
#define FIRE_CD 8 /* 开火冷却(帧) */
/* Fire cooldown (frames) */
#define HIT_R 3.2f /* 命中判定半径 */
/* Hit-test radius */
#define C_BG 0xFF0C1A2E
#define C_GROUND 0xFF24522F
#define C_PLAYER 0xFF58B6FF
#define C_ENEMY 0xFFE05A5A
#define C_BULLET 0xFFFFE066
#define C_MOUNT 0xFF3A5A46
/* ══ 网格装配(与前三个 3D 例子同一套写法)══ */
/* Mesh assembly (same style as the previous three 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]; }
/* 长方体(以原点为中心)—— 机身/弹丸/山都用它。 */
/* Box (centered at the origin) -- used for the fuselage / bullets / mountains. */
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 hu; float hw; float cx; float cy; float cz;
hu = ((q == 0 || q == 3) ? 1.0f : -1.0f) *
((axisU == 0) ? hx : ((axisU == 1) ? hy : hz));
hw = ((q == 0 || q == 1) ? 1.0f : -1.0f) *
((axisW == 0) ? hx : ((axisW == 1) ? hy : hz));
if (f == 0) { cx = hx; cy = hu; cz = hw; }
else if (f == 1) { cx = -hx; cy = hu; cz = hw; }
else if (f == 2) { cx = hu; cy = hy; cz = hw; }
else if (f == 3) { cx = hu; cy = -hy; cz = hw; }
else if (f == 4) { cx = hu; cy = hw; cz = hz; }
else { cx = hu; cy = hw; 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;
}
/* 大平板(地面)—— 给一个地平线,飞行姿态才有参照物。 */
/* Large slab (the ground) -- provides a horizon so the flight attitude has a reference. */
void gen_plane(float half, int segs) {
int i; int j; int k; int v;
v = 0;
for (i = 0; i <= segs; i = i + 1) {
for (j = 0; j <= segs; j = j + 1) {
float x = ((float)i / segs - 0.5f) * 2.0f * half;
float z = ((float)j / segs - 0.5f) * 2.0f * half;
mb_vertex(v, x, 0.0f, z, 0.0f, 1.0f, 0.0f);
v = v + 1;
}
}
g_vn = v;
k = 0;
for (i = 0; i < segs; 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;
}
/* ══ 状态 ═══════════════════════════════════════════════════════════════ */
/* == State == */
int vp;
int o_player;
int o_enemy[N_ENEMY];
int o_bullet[N_BULLET];
float px; float py; float pz; /* 玩家位置 */
/* Player position */
float pyaw; float ppitch; /* 朝向(度) */
/* Heading (degrees) */
int score; int shots; int fire_cd; int frame;
float ex[N_ENEMY]; float ey[N_ENEMY]; float ez[N_ENEMY];
float ea[N_ENEMY]; /* 敌机的绕行相位 */
/* Enemy orbit phase */
int e_alive[N_ENEMY];
float bxp[N_BULLET]; float byp[N_BULLET]; float bzp[N_BULLET];
float bvx[N_BULLET]; float bvy[N_BULLET]; float bvz[N_BULLET];
int b_alive[N_BULLET];
/* 前向量:**唯一真源**(位移与相机都由它推) */
/* Forward vector: **the single source of truth** (both motion and camera derive from it) */
void forward_vec(float yaw_deg, float pitch_deg, float* out) {
float y = yaw_deg * 0.017453292f;
float p = pitch_deg * 0.017453292f;
out[0] = cos(p) * sin(y);
out[1] = sin(p);
out[2] = cos(p) * cos(y);
}
void reset_game(void) {
int i;
px = 0.0f; py = 26.0f; pz = 0.0f;
pyaw = 0.0f; ppitch = 0.0f;
score = 0; shots = 0; fire_cd = 0;
for (i = 0; i < N_ENEMY; i = i + 1) {
ea[i] = 6.28318531f * i / N_ENEMY;
ey[i] = 16.0f + (float)(i % 3) * 8.0f;
e_alive[i] = 1;
}
for (i = 0; i < N_BULLET; i = i + 1) {
b_alive[i] = 0;
ui_3d_obj_set_visible(o_bullet[i], 0);
}
}
int main(void) {
int sw; int sh;
int mesh_plane; int mesh_player; int mesh_bullet; int mesh_mount;
int i;
int touch[3];
int dragging;
float fwd[3];
float camY;
float dt;
int lastMs; int nowMs; /* 墙钟步长用(见主循环顶部那段的说明) */
/* For the wall-clock step (see the note at the top of the main loop) */
ui_win_open_ex("3D 空战 Air Combat 3D", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
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;
vp = ui_3d_viewport_create(0, 0, sw, sh, 0);
if (vp <= 0) { printf("NO_3D_DEVICE=1\n"); return 0; }
ui_3d_viewport_set_bg(vp, C_BG);
ui_3d_viewport_set_fov(vp, 65.0f, 0.4f, 4000.0f);
/* ⚠ **不调 `ui_3d_viewport_fit`**:它每帧按半径重算距离,会把逐帧给的相机坐标覆盖掉。 */
/* ⚠ **Do not call `ui_3d_viewport_fit`**: it recomputes the distance from radius every frame and would overwrite the per-frame camera position. */
ui_3d_viewport_set_light(vp, 0.30f, 1.0f, 0.45f, 0xFFFFFFFF, 0.55f);
/* ── 网格 ─────────────────────────────────────────────────────────── */
/* ── Mesh ─────────────────────────────────────────────────────────── */
gen_plane(600.0f, 8);
mesh_plane = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(1.1f, 0.35f, 3.0f); /* 机身:**长轴 Z**(机头朝 +Z) */
/* Fuselage: **long axis Z** (nose faces +Z) */
mesh_player = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(0.3f, 0.3f, 0.9f);
mesh_bullet = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(1.0f, 6.0f, 1.0f); /* 山:细高的柱,给地面纵深 */
/* Mountain: a tall thin pillar, gives the ground depth */
mesh_mount = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
printf("AIR-MESH plane=%d player=%d bullet=%d mount=%d\n",
mesh_plane, mesh_player, mesh_bullet, mesh_mount);
if (mesh_plane <= 0 || mesh_player <= 0 || mesh_bullet <= 0 || mesh_mount <= 0)
{ printf("NO_MESH=1\n"); return 0; }
/* 地面 + 几座山(静态,给地平线和速度参照) */
/* Ground + a few mountains (static, provides a horizon and a speed reference) */
{
int o;
o = ui_3d_obj_create(vp, mesh_plane);
ui_3d_obj_set_pos(o, 0.0f, 0.0f, 0.0f);
ui_3d_obj_set_color(o, C_GROUND);
for (i = 0; i < 14; i = i + 1) {
float th = 6.28318531f * i / 14.0f;
int m;
float hh;
m = ui_3d_obj_create(vp, mesh_mount);
hh = 6.0f + (float)((i * 7) % 5) * 3.0f;
ui_3d_obj_set_scale(m, hh / 6.0f);
ui_3d_obj_set_pos(m, cos(th) * 120.0f, hh, sin(th) * 120.0f);
ui_3d_obj_set_color(m, C_MOUNT);
}
}
o_player = ui_3d_obj_create(vp, mesh_player);
ui_3d_obj_set_color(o_player, C_PLAYER);
for (i = 0; i < N_ENEMY; i = i + 1) {
o_enemy[i] = ui_3d_obj_create(vp, mesh_player);
ui_3d_obj_set_scale(o_enemy[i], 1.0f);
ui_3d_obj_set_color(o_enemy[i], C_ENEMY);
}
for (i = 0; i < N_BULLET; i = i + 1) {
o_bullet[i] = ui_3d_obj_create(vp, mesh_bullet);
ui_3d_obj_set_color(o_bullet[i], C_BULLET);
ui_3d_obj_set_visible(o_bullet[i], 0);
}
printf("AIR-OBJS=%d\n", 15 + 1 + N_ENEMY + N_BULLET);
reset_game();
dragging = 0;
camY = 0.0f;
frame = 0;
/* 步长**按墙钟算**(用户 2026-10-04 定的口径:**帧率不该是玩法的一部分**)。
* Step **by wall clock** (the user's 2026-10-04 ruling: **frame rate must not be part of the gameplay**).
* 老写法把它写死成 0.04(= 1/25 秒,配下方的 ui_msg_fetch_wait(25)),
* The old style hard-coded it to 0.04 (= 1/25 sec, paired with ui_msg_fetch_wait(25) below),
* 而宿主现在一帧跟着显示走 ⇒ 同一个游戏会快 2.4 倍。
* while the host now paces a frame to the display => the same game runs 2.4x too fast. */
lastMs = ui_time_get_ms();
dt = 0.04f;
while (1) {
float yawRate; float pitchRate;
int firing;
/* 这一帧的步长:距上一帧的**真实毫秒数**。夹上限是为了卡顿/切页面回来时
* This frame's step: the **real milliseconds** since the last frame. The upper clamp keeps a stutter or a return to the page
* 不要一次跳出去;下限是为了同一毫秒内连跑两帧时不出现 dt=0(静止)。
* from jumping in one go; the lower clamp keeps two frames in the same millisecond from producing dt=0 (frozen). */
nowMs = ui_time_get_ms();
dt = (nowMs - lastMs) * 0.001f;
if (dt > 0.1f) dt = 0.1f;
if (dt <= 0.0f) dt = 0.0001f;
lastMs = nowMs;
/* ── 输入 ──────────────────────────────────────────────────────── */
/* ── Input ─────────────────────────────────────────────────────────────────── */
yawRate = 0.0f; pitchRate = 0.0f;
if (ui_key_is_down(VML_KEY_LEFT)) yawRate = -1.0f;
if (ui_key_is_down(VML_KEY_RIGHT)) yawRate = 1.0f;
if (ui_key_is_down(VML_KEY_UP)) pitchRate = 1.0f;
if (ui_key_is_down(VML_KEY_DOWN)) pitchRate = -1.0f;
if (ui_key_is_down(65)) yawRate = -1.0f; /* 'A' */
if (ui_key_is_down(68)) yawRate = 1.0f; /* 'D' */
if (ui_key_is_down(87)) pitchRate = 1.0f; /* 'W' */
if (ui_key_is_down(83)) pitchRate = -1.0f; /* 'S' */
if (ui_key_is_down(VML_KEY_ESCAPE)) break;
firing = 0;
if (ui_key_is_down(VML_KEY_SPACE)) firing = 1;
if (ui_touch_read(0, touch)) {
/* 触摸:屏幕左右半边转向、上下半边俯仰;**按下即开火**(冷却由 fire_cd 管) */
/* Touch: left/right half turns, top/bottom half pitches; **press fires** (cooldown handled by fire_cd) */
if (touch[2]) {
if (touch[0] < sw / 2) yawRate = -1.0f; else yawRate = 1.0f;
if (touch[1] < sh / 2) pitchRate = 1.0f; else pitchRate = -1.0f;
firing = 1;
dragging = 1;
} else {
dragging = 0;
}
}
/* ── 玩家:转向 + 前进(前向量只算一次,位移与相机共用)─────── */
/* ── Player: turn + advance (forward vector computed once, shared by motion and camera) ─────── */
pyaw = pyaw + yawRate * 62.0f * dt;
if (pyaw > 180.0f) pyaw = pyaw - 360.0f;
if (pyaw < -180.0f) pyaw = pyaw + 360.0f;
ppitch = ppitch + pitchRate * 48.0f * dt;
if (ppitch > 62.0f) ppitch = 62.0f;
if (ppitch < -62.0f) ppitch = -62.0f;
forward_vec(pyaw, ppitch, fwd);
px = px + fwd[0] * SPEED * dt;
py = py + fwd[1] * SPEED * dt;
pz = pz + fwd[2] * SPEED * dt;
/* 别钻到地下、也别飞出天外(低空掠过地面最有速度感,但要兜住) */
/* Don't burrow underground or fly off the top (skimming the ground feels fastest, but keep it bounded) */
if (py < 4.0f) { py = 4.0f; if (ppitch < 0.0f) ppitch = 0.0f; }
if (py > 120.0f) { py = 120.0f; if (ppitch > 0.0f) ppitch = 0.0f; }
/* ⚠ X 取负:正 ppitch = 抬头,而绕 X 的正向旋转把 +Z 转向 -Y(低头) */
/* ⚠ Negate X: positive ppitch = nose-up, while a positive rotation about X turns +Z toward -Y (nose-down) */
ui_3d_obj_set_pos(o_player, px, py, pz);
ui_3d_obj_set_rot(o_player, -ppitch, pyaw, 0.0f);
/* ── 开火:对象池里找一个空闲的 ────────────────────────────────── */
/* ── Fire: find a free slot in the object pool ────────────────────────────────── */
if (fire_cd > 0) fire_cd = fire_cd - 1;
if (firing && fire_cd == 0) {
int b;
for (b = 0; b < N_BULLET; b = b + 1) {
if (b_alive[b] == 0) break;
}
if (b < N_BULLET) {
b_alive[b] = 1;
bxp[b] = px + fwd[0] * 3.5f;
byp[b] = py + fwd[1] * 3.5f;
bzp[b] = pz + fwd[2] * 3.5f;
bvx[b] = fwd[0] * BULLET_SP;
bvy[b] = fwd[1] * BULLET_SP;
bvz[b] = fwd[2] * BULLET_SP;
ui_3d_obj_set_pos(o_bullet[b], bxp[b], byp[b], bzp[b]);
ui_3d_obj_set_visible(o_bullet[b], 1);
shots = shots + 1;
fire_cd = FIRE_CD;
}
}
/* ── 子弹:前进 + 到期回收(**对象池**:不建不删,只切可见性)──── */
/* ── Bullets: advance + recycle on expiry (**object pool**: never create/delete, just toggle visibility) ──── */
for (i = 0; i < N_BULLET; i = i + 1) {
if (b_alive[i] == 0) continue;
bxp[i] = bxp[i] + bvx[i] * dt;
byp[i] = byp[i] + bvy[i] * dt;
bzp[i] = bzp[i] + bvz[i] * dt;
if (byp[i] < 0.5f || byp[i] > 400.0f) { b_alive[i] = 0; ui_3d_obj_set_visible(o_bullet[i], 0); continue; }
/* 飞太远也收回(不然池子会被"打向天空"的弹占满) */
/* Also recycle when they fly too far (else the pool fills up with shots fired "into the sky") */
{
float ddx = bxp[i] - px; float ddy = byp[i] - py; float ddz = bzp[i] - pz;
if (ddx * ddx + ddy * ddy + ddz * ddz > 90000.0f)
{ b_alive[i] = 0; ui_3d_obj_set_visible(o_bullet[i], 0); continue; }
}
ui_3d_obj_set_pos(o_bullet[i], bxp[i], byp[i], bzp[i]);
}
/* ── 敌机:绕圈巡逻(声明式位置由这里算,不用 `ui_3d_obj_set_orbit`——
/* ── Enemies: circle patrol (declarative position computed here, not via `ui_3d_obj_set_orbit` --
* 因为还要按朝向转机身,而 `obj_orbit` 只管位置)───────────────
* because we also rotate the body to face the heading, while `obj_orbit` only handles position) ── */
for (i = 0; i < N_ENEMY; i = i + 1) {
float r;
ea[i] = ea[i] + 0.35f * dt;
r = 46.0f + (float)(i % 3) * 15.0f;
ex[i] = cos(ea[i]) * r;
ez[i] = sin(ea[i]) * r;
e_alive[i] = 1;
ui_3d_obj_set_pos(o_enemy[i], ex[i], ey[i], ez[i]);
/* 机头朝切线:atan2 求航向(度)。`obj_orbit` 做不到"朝向也转" */
/* Nose along the tangent: atan2 gives the heading (degrees). `obj_orbit` cannot "turn the facing too" */
ui_3d_obj_set_rot(o_enemy[i], 0.0f, -(ea[i] * 57.29577951f) + 90.0f, 0.0f);
ui_3d_obj_set_visible(o_enemy[i], 1);
}
/* ── 命中:子弹 vs 敌机(三维距离),打中就收弹 + 得分 ─────────── */
/* ── Hit: bullet vs enemy (3D distance); a hit recycles the bullet + scores ─────────── */
for (i = 0; i < N_BULLET; i = i + 1) {
int k;
if (b_alive[i] == 0) continue;
for (k = 0; k < N_ENEMY; k = k + 1) {
float ddx = bxp[i] - ex[k];
float ddy = byp[i] - ey[k];
float ddz = bzp[i] - ez[k];
if (ddx * ddx + ddy * ddy + ddz * ddz < HIT_R * HIT_R) {
b_alive[i] = 0;
ui_3d_obj_set_visible(o_bullet[i], 0);
score = score + 1;
/* 敌机换个高度/半径重生(不复用同一处,免得一炮扫一排) */
/* Enemy respawns at a different height/radius (not the same spot, so one shot doesn't wipe a row) */
ey[k] = 14.0f + (float)((score + k) % 4) * 9.0f;
ea[k] = ea[k] + 2.1f;
break;
}
}
}
/* ── 跟随相机:eye 由**前向量**推(不是只在后面平移)───────────── */
/* ── Follow camera: eye derived from the **forward vector** (not just translated behind) ───────────── */
{
float ex2 = px - fwd[0] * 15.0f;
float ey2 = py - fwd[1] * 15.0f + 4.6f;
float ez2 = pz - fwd[2] * 15.0f;
float tx2 = px + fwd[0] * 26.0f;
float ty2 = py + fwd[1] * 26.0f;
float tz2 = pz + fwd[2] * 26.0f;
if (ey2 < 2.0f) ey2 = 2.0f;
ui_3d_viewport_set_camera(vp, ex2, ey2, ez2, tx2, ty2, tz2);
}
/* ── HUD ──────────────────────────────────────────────────────── */
ui_2d_clear(0);
ui_2d_text(12, 10, "3D 空战 Air Combat 3D", -1, 15, VML_ANCHOR_LEFT);
{
char buf[80];
sprintf(buf, "得分 %d 高度 %.0f 航向 %.0f 弹药 %d", score, py, pyaw, shots);
ui_2d_text(12, 30, buf, -8355712, 12, VML_ANCHOR_LEFT);
}
ui_2d_text(12, 48, "←/→ 转向 ↑/↓ 俯仰 空格/点屏 开火", -8355712, 11, VML_ANCHOR_LEFT);
/* 准星:屏幕正中(相机永远看向机头前方远点 ⇒ 准星就在中心) */
/* Crosshair: dead center of the screen (the camera always looks at a far point ahead of the nose => it lands in the middle) */
{
int cx; int cy;
cx = sw / 2; cy = sh / 2;
ui_2d_text(cx - 6, cy - 10, "+", -65536, 18, VML_ANCHOR_LEFT);
}
ui_2d_present();
ui_msg_fetch_wait(25); /* 只是「等输入最多等一会儿」(2026-10-04 起**上屏节奏由宿主管**:`ui_2d_present()` 之后宿主按显示预算走,并把这个超时夹到「距下一帧还有多久」) */
/* Just "wait for input, at most a while" (since 2026-10-04 **the host owns on-screen pacing**: after `ui_2d_present()` the host follows 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("AIR-DONE frames=%d score=%d shots=%d\n", frame, score, shots);
ui_3d_mesh_free(mesh_plane);
ui_3d_mesh_free(mesh_player);
ui_3d_mesh_free(mesh_bullet);
ui_3d_mesh_free(mesh_mount);
ui_3d_viewport_free(vp);
return 0;
}