c/3d/tank3d.c · 24648 字节 · 下载原文
/* tank3d.c —— **3D 坦克对战**:第五个"用真游戏验接口"的探针。
* tank3d.c — **3D tank battle**: the fifth "write a real game to test the API" probe.
*
* ## 它压的是**唯一一个此前一个游戏都没用到的号**:`ui_3d_obj_set_parent`
* ## It stresses **the one syscall no earlier game touched**: `ui_3d_obj_set_parent`
*
* 前四个游戏(贪吃蛇/桌球/赛车/空战)全是**平铺**的物体 —— 每个物体的位姿都直接写在世界坐标里。
* The first four games (snake / pool / race / air combat) all used **flat** objects -- every object's pose written straight in world coordinates.
* 而坦克的经典结构是**三层父子**:
* A tank's classic structure is a **three-level parent/child chain**:
*
* 车体(位置 + 航向)
* hull (position + heading)
* └─ 炮塔(**局部**旋转:独立瞄准)
* └─ turret (**local** rotation: independent aiming)
* └─ 炮管(局部偏移 + 仰角)
* └─ barrel (local offset + elevation)
*
* 这一条逼出两个接口事实:
* That forced out two facts about the API:
*
* ① **子物体的 `ui_3d_obj_set_pos` / `ui_3d_obj_set_rot` 是"父的局部空间"里的** —— 传世界坐标进去会得到
* (1) **a child's `ui_3d_obj_set_pos` / `ui_3d_obj_set_rot` are in "the parent's local space"** -- passing world coordinates gives
* "世界坐标再叠一次父变换"的荒谬结果(看着像"物体飞了",但**不报错**)。
* the absurd "world coordinates with the parent transform applied once more" (it looks like the object flew away, yet **no error is reported**).
* ② **父变换是每帧重新合成的**(`WorldMatrix` 递归),所以父一动、子**自动跟着动**,
* (2) **the parent transform is recomposed every frame** (`WorldMatrix` recursion), so when the parent moves the children **follow automatically**,
* 不需要每帧把子物体的世界坐标重算一遍 —— 这正是 `ui_3d_obj_set_parent` 的价值所在。
* with no need to recompute the children's world coordinates each frame -- that is exactly the value of `ui_3d_obj_set_parent`.
*
* 玩法上它体现为一句话:**车体转,炮塔纹丝不动地继续瞄着目标**(因为炮塔角是局部的,
* In gameplay it comes down to one sentence: **the hull turns, the turret keeps aiming at the target without budging** (because the turret angle is local,
* 自动把车体的旋转抵消掉了)。
* it automatically cancels the hull's rotation out).
*
* ## 玩法
* ## Gameplay
*
* 车自己往前开,←/→ 转向;**炮塔自动瞄准最近的目标**,空格/点屏开火。
* The tank drives forward by itself; ←/→ steer; the **turret auto-aims at the nearest target**; Space / tap the screen fires.
* 打光所有靶子就赢。Esc 退出。
* Destroy all targets to win. Esc quits.
*
* ## 判据
* ## Criteria
*
* 机械化验证:`RUN_FRAMES` > 0 时跑满帧数并打印
* Mechanical check: with `RUN_FRAMES` > 0 it runs the full frame count and prints
* `TANK-DONE frames=… kills=… shots=… turret=…`(行首锚定即可断)。
* `TANK-DONE frames=... kills=... shots=... turret=...` (anchor on the line start to decide).
*/
#include <stdio.h>
#include <math.h>
#include <vml_3d.h>
#include <vml_ui.h>
#define RUN_FRAMES 0
#define N_TARGET 8
#define N_BULLET 16
#define SPEED 20.0f
#define TURN 1.6f /* 车体转向速率(弧度/秒) */
/* hull turn rate (radians/sec) */
#define TURRET_RATE 2.6f /* 炮塔转速(弧度/秒)—— 比车体快,才有"追着目标转"的观感 */
/* turret turn rate (radians/sec) -- faster than the hull, so it looks like it chases the target */
#define BULLET_SP 70.0f
#define FIRE_CD 14
#define HIT_R 3.0f
#define ARENA 70.0f /* 场地半径(车开出去就绕回来) */
/* arena radius (drive past it and you are pulled back) */
#define C_BG 0xFF141A12
#define C_GROUND 0xFF3E4A2E
#define C_HULL 0xFF6B7A4A
#define C_TURRET 0xFF8A9A5E
#define C_BARREL 0xFF4A5233
#define C_TARGET 0xFFC85A3A
#define C_BULLET 0xFFFFE066
/* ══ 网格装配 ══ */
/* ══ Mesh assembly ══ */
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]; }
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;
}
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_hull; int o_turret; int o_barrel;
int o_target[N_TARGET];
int o_bullet[N_BULLET];
float hull_x; float hull_z; float hull_yaw; /* 世界位姿 */
/* world pose */
float turret_local; /* **局部**偏航(相对车体) */
/* **local** yaw (relative to the hull) */
float barrel_local; /* **局部**仰角 */
/* **local** elevation */
float tx[N_TARGET]; float tz[N_TARGET]; int t_alive[N_TARGET];
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];
int kills; int shots; int fire_cd; int frame;
int lastMs; int nowMs; /* 墙钟步长(见主循环顶部) */
/* wall-clock step (see the top of the main loop) */
float dt;
float ang_norm(float a) {
while (a > 3.14159265f) a = a - 6.28318531f;
while (a < -3.14159265f) a = a + 6.28318531f;
return a;
}
void reset_game(void) {
int i;
hull_x = 0.0f; hull_z = 0.0f; hull_yaw = 0.0f;
turret_local = 0.0f; barrel_local = -6.0f;
kills = 0; shots = 0; fire_cd = 0;
for (i = 0; i < N_TARGET; i = i + 1) {
float th = 6.28318531f * i / N_TARGET;
tx[i] = cos(th) * 40.0f;
tz[i] = sin(th) * 40.0f;
t_alive[i] = 1;
ui_3d_obj_set_visible(o_target[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_ground; int mesh_hull; int mesh_turret; int mesh_barrel; int mesh_target; int mesh_bullet;
int i;
int touch[3];
float aim_world; float best; int best_i;
ui_win_open_ex("3D 坦克 Tank 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, 62.0f, 0.4f, 3000.0f);
/* ⚠ 跟随相机**不能**配 `vp_fit`(见 race3d 的说明),这里只用 `ui_3d_viewport_set_camera` */
/* ⚠ a follow camera must **not** use `vp_fit` (see the note in race3d); here only `ui_3d_viewport_set_camera` is used */
ui_3d_viewport_set_light(vp, 0.35f, 1.0f, 0.30f, 0xFFFFFFFF, 0.52f);
/* ── 网格 ─────────────────────────────────────────────────────────── */
/* ── Mesh ───────────────────────────────────────────────────────── */
gen_plane(400.0f, 6);
mesh_ground = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(1.4f, 0.5f, 2.2f); /* 车体:长轴 Z */
/* hull: long axis Z */
mesh_hull = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(1.0f, 0.45f, 1.0f); /* 炮塔 */
/* turret */
mesh_turret = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(0.18f, 0.18f, 1.5f); /* 炮管:长轴 Z,局部 +Z 指向前方 */
/* barrel: long axis Z, local +Z points forward */
mesh_barrel = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(1.2f, 1.6f, 1.2f); /* 靶子 */
/* target */
mesh_target = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
gen_box(0.25f, 0.25f, 0.7f);
mesh_bullet = ui_3d_mesh_create(mb_ptr(), g_vn, g_in);
printf("TANK-MESH ground=%d hull=%d turret=%d barrel=%d target=%d bullet=%d\n",
mesh_ground, mesh_hull, mesh_turret, mesh_barrel, mesh_target, mesh_bullet);
if (mesh_ground <= 0 || mesh_hull <= 0 || mesh_turret <= 0 || mesh_barrel <= 0
|| mesh_target <= 0 || mesh_bullet <= 0)
{ printf("NO_MESH=1\n"); return 0; }
/* 地面 */
/* Ground */
{
int o;
o = ui_3d_obj_create(vp, mesh_ground);
ui_3d_obj_set_pos(o, 0.0f, 0.0f, 0.0f);
ui_3d_obj_set_color(o, C_GROUND);
}
/* ── 三层父子:车体 → 炮塔 → 炮管 ────────────────────────────────────
/* ── Three-level parent/child: hull -> turret -> barrel ────────────────
* ⚠ 炮塔与炮管的坐标是**父的局部空间**:炮塔在车体上方 0.9、炮管在炮塔前方 1.2。
* ⚠ The turret's and barrel's coordinates are in the **parent's local space**: the turret 0.9 above the hull, the barrel 1.2 in front of the turret.
* 写世界坐标会得到"世界坐标再叠一次父变换"的结果 —— 看着像物体飞了,**但不报错**。
* Writing world coordinates gives "world coordinates with the parent transform applied once more" -- looks like the object flew away, **but no error is reported**. */
o_hull = ui_3d_obj_create(vp, mesh_hull);
ui_3d_obj_set_color(o_hull, C_HULL);
o_turret = ui_3d_obj_create(vp, mesh_turret);
ui_3d_obj_set_pos(o_turret, 0.0f, 0.9f, 0.0f); /* 局部:车体上方 */
/* local: above the hull */
ui_3d_obj_set_color(o_turret, C_TURRET);
ui_3d_obj_set_parent(o_turret, o_hull); /* ← 本文件的主角 */
/* <- the star of this file */
o_barrel = ui_3d_obj_create(vp, mesh_barrel);
ui_3d_obj_set_pos(o_barrel, 0.0f, 0.0f, 1.2f); /* 局部:炮塔前方 */
/* local: in front of the turret */
ui_3d_obj_set_color(o_barrel, C_BARREL);
ui_3d_obj_set_parent(o_barrel, o_turret); /* ← 三层 */
/* <- three levels deep */
for (i = 0; i < N_TARGET; i = i + 1) {
o_target[i] = ui_3d_obj_create(vp, mesh_target);
ui_3d_obj_set_pos(o_target[i], 0.0f, 1.6f, 0.0f);
ui_3d_obj_set_color(o_target[i], C_TARGET);
}
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("TANK-OBJS=%d\n", 1 + 3 + N_TARGET + N_BULLET);
reset_game();
frame = 0;
lastMs = ui_time_get_ms();
dt = 0.04f;
while (1) {
float steer;
int firing;
/* 步长**按墙钟算**(用户 2026-10-04:帧率不该是玩法的一部分)。
/* The step is **computed from the wall clock** (user, 2026-10-04: frame rate should not be part of the gameplay).
* 老写法把 0.04(=1/25 秒)写死,宿主改成跟着显示走之后会快 2.4 倍。
* The old form hard-coded 0.04 (= 1/25 s), which runs 2.4x too fast once the host follows the display. */
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 ───────────────────────────────────────────────────── */
steer = 0.0f;
if (ui_key_is_down(VML_KEY_LEFT)) steer = -1.0f;
if (ui_key_is_down(VML_KEY_RIGHT)) steer = 1.0f;
if (ui_key_is_down(65)) steer = -1.0f; /* 'A' */
if (ui_key_is_down(68)) steer = 1.0f; /* 'D' */
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)) {
if (touch[2]) {
if (touch[0] < sw / 2) steer = -1.0f; else steer = 1.0f;
firing = 1;
}
}
/* ── 车体:只动**世界**位姿 ───────────────────────────────────── */
/* ── Hull: moves its **world** pose only ─────────────── */
hull_yaw = hull_yaw + steer * TURN * dt;
if (hull_yaw > 3.14159265f) hull_yaw = hull_yaw - 6.28318531f;
if (hull_yaw < -3.14159265f) hull_yaw = hull_yaw + 6.28318531f;
hull_x = hull_x + sin(hull_yaw) * SPEED * dt;
hull_z = hull_z + cos(hull_yaw) * SPEED * dt;
/* 场地边界:开出去就贴边绕(**注意钳的是父**,两个子会自动跟着走) */
/* Arena boundary: drive out and you hug the edge (**note it clamps the parent**; both children follow automatically) */
{
float d = sqrt(hull_x * hull_x + hull_z * hull_z);
if (d > ARENA) { hull_x = hull_x * ARENA / d; hull_z = hull_z * ARENA / d; }
}
ui_3d_obj_set_pos(o_hull, hull_x, 0.5f, hull_z);
ui_3d_obj_set_rot(o_hull, 0.0f, hull_yaw * 57.29577951f, 0.0f);
/* ── 炮塔:**只写局部角** —— 车体怎么转,它自动把那份旋转抵消掉 ──── */
/* ── Turret: **writes only the local angle** -- it cancels the hull rotation out ── */
best = 1e9f; best_i = -1;
for (i = 0; i < N_TARGET; i = i + 1) {
float dx; float dz; float d2;
if (t_alive[i] == 0) continue;
dx = tx[i] - hull_x; dz = tz[i] - hull_z;
d2 = dx * dx + dz * dz;
if (d2 < best) { best = d2; best_i = i; }
}
if (best_i >= 0) {
float want; float diff;
/* 目标的**世界**方位角 → 减去车体航向 = 炮塔该有的**局部**角 */
/* target's **world** azimuth -> minus the hull heading = the **local** angle the turret should have */
want = atan2(tx[best_i] - hull_x, tz[best_i] - hull_z);
diff = ang_norm(want - hull_yaw);
/* 慢慢转过去(每帧最多 TURRET_RATE×dt),转到位就停 */
/* turn slowly toward it (at most TURRET_RATE x dt per frame); stop once aligned */
if (diff > TURRET_RATE * dt) diff = TURRET_RATE * dt;
if (diff < -TURRET_RATE * dt) diff = -TURRET_RATE * dt;
turret_local = ang_norm(turret_local + diff);
barrel_local = -6.0f;
}
ui_3d_obj_set_rot(o_turret, 0.0f, turret_local * 57.29577951f, 0.0f);
ui_3d_obj_set_rot(o_barrel, barrel_local, 0.0f, 0.0f);
/* ── 开火:**炮口的世界位置要自己算**(父变换不会替我们求世界坐标)──
/* ── Fire: **the muzzle's world position must be computed by hand** (the parent transform does not give us world coordinates) --
* 世界角 = 车体航向 + 炮塔局部角;再沿它推出炮口位置。
* world angle = hull heading + turret local angle; push the muzzle position out along it. */
if (fire_cd > 0) fire_cd = fire_cd - 1;
if (firing && fire_cd == 0) {
int b;
float wa; float mx; float mz;
for (b = 0; b < N_BULLET; b = b + 1) { if (b_alive[b] == 0) break; }
if (b < N_BULLET) {
wa = hull_yaw + turret_local;
mx = hull_x + sin(wa) * 3.2f;
mz = hull_z + cos(wa) * 3.2f;
b_alive[b] = 1;
bxp[b] = mx; byp[b] = 1.4f; bzp[b] = mz;
bvx[b] = sin(wa) * BULLET_SP;
bvy[b] = 0.0f;
bvz[b] = cos(wa) * 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 ────────────────────────────────────────────────── */
for (i = 0; i < N_BULLET; i = i + 1) {
if (b_alive[i] == 0) continue;
bxp[i] = bxp[i] + bvx[i] * dt;
bzp[i] = bzp[i] + bvz[i] * dt;
{
float d = sqrt(bxp[i] * bxp[i] + bzp[i] * bzp[i]);
if (d > ARENA + 8.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]);
}
/* ── 命中 ─────────────────────────────────────────────────────── */
/* ── Hits ───────────────────────────────────────────────────── */
for (i = 0; i < N_BULLET; i = i + 1) {
int k;
if (b_alive[i] == 0) continue;
for (k = 0; k < N_TARGET; k = k + 1) {
float ddx; float ddz;
if (t_alive[k] == 0) continue;
ddx = bxp[i] - tx[k];
ddz = bzp[i] - tz[k];
if (ddx * ddx + ddz * ddz < HIT_R * HIT_R) {
b_alive[i] = 0;
ui_3d_obj_set_visible(o_bullet[i], 0);
t_alive[k] = 0;
ui_3d_obj_set_visible(o_target[k], 0);
kills = kills + 1;
break;
}
}
}
/* ── 跟随相机 ─────────────────────────────────────────────────── */
/* ── Follow camera ────────────────────────────────────────── */
{
float fx = sin(hull_yaw); float fz = cos(hull_yaw);
ui_3d_viewport_set_camera(vp, hull_x - fx * 11.0f, 6.4f, hull_z - fz * 11.0f,
hull_x + fx * 14.0f, 1.2f, hull_z + fz * 14.0f);
}
/* ── HUD ──────────────────────────────────────────────────────── */
ui_2d_clear(0);
ui_2d_text(12, 10, "3D 坦克 Tank 3D", -1, 15, VML_ANCHOR_LEFT);
{
char buf[80];
sprintf(buf, "击毁 %d/%d 开火 %d 炮塔 %.0f°", kills, N_TARGET, shots,
turret_local * 57.29577951f);
ui_2d_text(12, 30, buf, -8355712, 12, VML_ANCHOR_LEFT);
}
ui_2d_text(12, 48, "←/→ 驾驶 空格/点屏 开火(炮塔自动瞄准)", -8355712, 11, VML_ANCHOR_LEFT);
if (kills >= N_TARGET) {
ui_2d_text(12, 68, "全歼!ALL CLEAR", -65281, 14, VML_ANCHOR_LEFT);
}
ui_2d_present();
ui_msg_fetch_wait(25);
frame = frame + 1;
if (RUN_FRAMES > 0) { if (frame >= RUN_FRAMES) break; }
}
printf("TANK-DONE frames=%d kills=%d shots=%d turret=%d\n",
frame, kills, shots, (int)(turret_local * 57.29577951f));
ui_3d_mesh_free(mesh_ground); ui_3d_mesh_free(mesh_hull); ui_3d_mesh_free(mesh_turret);
ui_3d_mesh_free(mesh_barrel); ui_3d_mesh_free(mesh_target); ui_3d_mesh_free(mesh_bullet);
ui_3d_viewport_free(vp);
return 0;
}