csharp/app/breakout.cs · 32144 字节 · 下载原文
// 打砖块 —— 用 **C#** 写的手机游戏(不是 C)
// Breakout — a phone game written in **C#** (not C)
//
// 玩法:挡板在底下,球在场上弹。**拖动手指**(挡板跟着手指走)或方向键左右移动挡板,
// Gameplay: the paddle sits at the bottom and the ball bounces around the field. **Drag your
// 点一下(或空格/回车)发球。球打光一整片砖就过关(挡板与球都提速、砖块加多一行),
// finger** (the paddle follows it) or use the left/right arrows to move the paddle; tap (or
// 三条命打完就结束。球碰到挡板的哪个位置决定反弹角度 —— 打中间弹得直、打边缘弹得斜。
// Space/Enter) to serve. Clear every brick to advance a level (paddle and ball speed up, one
// more row of bricks); three lives, then it is over. Where the ball hits the paddle sets the
// bounce angle — the middle sends it straight back, the edges send it sideways.
//
// ## 为什么写得这么"平"
// ## Why it is written so "flat"
//
// C# 前端只支持一个子集(`Examples/csharp/g2048.cs` 的文件头列了完整名单),这里沿用同一套
// The C# front end supports only a subset (the full list is in `Examples/csharp/g2048.cs`'s header);
// 已被验证过的写法:一个类 + `static` 字段/方法 + 定长数组 + `while`/`if`/`switch`。
// this file sticks to the same verified constructs: one class, `static` fields/methods, fixed-length
// 全篇**整数**物理(1 像素/拍),不碰浮点 —— 与本仓其它例程同一取舍。
// arrays and `while`/`if`/`switch`. The physics is **all integers** (1 pixel per tick), no floating
// point — the same trade-off as the other examples in this repo.
//
// ## 音效按「一次事件一个音」
// ## Sound: one beep per event
//
// `ui_audio_beep` 是**单通道**的(后一个音掐掉前一个),所以这里给每种事件挑一个**不同音高**、
// `ui_audio_beep` is **single-channel** (a new tone cuts the previous one), so every event gets its own
// 而且一拍里最多发一个:砖块 880 / 挡板 440 / 丢命 131 / 过关 1047 / 结束 98。
// pitch and at most one beep per tick: brick 880 / paddle 440 / lost life 131 / level clear 1047 /
// game over 98.
//
// ## 每帧图元数
// ## Primitives per frame
//
// 砖块 42 + 墙 4 + 挡板 2 + 球 2 + HUD ≈ **60**,远在 400 之下。数值由**宿主自己**报
// 42 bricks + 4 walls + 2 paddle + 2 ball + HUD ≈ **60**, far below 400. The number comes from the
// (`ui_res_get_count(0)`,每个新高峰打印一行 `PRIMAX n`,见 `draw()`)。
// **host itself** (`ui_res_get_count(0)`; every new high water mark prints a `PRIMAX n` line — see `draw()`).
//
// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。
// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.
// 显式引用 UI 库:`ui_*` 的声明在 `Lib/csharp/VmlUi.cs`(tools/GenLib 生成,别手改)。
// Explicit reference to the UI library: `ui_*` is declared in `Lib/csharp/VmlUi.cs` (generated by tools/GenLib; don't hand-edit).
using Vml.Ui;
class Breakout
{
// ── 消息类型(源:Lib/c/vml_ui.h)──
// ── Message types (source: Lib/c/vml_ui.h) ──
const int MSG_NONE = 0;
const int MSG_KEYDOWN = 1;
const int MSG_TOUCHDOWN = 6;
const int MSG_TOUCHMOVE = 7;
const int MSG_TOUCHUP = 8;
const int MSG_TIMER = 9;
const int MSG_WINDOWCLOSE = 10;
const int MSG_WINDOWRESIZE = 11;
// ── 键码 ──
// ── Key codes ──
const int KEY_ENTER = 13;
const int KEY_ESCAPE = 27;
const int KEY_SPACE = 32;
const int KEY_LEFT = 37;
const int KEY_UP = 38;
const int KEY_RIGHT = 39;
const int KEY_DOWN = 40;
const int KEY_PAD_A = 65;
// ── 锚点 / 样式 / 竖对齐 ──
// ── Anchors / styles / vertical alignment ──
const int ANCHOR_LEFT = 0;
const int ANCHOR_CENTER = 1;
const int ANCHOR_RIGHT = 2;
const int FONT_BOLD = 1;
const int VANCHOR_MIDDLE = 1;
// ── 语言 / 对话框 ──
// ── Language / dialog ──
const int LANG_ZH = 0;
const int DLG_INFO = 0;
// ── 窗口声明(开窗之前生效)──
// ── Window declarations (take effect before the window opens) ──
const int WIN_PORTRAIT = 0;
const int WIN_NO_GAMEPAD = 0;
// ── 规则常量 ──
// ── Rule constants ──
const int COLS = 7;
const int MAXB = 42; // 6 行 × 7 列的上限
// upper bound: 6 rows x 7 columns
const int ROWS_MAX = 6;
const int START_LIVES = 3;
const int TICK_MS = 33; // ≈30fps
const int BALL_R = 6;
const int PADDLE_H = 12;
// ── 颜色(0xAARRGGBB 的十进制负数形式)──
// ── Colors (the decimal-negative form of 0xAARRGGBB) ──
const int BG = -15724520; // 0xFF101018 底色
// 0xFF101018 background
const int WALL = -15263713; // 0xFF17181F 墙
// 0xFF17181F wall
const int PADDLE_C = -11409298; // 0xFF51E86E 挡板
// 0xFF51E86E paddle
const int BALL_C = -1; // 0xFFFFFFFF 球
// 0xFFFFFFFF ball
const int HUD_C = -6643536; // 0xFF9AA0B0 次要文字
// 0xFF9AA0B0 secondary text
const int WHITE = -1;
const int ACCENT = -11409298; // 0xFF51E86E
const int DARKTXT = -15724520;
// 砖块按行分色(越靠下越硬、颜色越暖)
// Bricks are colored by row (lower rows are tougher and warmer)
const int B0 = -11890471; // 0xFF4A90D9
const int B1 = -6962941; // 0xFF95A5B3
const int B2 = -2509750; // 0xFFD9B44A
const int B3 = -2069424; // 0xFFE06C50
const int B4 = -2523025; // 0xFFD9534F
const int B5 = -7623434; // 0xFF8B5CF6
// ── 状态 ──
// ── State ──
static int[] bAlive = new int[MAXB];
static int[] bHp = new int[MAXB];
static int sw;
static int sh;
static int lang;
static int score;
static int best;
static int lives;
static int level;
static int rows; // 本关砖块行数
// brick rows in this level
static int left; // 还剩几块砖
// bricks still standing
static int state; // 0=待发球 1=进行中 2=过关横幅 3=结束
// 0=awaiting serve 1=playing 2=level-cleared banner 3=over
static int banner; // 横幅还剩几拍
// how many ticks the banner still lasts
static int frames;
static int acc; // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径
// Motion wall-clock debt in ms — steps are caught up from the wall clock (user's 2026-10-04 rule)
static int lastMs; // 上次补步取的墙钟时刻(进主循环时初始化)
// wall-clock moment of the last catch-up (initialized on entering the main loop)
// ── 几何(开窗时算一次;画与命中判定共用同一套数)──
// ── Geometry (computed once at open; drawing and hit-testing share the same numbers) ──
static int fl;
static int fr;
static int ft;
static int fb;
static int bw;
static int bh;
static int btop;
static int pw;
static int px;
static int py;
// ── 球(整数像素/拍)──
// ── Ball (integer pixels per tick) ──
static int bx;
static int by;
static int vx;
static int vy;
// ── 触摸 ──
// ── Touch ──
// ⚠ 挡板跟手用的是**轮询**(`ui_touch_fetch/ui_touch_get_x`),不是事件。
// ⚠ The paddle follows the finger by **polling** (`ui_touch_fetch/ui_touch_get_x`), not by events.
// 理由:拖动是"连续量、只关心最新位置",而事件队列会积压(实测一次拖动能涨到上千条,
// Reason: a drag is a continuous quantity and only its newest position matters, while the event
// 触摸于是要等半天才反应)。轮询读的是**宿主此刻的手指状态**,积压与否都不影响。
// queue piles up (measured: one drag can grow it to four figures, after which touch reacts
// sluggishly). Polling reads the host's **current** finger state and is immune to that.
// 事件只用来**发起球**(那是离散语义,一次点击算一次)。
// Events are used only to **serve** (a discrete action: one tap counts once).
static int lastPx;
// ── 事件文本(**每回合只拼一次**,不在逐帧路径上)──
// ── Event text (**built once per event**, never on the per-frame path) ──
// ⚠ 字符串的 `+` 是**真拼接**(v0.96.592 修的,用的是共享库的 strcat),但它**每次都
// ⚠ The string `+` really is a **concatenation** now (fixed in v0.96.592, built on the shared
// 要 alloc 一块**不回收**的内存** ⇒ 放进 `draw()` 里就是每帧漏几块。这里的规矩是:
// library's strcat), but **every one of them allocs a block that is never freed** ⇒ putting
// **逐帧要显示的数走 `drawNum`(逐位画、零分配),要拼的话就在事件里拼一次存进字段**。
// it inside `draw()` would leak a few blocks per frame. The rule here: **numbers drawn every
// frame go through `drawNum` (digit by digit, zero allocation); anything that needs
// concatenation is built once, in the event, and kept in a field.**
static string bannerText;
static string overText;
static string liveText;
static int primMax;
static string T(string zh, string en)
{
if (lang == LANG_ZH) return zh;
return en;
}
static string digitText(int d)
{
if (d == 0) return "0";
if (d == 1) return "1";
if (d == 2) return "2";
if (d == 3) return "3";
if (d == 4) return "4";
if (d == 5) return "5";
if (d == 6) return "6";
if (d == 7) return "7";
if (d == 8) return "8";
return "9";
}
static int numDigits(int v)
{
if (v < 10) return 1;
if (v < 100) return 2;
if (v < 1000) return 3;
if (v < 10000) return 4;
if (v < 100000) return 5;
return 6;
}
static int pow10(int n)
{
if (n <= 0) return 1;
if (n == 1) return 10;
if (n == 2) return 100;
if (n == 3) return 1000;
if (n == 4) return 10000;
return 100000;
}
// 逐位画一个数(不拼字符串、不分配内存)。anchor:0=左 1=中 2=右
// Draw a number digit by digit (no string building, no allocation). anchor: 0=left 1=centre 2=right
static void drawNum(int x, int y, int v, int color, int size, int anchor)
{
int n = numDigits(v);
int dw = size * 14 / 25; // 字宽实测值,见 g2048.cs 的文件头
// measured glyph width, see the file header of g2048.cs
int w = n * dw;
int startX = x;
if (anchor == ANCHOR_CENTER) startX = x - w / 2;
if (anchor == ANCHOR_RIGHT) startX = x - w;
int q = v;
if (q < 0) q = 0;
int i = 0;
while (i < n)
{
int d = (q / pow10(n - 1 - i)) % 10;
ui_2d_text(startX + i * dw, y, digitText(d), color, size, ANCHOR_LEFT);
i = i + 1;
}
}
static int brickColor(int r)
{
if (r == 0) return B0;
if (r == 1) return B1;
if (r == 2) return B2;
if (r == 3) return B3;
if (r == 4) return B4;
return B5;
}
static int brickX(int i)
{
return fl + (i % COLS) * bw;
}
static int brickY(int i)
{
return btop + (i / COLS) * bh;
}
// ════════════════════════════════════════════════════════════════════
// 版面
// Layout
// ════════════════════════════════════════════════════════════════════
static void layout()
{
int pad = sw / 24;
if (pad < 6) pad = 6;
fl = pad;
fr = sw - pad;
ft = 78;
fb = sh - 74;
bw = (fr - fl) / COLS;
bh = 20;
btop = ft + 10;
pw = sw / 4;
if (pw < 64) pw = 64;
if (pw > fr - fl) pw = fr - fl;
py = fb - 26;
if (px < fl) px = fl;
if (px + pw > fr) px = fr - pw;
}
// ════════════════════════════════════════════════════════════════════
// 关卡
// Levels
// ════════════════════════════════════════════════════════════════════
static void buildLevel()
{
rows = 3 + level;
if (rows > ROWS_MAX) rows = ROWS_MAX;
int n = rows * COLS;
int i = 0;
while (i < MAXB)
{
bAlive[i] = 0;
bHp[i] = 0;
i = i + 1;
}
i = 0;
while (i < n)
{
bAlive[i] = 1;
// 最下面一行要打两下(越往下越硬)
// The bottom row takes two hits (lower rows are tougher)
if (i / COLS == rows - 1 && level > 0) bHp[i] = 2;
else bHp[i] = 1;
i = i + 1;
}
left = n;
}
static int ballSpeed()
{
int s = 6 + level;
if (s > 12) s = 12;
return s;
}
static void serve()
{
state = 0;
px = (fl + fr) / 2 - pw / 2;
bx = px + pw / 2;
by = py - BALL_R - 1;
vx = 3;
vy = 0 - ballSpeed();
println_str("SERVE");
}
static void launch()
{
if (state != 0) return;
state = 1;
ui_audio_beep(660, 60);
println_str("LAUNCH");
}
static void newGame()
{
score = 0;
lives = START_LIVES;
level = 0;
banner = 0;
bannerText = "";
buildLevel();
serve();
println_str("NEWGAME");
}
static void levelClear()
{
level = level + 1;
buildLevel();
// 过关横幅**在这里拼一次**(不在 draw 里拼 —— 那样每帧都要 alloc)
// The level banner is **built here, once** (not inside draw — that would alloc every frame)
bannerText = T("过关!第 ", "LEVEL CLEAR! Lv ") + (level + 1);
banner = 34;
state = 2;
ui_audio_beep(1047, 200);
println_str("LEVELCLEAR");
print_str("LEVEL ");
println_int(level + 1);
}
static void loseLife()
{
lives = lives - 1;
print_str("LIFE ");
println_int(lives);
ui_audio_beep(131, 320);
if (lives <= 0)
{
state = 3;
// 结束语也**拼一次**存起来
// The closing line is likewise built once and kept
overText = T("三条命打完了 · 得分 ", "Out of lives · score ") + score;
println_str("OVER");
draw();
int a = ui_dlg_msg(T("打砖块", "Breakout"),
T("三条命打完了。\n再来一局?", "Out of lives.\nPlay again?"),
DLG_INFO);
if (a != 0) { ui_win_close(); }
newGame();
return;
}
serve();
}
// ════════════════════════════════════════════════════════════════════
// 绘制
// Drawing
// ════════════════════════════════════════════════════════════════════
static void draw()
{
ui_2d_clear(BG);
// ── 场地边框(三条墙 + 底边)──
// ── Field frame (three walls + the bottom edge) ──
ui_2d_rect(fl - 4, ft - 4, (fr - fl) + 8, 4, WALL, 1, 0, 0);
ui_2d_rect(fl - 4, ft - 4, 4, (fb - ft) + 8, WALL, 1, 0, 0);
ui_2d_rect(fr, ft - 4, 4, (fb - ft) + 8, WALL, 1, 0, 0);
ui_2d_rect(fl - 4, fb, (fr - fl) + 8, 4, WALL, 1, 0, 0);
// ── 砖块 ──
// ── Bricks ──
int i = 0;
while (i < rows * COLS)
{
if (bAlive[i] != 0)
{
int c = brickColor(i / COLS);
ui_2d_rect(brickX(i) + 1, brickY(i) + 1, bw - 3, bh - 3, c, 1, 0, 3);
if (bHp[i] > 1) ui_2d_rect(brickX(i) + 5, brickY(i) + bh / 2 - 1, bw - 11, 3, WHITE, 1, 0, 0);
}
i = i + 1;
}
// ── 挡板 + 球 ──
// ── Paddle + ball ──
ui_2d_rect(px, py, pw, PADDLE_H, PADDLE_C, 1, 0, 5);
ui_2d_rect(px + 3, py + 3, pw - 6, 3, WHITE, 1, 0, 2);
ui_2d_circle(bx, by, BALL_R, BALL_C, 1, 0);
ui_2d_circle(bx, by, BALL_R + 3, ACCENT, 0, 1);
// ── HUD ──
// ── HUD ──
ui_2d_text(fl, 26, T("打砖块", "Breakout"), WHITE, 17, ANCHOR_LEFT);
ui_2d_text(fl, 56, T("得分", "Score"), HUD_C, 11, ANCHOR_LEFT);
drawNum(fl + 40, 56, score, ACCENT, 15, ANCHOR_LEFT);
ui_2d_text(sw / 2 + 10, 56, T("关卡", "Level"), HUD_C, 11, ANCHOR_LEFT);
drawNum(sw / 2 + 50, 56, level + 1, WHITE, 15, ANCHOR_LEFT);
// 「命」这两个字摆在**那排圆点的左边**(右对齐在圆点左边一点)——
// The two "lives" glyphs sit **to the left of the dot row** (right-aligned just left of the dots) -
// 直接右对齐到 `fr` 会与圆点叠在一起(实测第一版就是这样)。
// right-aligning straight to `fr` would overlap the dots (that is what the first version did).
int h = 0;
ui_2d_text(fr - lives * 16 - 6, 56, T("命", "Lives"), HUD_C, 11, ANCHOR_RIGHT);
while (h < lives)
{
ui_2d_circle(fr - 12 - h * 16, 51, 5, ACCENT, 1, 0);
h = h + 1;
}
// ── 底部提示 / 结束语 ──
// ── Bottom hint / closing line ──
if (state == 3)
{
ui_2d_text(sw / 2, sh - 46, overText, WHITE, 13, ANCHOR_CENTER);
}
else if (state == 0)
{
ui_2d_text(sw / 2, sh - 46, T("拖动屏幕移动挡板,点一下发球", "Drag to move the paddle, tap to serve"),
ACCENT, 13, ANCHOR_CENTER);
}
else
{
ui_2d_text(sw / 2, sh - 46, T("拖动屏幕(或方向键)移动挡板", "Drag (or use the arrows) to move the paddle"),
HUD_C, 12, ANCHOR_CENTER);
}
// ── 过关横幅 ──
// ── Level-clear banner ──
if (banner > 0)
{
int bwd = fr - fl - 30;
ui_2d_rect(fl + 15, fb / 2 - 34, bwd, 68, -16777216, 1, 0, 12);
ui_2d_text_v(sw / 2, fb / 2, bannerText, ACCENT, 18, ANCHOR_CENTER, VANCHOR_MIDDLE, FONT_BOLD);
}
// 每帧图元数(宿主侧真值)—— **只在新高时打印**,被超时杀掉的运行也留得下数
// Primitives per frame (the host's own count) — printed **only when it grows**, so a run
// killed by the timeout still leaves the number behind
int np = ui_res_get_count(0);
if (np > primMax)
{
primMax = np;
print_str("PRIMAX ");
println_int(primMax);
}
ui_2d_present();
}
// ════════════════════════════════════════════════════════════════════
// 物理(整数像素/拍)
// Physics (integer pixels per tick)
// ════════════════════════════════════════════════════════════════════
static void movePaddle(int x)
{
int nx = x - pw / 2;
if (nx < fl) nx = fl;
if (nx + pw > fr) nx = fr - pw;
if (nx != px)
{
// `Math.Abs` **走的是前端新增的“标准方法 → 库标签”映射**(共享库的 `abs`,
// `Math.Abs` **goes through the frontend's new "standard method -> library label" mapping** (the shared `abs`,
// 与手写 `if (d < 0) d = 0 - d;` 等价 —— v0.96.592 修)。
// equivalent to a hand-written `if (d < 0) d = 0 - d;` - fixed in v0.96.592).
int d = Math.Abs(nx - px);
px = nx;
if (d >= 12)
{
// 跟随日志(只在真的动了一截时打,免得刷屏)
// Follow log (printed only on a real move, to avoid flooding)
print_str("PADDLE ");
println_int(px);
lastPx = px;
}
}
if (state == 0)
{
bx = px + pw / 2;
by = py - BALL_R - 1;
}
}
static void tick()
{
frames = frames + 1;
if (banner > 0) banner = banner - 1;
if (state == 2 && banner == 0)
{
state = 0;
serve();
}
// 触摸:**轮询**手指此刻在哪(拖动只关心最新位置,见 `lastPx` 上面的说明)
// Touch: **poll** where the finger is right now (a drag only cares about the newest position,
// see the note above `lastPx`)
if (ui_touch_fetch(0) != 0 && ui_touch_is_down() != 0) movePaddle(ui_touch_get_x());
// 键盘兜底:按住方向键持续移动(`ui_key_is_down` 是"此刻按着没")
// Keyboard fallback: hold an arrow to keep moving (`ui_key_is_down` is "is it held right now")
if (ui_key_is_down(KEY_LEFT) != 0) movePaddle(px - 9 + pw / 2);
if (ui_key_is_down(KEY_RIGHT) != 0) movePaddle(px + 9 + pw / 2);
if (state != 1) return;
bx = bx + vx;
by = by + vy;
// 左右墙
// Side walls
if (bx - BALL_R < fl)
{
bx = fl + BALL_R;
vx = 0 - vx;
ui_audio_beep(330, 30);
println_str("WALLX");
}
if (bx + BALL_R > fr)
{
bx = fr - BALL_R;
vx = 0 - vx;
ui_audio_beep(330, 30);
println_str("WALLX");
}
// 顶
// Top
if (by - BALL_R < ft)
{
by = ft + BALL_R;
vy = 0 - vy;
ui_audio_beep(330, 30);
println_str("WALLY");
}
// ── 挡板 ──
// ── Paddle ──
if (vy > 0 && by + BALL_R >= py && by - BALL_R <= py + PADDLE_H &&
bx + BALL_R >= px && bx - BALL_R <= px + pw)
{
by = py - BALL_R;
// 打在挡板哪一段决定反弹的横速度:中间直、边缘斜
// Where the ball lands on the paddle sets its horizontal speed: middle straight, edges angled
int mid = px + pw / 2;
int off = bx - mid;
int span = pw / 2;
if (span < 1) span = 1;
int nvx = off * 5 / span; // −5..+5
if (nvx > 4) nvx = 4;
if (nvx < -4) nvx = -4;
if (nvx == 0) nvx = 1; // 永不垂直(垂直会变成死循环弹不出去)
// never vertical (vertical becomes an endless bounce and can never escape)
vx = nvx;
vy = 0 - ballSpeed();
ui_audio_beep(440, 45);
print_str("PADDLEHIT ");
println_int(off);
}
// ── 砖块 ──
// ── Bricks ──
int i = 0;
while (i < rows * COLS)
{
if (bAlive[i] != 0)
{
int rx = brickX(i);
int ry = brickY(i);
if (bx + BALL_R > rx && bx - BALL_R < rx + bw - 2 &&
by + BALL_R > ry && by - BALL_R < ry + bh - 2)
{
// 穿入较浅的那个轴才是要反射的轴
// The axis with the shallower penetration is the one to reflect
int ox = (rx + bw - 2) - (bx - BALL_R);
int t2 = (bx + BALL_R) - rx;
if (t2 < ox) ox = t2;
int oy = (ry + bh - 2) - (by - BALL_R);
int t3 = (by + BALL_R) - ry;
if (t3 < oy) oy = t3;
if (ox < oy) { vx = 0 - vx; bx = bx + vx; }
else { vy = 0 - vy; by = by + vy; }
bHp[i] = bHp[i] - 1;
if (bHp[i] <= 0)
{
bAlive[i] = 0;
left = left - 1;
}
score = score + 10;
if (score > best) best = score;
ui_audio_beep(880, 35);
print_str("BRICK ");
print_int(i);
print_str(" LEFT ");
println_int(left);
if (left == 0) { levelClear(); return; }
return; // 一拍最多处理一块砖(防一次穿多块)
// one brick per tick at most (stops the ball passing through several)
}
}
i = i + 1;
}
// 掉底
// Fell past the bottom
if (by - BALL_R > fb)
{
loseLife();
}
}
// ════════════════════════════════════════════════════════════════════
// 入口
// Entry point
// ════════════════════════════════════════════════════════════════════
static void Main()
{
lang = ui_scr_get_language();
// 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)
// Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)
ui_win_open_ex(T("打砖块", "Breakout"), 0, 0, WIN_PORTRAIT, WIN_NO_GAMEPAD);
ui_scr_set_keep_on(1);
// 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)
// Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)
sw = ui_scr_get_w();
sh = ui_scr_get_h();
if (sw <= 0) sw = 411;
if (sh <= 0) sh = 726;
layout();
newGame();
draw();
int tid = ui_timer_set(TICK_MS, 0);
acc = 0;
lastMs = ui_time_get_ms(); // 墙钟起点:进主循环时取(不在模块级/全局初始化里调 syscall)
// Wall-clock origin: taken when entering the main loop (no syscall at module level or in a global initializer)
while (ui_win_is_closed() == 0)
{
int t = ui_msg_fetch_wait(TICK_MS);
// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
// How many steps this frame: **by wall-clock debt** (timer resolution differs per platform, so
// 用户 2026-10-04 口径:所有游戏时钟取系统毫秒数。
// "how many ticks arrived" is not a measure of time).
// User rule of 2026-10-04: every game clock reads system milliseconds.
int now2 = ui_time_get_ms();
acc = acc + (now2 - lastMs);
lastMs = now2;
if (acc > TICK_MS * 8) acc = 0; // 卡太久别一次补一大串(防瞬移)
// Do not catch up in one huge burst after a long stall (anti-teleport)
if (acc >= TICK_MS)
{
while (acc >= TICK_MS) { acc = acc - TICK_MS; tick(); }
draw();
}
if (t == MSG_NONE) continue;
if (t == MSG_WINDOWCLOSE) break;
if (t == MSG_WINDOWRESIZE)
{
sw = ui_msg_get_a();
sh = ui_msg_get_b();
if (sw <= 0) sw = 411;
if (sh <= 0) sh = 726;
layout();
draw();
continue;
}
if (t == MSG_TIMER)
{
// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)
// The timer **only wakes us up** — the advance happens above, by wall-clock debt
continue;
}
if (t == MSG_TOUCHDOWN)
{
launch();
draw();
continue;
}
if (t == MSG_TOUCHMOVE)
{
// 位置靠轮询拿(见 `lastPx` 上面的说明),这类消息本身没有价值
// the position is polled instead (see the note above `lastPx`); this kind of message carries no value in itself
// ⇒ 直接丢掉,免得队列只涨不落(`ui_msg_drop(1)` = 丢掉"触摸"这一类)
// The position comes from polling (see the note above `lastPx`), so these messages carry
// nothing ⇒ drop them, so the queue cannot grow without bound (`ui_msg_drop(1)` drops
// the "touch" kind)
ui_msg_drop(1);
continue;
}
if (t == MSG_TOUCHUP) continue;
if (t == MSG_KEYDOWN)
{
int k = ui_msg_get_a();
if (k == KEY_ESCAPE) break;
if (k == KEY_SPACE || k == KEY_ENTER || k == KEY_PAD_A)
{
if (state == 3) newGame();
else launch();
draw();
}
else if (k == KEY_LEFT) { movePaddle(px - 18 + pw / 2); draw(); }
else if (k == KEY_RIGHT) { movePaddle(px + 18 + pw / 2); draw(); }
continue;
}
}
ui_timer_kill(tid);
ui_scr_set_keep_on(0);
ui_win_close();
print_str("PRIMAX-FINAL ");
println_int(primMax);
println_str("breakout: done");
}
}