swift/app/flappy.swift · 32930 字节 · 下载原文
// 过管子(Flappy 式)—— 用 **Swift** 写的手机游戏
// Flappy-style pipe game — written in **Swift**
//
// ◆ 玩法
// ◆ Gameplay
//
// 点一下(或按空格/上键)扇翅膀上升,其余时间被重力往下拽;管子从右边成对飘过来,
// Tap (or press Space / Up) to flap upward; the rest of the time gravity pulls you down. Pipe pairs
// 从中间穿过去 +1 分,**撞管或落地就结束**;最高分**存进设备**(下次进来还在)。
// drift in from the right: pass through a gap for +1, and **hitting a pipe or the ground ends the run**.
// The best score is **persisted on the device** (it is still there next time).
//
// 竖屏自适应:地面高度、管子间距、缺口高度、鸟的大小**全部由窗口尺寸推**,没有一处写死分辨率。
// Adaptive portrait: the ground height, pipe spacing, gap size and bird size are **all derived from the
// window size** — no hardcoded resolution anywhere.
//
// 触摸为主 + 键盘兜底(空格/回车/上键扇翅膀、R 重开、ESC 退出)。
// Touch first, keyboard as backup (Space / Enter / Up to flap, R to restart, ESC to quit).
//
// ◆ 这份为什么和 `minesweeper.swift` 写法不同
// ◆ Why this file is written differently from `minesweeper.swift`
//
// 两条游戏是同一天写的,中间**修掉了 Swift 前端的几个静默缺陷**(台账见
// Both games were written the same day, and in between **several silent Swift-frontend defects were
// `third_party/vml/FRONTEND_DEFECTS.md` 的 `## Swift` 节)。这一份**用上了修好的那几条**:
// fixed** (ledger: `third_party/vml/FRONTEND_DEFECTS.md`, section `## Swift`). This file **uses the
// fixed ones**:
// · **字符串 `+` 真的能拼了** —— HUD 的「分数 / 最高」文案直接拼(见 `flHudText`)。
// · **String `+` really concatenates now** — the "score / best" HUD text is built by concatenation
// 修之前拼出来是**空串**,扫雷那边只能拆成多次 `ui_2d_text` 调用。
// (see `flHudText`). Before the fix it produced the **empty string**, which is why minesweeper has to
// split that text into several `ui_2d_text` calls.
// · **`defer` 真的会跑** —— `flNewBest` 用它在"写存档"这条路上兜一层收尾。
// · **`defer` really runs** — `flNewBest` uses it to wrap the save path.
// · **`.count` 读的是真长度**(这里只用了一处:管子槽位数)。
// · **`.count` returns the real length** (used once here: the pipe slot count).
// · **调用处可以写参数标签**(`flBox(x:y:w:h:)`)。
// · **Argument labels are allowed at call sites** (`flBox(x:y:w:h:)`).
// ✅ 2026-10-04 复验(上面四条逐条量过,都仍成立):
// ✅ Re-checked 2026-10-04 (all four measured; each still holds):
// · `+`:`flHudText()` 在 trace 里落成 `str="得分 0 最高 0"`;
// · `+` — `flHudText()` traces into the HUD text (the score/best line) as a non-empty `str=...`;
// · `defer`:`func f(){ defer{A[1]=99}; return 7 }` ⇒ 返回 7 且 `A[1]` = 99(确实跑了);
// · `defer` — `func f(){ defer{A[1]=99}; return 7 }` returns 7 with `A[1]` = 99 (it really runs);
// · `.count`:`[1,2,3,4,5].count` 实测 5;
// · `.count` — `[1,2,3,4,5].count` measures 5;
// · 参数标签:`g(x: 3, y: 4)` 实测 7。(注:下文说的 `flNewBest` 现名 `flSaveBest`,`flBox` 本文件里也不存在。)
// · argument labels — `g(x: 3, y: 4)` measures 7. (Note: `flNewBest` below is now `flSaveBest`, and `flBox` does not exist here either.)
//
// ◆ 结构约束(与本仓 Swift 前端的实测一致,别照别的语言的习惯写)
// ◆ Structural constraints (matching what the Swift frontend here actually does)
//
// ① **只用一个全局数组**(多开会互相踩内存)—— 全塞进 `A`,按下标分区。
// ① **One global array only** (several clobber each other's memory) — everything goes into `A`, by region.
// ② **函数名一律带 `fl` 前缀** —— `add`/`min`/`max`/`abs`/`pow`/`sum` 这些名字会被**链接进来的库版本
// ② **Every function carries an `fl` prefix** — names like `add`/`min`/`max`/`abs`/`pow`/`sum` are
// 静默顶掉**(实测 `add(3,4)` 得 7 而不是 304)。
// **silently overridden** by the library versions that get linked in (measured: `add(3,4)` gives 7).
// (2026-10-04 复验仍成立:`add(3,4)` = 7、改名的 `addx(3,4)` = 304;冲突源是 `Lib/shared/crosslang.vml` 的裸标签 `add:`。)
// (Re-checked 2026-10-04, still true: `add(3,4)` = 7 while a renamed `addx(3,4)` = 304; the collision is the bare label `add:` in `Lib/shared/crosslang.vml`.)
// ③ **全整数**:物理量用**定点**(`FP = 8`,即 1/8 像素),不碰 float/double ——
// ③ **Integers throughout**: physics uses **fixed point** (`FP = 8`, i.e. eighths of a pixel), never
// 这一支的浮点是另一套寄存器(F/D),而定点没有取整漂移,位置判定与绘制天然同源。
// float/double — floating point here lives in a separate register bank (F/D), while fixed point has no
// rounding drift and keeps hit-testing and drawing on the same source of truth.
//
// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。
// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.
// 状态表 —— **只用一个全局数组**,按下标分区:
// State table — **one global array only**, partitioned by index:
// ├ 0..7 管子 x(3 个槽,1/8 像素)
// ├ 0..7 pipe x (3 slots, eighths of a pixel)
// ├ 16..23 管子缺口上沿 y
// ├ 16..23 pipe gap top y
// ├ 32..39 这一对管子是否已经加过分
// ├ 32..39 whether this pair has been scored
// ├ 48..55 云 x(3 朵)
// ├ 48..55 cloud x (3)
// ├ 64..95 状态槽(见下)
// ├ 64..95 state slots (below)
// └ 128..511 空闲
// └ 128..511 free
// 64=st 65=w 66=h 67=lang 68=birdY 69=birdVy 70=birdX 71=score 72=best 73=tick
// 74=startTick 75=deadTick 76=flash 77=groundY 78=gapH 79=pipeW 80=speed 81=bob
// 82=timerId 83=readyTaps 84=scroll 85=gapMin 86=gapMax 87=spawnX 88=flaps 89=hitX
// st: 0=待开始 1=飞行中 2=坠落 3=结束
// st: 0=ready 1=flying 2=falling 3=over
#include "../../../Lib/swift/vml_ui.swift"
var A = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ── 定点 ──────────────────────────────────────────────────────
// ── Fixed point ───────────────────────────────────────────────
// 位置与速度的单位都是 **1/FP 像素**。除法只在绘制/判定那一刻做一次。
// Positions and velocities are in **1/FP pixel** units. The division happens once, at draw/test time.
func flFP() -> Int { return 8 }
// ── 界面语言(只查一次,`ui_scr_get_language` 是 syscall)──────────────────
// ── UI language (queried once; `ui_scr_get_language` is a syscall) ──────────────────
func flT(zh: String, en: String) -> String {
if A[67] == 0 { return zh }
return en
}
// ── 颜色(0xAARRGGBB 写成负数十进制)──────────────────────────────
// ── Colors (0xAARRGGBB written as negative decimals) ──────────────────────────────
func flSky() -> Int { return -15986144 } // 0xFF0C1220
func flSky2() -> Int { return -15064528 } // 0xFF1A2230
func flPipe() -> Int { return -13722033 } // 0xFF2E9E4F
func flPipeCap() -> Int { return -14714312 } // 0xFF1F7A38
func flBird() -> Int { return -11713 } // 0xFFFFD23F
func flBeak() -> Int { return -36797 } // 0xFFFF7043
func flGround() -> Int { return -12702178 } // 0xFF3E2E1E
func flGrass() -> Int { return -10508214 } // 0xFF5FA84A
func flText() -> Int { return -6643536 } // 0xFF9AA0B0
func flWhite() -> Int { return -1 }
func flCloud() -> Int { return -14405560 } // 0xFF243048
func flBlack() -> Int { return -16777216 }
// ── 消息类型 / 键码(唯一真源 Lib/c/vml_ui.h)────────────────────────
// ── Message types / key codes (source of truth: Lib/c/vml_ui.h) ────────────────────
func flMsgKeydown() -> Int { return 1 }
func flMsgTouchdown() -> Int { return 6 }
func flMsgTimer() -> Int { return 9 }
func flMsgClose() -> Int { return 10 }
func flKeyEnter() -> Int { return 13 }
func flKeyEsc() -> Int { return 27 }
func flKeySpace() -> Int { return 32 }
func flKeyUp() -> Int { return 38 }
func flKeyR() -> Int { return 82 }
// ── 管子槽位 ──────────────────────────────────────────────────
// ── Pipe slots ────────────────────────────────────────────────
// ⚠ 这个**常量**同时被初始化、左移、回收、计分、碰撞五个循环用 —— 定下来之后要 grep 一遍
// ⚠ This **constant** is used by five loops (init, scroll, recycle, score, collide). Once it is fixed,
// 所有遍历它的循环(同目录 `plane.swift` 的文件头记着"敌机槽位从 4 改成 6 时漏改一圈"
// grep every loop that iterates over it — `plane.swift` in this directory records the pitfall of
// 那个坑:第 5、6 号槽**永不回收、也永不扣命**,表现成"游戏自己停了")。
// changing the enemy slot count from 4 to 6 and missing one loop (slots 5 and 6 were never recycled
// and never cost a life, which showed up as "the game stopped by itself").
func flSlots() -> Int { return 3 }
// ── 版面:**全部由窗口尺寸推**,一处解析度都不写死 ──────────────────
// ── Layout: **everything derived from the window size**, no hardcoded resolution ─────────────
func flLayout() {
var w = A[65]
var h = A[66]
// 地面吃掉下方约 1/8(下限 70px、上限 140px)
// The ground takes roughly the bottom 1/8 of the height (clamped to 70..140 px)
var gy = h / 8
if gy < 70 { gy = 70 }
if gy > 140 { gy = 140 }
A[77] = h - gy
// 鸟:半径 ≈ 宽的 1/22(下限 12、上限 22)
// Bird: radius ≈ 1/22 of the width (clamped to 12..22)
A[79] = w / 14
if A[79] < 46 { A[79] = 46 }
if A[79] > 92 { A[79] = 92 }
// 缺口高度 ≈ 可用高的 1/4(下限 150、上限 260)
// Gap height ≈ 1/4 of the usable height (clamped to 150..260)
A[78] = (A[77] - 60) / 4
if A[78] < 150 { A[78] = 150 }
if A[78] > 260 { A[78] = 260 }
A[85] = 60
A[86] = A[77] - A[78] - 60
if A[86] < A[85] { A[86] = A[85] + 10 }
// ⚠ 速度与间距必须**一起定**:手感是"两对管子之间约 1.3 秒",那是 `间距 / 速度` 的比值,
// ⚠ Speed and spacing must be chosen **together**: the feel is "about 1.3 s between pipe pairs",
// 单独调哪一个都会把手感调飞(实测第一版速度取 h/160 ⇒ 16 px/s,管子过一块要 25 秒)。
// which is the ratio `spacing / speed`; tuning either one alone throws the feel off (the first version
// used h/160 ⇒ 16 px/s, i.e. 25 s for one pipe to cross).
// 速度单位是 **1/8 像素/拍**,30ms 一拍 ⇒ 每秒走 `A[80] / 8 * 33.3` 像素。
// Speed is in **eighths of a pixel per tick**; a 30 ms tick means `A[80] / 8 * 33.3` px per second.
A[80] = h / 24
if A[80] < 10 { A[80] = 10 }
if A[80] > 60 { A[80] = 60 }
A[87] = w * 2 / 5
if A[87] < 140 { A[87] = 140 }
// 间距至少要够"三块管子同时在场"才看得出是管子阵(窄屏按间距兜底,别挤成一堆)
// Spacing must at least keep three pipes on screen, otherwise they pile up on narrow screens
if A[87] < w / 2 { A[87] = w / 2 }
// ⚠ 鸟的 x **也存成定点**:原来存的是像素、绘制时又除了一次 FP ⇒ 贴到屏幕最左边
// ⚠ The bird's x is **also stored in fixed point**: it used to be stored in pixels and then divided
// (实测 160/8 = 20)。单位混用是这一族 bug 的通用形态:**同一列数据只能有一种单位**。
// by FP again at draw time, which pinned it to the far left (measured 160/8 = 20). Mixing units is
// the generic form of this bug family — **one column of data, one unit**.
A[70] = (w / 3 + A[79] / 2) * flFP()
A[83] = 0
A[84] = 0
}
// 鸟的半径(像素)—— 绘制与碰撞**共用这一个函数**,不允许两处各算一遍
// Bird radius (pixels) — drawing and collision **share this one function**, never two separate calculations
func flBirdR() -> Int {
var r = A[79] / 3
if r < 7 { r = 7 }
return r
}
// ── 音效(单通道:一次事件只发一个音)──────────────────────────
// ── Sound (single channel: one sound per event) ──────────────────
func flSfxFlap() { ui_audio_beep(660, 32) }
func flSfxScore() { ui_audio_beep(1319, 55) }
func flSfxHit() { ui_audio_beep(196, 90) }
func flSfxDie() { ui_audio_beep(131, 300) }
// ── 存档:最高分 ────────────────────────────────────────────────
// ── Save: best score ────────────────────────────────────────────
// **`defer` 用在这里**:不管走哪条出口,缓冲区都要还回去(修之前的 `defer` 一个字都不跑)。
// **`defer` in use**: whatever exit is taken, the scratch slot is released (before the fix, `defer`
// never ran a single statement).
func flLoadBest() {
var buf = " "
defer { A[72] = A[72] }
var n = ui_store_get("flappy_best", buf, 48)
if n <= 0 {
A[72] = 0
return
}
var v = str_to_int(buf)
if v < 0 { v = 0 }
A[72] = v
}
func flSaveBest() {
defer { A[72] = A[72] }
if A[71] > A[72] { A[72] = A[71] }
ui_store_set("flappy_best", int_to_str(A[72]))
}
// ── HUD 文案:**字符串 `+` 已经修好了**(修之前拼出来是空串,只能拆成多次 ui_2d_text)──
// ── HUD text: **string `+` is fixed** (before the fix it produced the empty string, so the text had to
// be split across several `ui_2d_text` calls) ──
func flHudText() -> String {
return flT("得分 ", "Score ") + int_to_str(A[71]) + flT(" 最高 ", " Best ") + int_to_str(A[72])
}
// ── 开一局 ────────────────────────────────────────────────────
// ── Start a run ───────────────────────────────────────────────
func flReset() {
var i = 0
while i < flSlots() {
// ⚠ 管子的 x **一律定点**(与鸟同一列数据同一单位)。第一版这里存的是**像素**、
// ⚠ The pipe x is **fixed point throughout**, the same unit as the bird. The first version stored
// 滚动却减的是定点量 ⇒ 管子每拍走 30 **像素**(快 8 倍,0.4 秒就撞上),
// **pixels** here while the scroll subtracted a fixed-point amount, so pipes moved 30 **pixels**
// 而同一族错误在鸟那一处是"存像素、绘制除 8"(贴到屏幕左边)。
// per tick (8× too fast, hitting the bird within 0.4 s) — while the same family of mistake at the
// 一个位置量只能有一种单位,改了就要把**初始化 / 滚动 / 回收 / 计分 / 碰撞 / 绘制**
// bird was "store pixels, divide by 8 when drawing" (pinning it to the left edge). One position
// 六处一起改(漏一处就是"看着像另一个 bug")。
// quantity, one unit — and changing it means changing **init / scroll / recycle / score /
// collide / draw** together (missing one looks like a different bug entirely).
A[i] = (A[65] + i * A[87]) * flFP()
A[16 + i] = 60 + ui_math_rand(A[86] - A[85])
A[32 + i] = 0
i = i + 1
}
i = 0
while i < 3 {
A[48 + i] = ui_math_rand(A[65])
i = i + 1
}
A[68] = (A[77] / 2) * flFP()
A[69] = 0
A[71] = 0
A[81] = 0
A[88] = 0
A[89] = -1
A[76] = 0
A[84] = 0
A[74] = ui_time_get_ms()
A[64] = 0
// 上一局没读完的输入(一次点击常有多条)不能喂给新一局。
// ⚠ Input left over from the previous round (one tap is often several messages) must not feed the new
// ⚠ **但第一次(程序刚起来那一次)不能丢** —— 实测:启动那一下 `ui_win_open` + 首帧绘制
// one — but **the very first call (program startup) must not drop anything**. Measured: start-up
// 比玩家的手快不了多少,200ms 就点下去的那一下会**正躺在队列里**,
// (`ui_win_open` + first frame) is not much faster than a player's finger, so a tap sent at 200 ms is
// 被这里一句 `ui_msg_clear()` 静默吃掉 ⇒ 玩家看到的是"点了没反应,得再点一下"。
// **already sitting in the queue** and this `ui_msg_clear()` silently ate it, which reads as "tapped
// 所以只从**第二局**起清队列(A[90] 就是"已经开过一局"这个标志)。
// and nothing happened, tap again". So the queue is cleared only from the **second** run on
// (A[90] is the "a run has already been started" flag).
if A[90] != 0 { ui_msg_clear() }
else { A[90] = 1 }
}
// ── 扇翅膀 ────────────────────────────────────────────────────
// ── Flap ──────────────────────────────────────────────────────
func flFlap() {
// ⚠ **坠落中(st=2)与结束后(st=3)都不许再扇翅膀**。
// ⚠ **No flapping while falling (st=2) or after the run is over (st=3)**. The first version only
// 第一版只挡了 st=3 ⇒ 撞了之后继续点屏幕,鸟会**一直被顶着往上升**、
// blocked st=3, so tapping after a hit kept **pushing the bird upward forever**, it never reached
// 永远落不了地 ⇒ `flSettle` 永远不执行 ⇒ 结算画面与最高分存档**整条路都不跑**
// the ground, `flSettle` never ran, and the whole settle path (final screen + best-score save)
// (现象是"撞了以后游戏不结束,鸟飞出屏幕")。这一类叫「结束态忘了收输入」。
// never executed. The symptom reads as "after a crash the game never ends and the bird flies off
// screen". This family is called "the end state forgot to cut off input".
if A[64] == 2 || A[64] == 3 { return }
if A[64] == 0 { A[64] = 1 }
A[69] = 0 - 60
A[88] = A[88] + 1
flSfxFlap()
}
// ── 撞了:先坠落,落了地才结算 ────────────────────────────────
// ── Hit: fall first, settle only once the ground is reached ────────────────────
func flDie(why: Int) {
if A[64] != 1 { return }
A[64] = 2
A[75] = ui_time_get_ms()
A[76] = 12
A[89] = why
flSfxHit()
ui_vibrate_simple(90, 1)
}
func flSettle() {
A[64] = 3
flSaveBest()
flSfxDie()
ui_vibrate_simple(50, 1)
}
// ── 每拍(30ms ⇒ 约 33 拍/秒;⚠ 拍是逻辑,帧率由宿主按显示预算走)──
// ── Per tick (30ms ⇒ about 33 ticks/s; ⚠ a tick is logic, the host paces frames) ──
func flTick() {
var i = 0
if A[64] == 2 {
// 坠落:还是同一套重力,只是不再响应输入
// Falling: the same gravity, just no longer responding to input
A[69] = A[69] + 6
if A[69] > 90 { A[69] = 90 }
A[68] = A[68] + A[69]
if A[68] >= A[77] * flFP() {
A[68] = A[77] * flFP()
flSettle()
}
return
}
if A[64] == 0 {
// 待开始:鸟原地上下浮,管子已经排好但不动
// Ready: the bird bobs in place; the pipes are already laid out but do not move yet
A[81] = (A[81] + 1) % 60
var t = A[81]
if t < 30 { A[68] = (A[77] / 2 + t / 3) * flFP() }
else { A[68] = (A[77] / 2 + (60 - t) / 3) * flFP() }
return
}
if A[64] != 1 { return }
// 重力 + 位移
// Gravity + motion
A[69] = A[69] + 6
if A[69] > 90 { A[69] = 90 }
A[68] = A[68] + A[69]
// 管子左移 + 回收(x + 3×间距 = 保持等距)
// Pipes move left + recycle (x + 3×spacing keeps them evenly spaced)
i = 0
while i < flSlots() {
A[i] = A[i] - A[80]
if A[i] + A[79] * flFP() < 0 {
A[i] = A[i] + flSlots() * A[87] * flFP()
A[16 + i] = 60 + ui_math_rand(A[86] - A[85])
A[32 + i] = 0
}
i = i + 1
}
// 云(视差:比管子慢一半)
// Clouds (parallax: half the pipe speed)
i = 0
while i < 3 {
A[48 + i] = A[48 + i] - A[80] / 2
if A[48 + i] < 0 - 60 { A[48 + i] = A[65] + 20 }
i = i + 1
}
// 地面纹理滚动
// Ground texture scrolls
A[84] = A[84] + A[80]
if A[84] > 40 { A[84] = A[84] - 40 }
// 计分:管子的右沿走过了鸟的中心
// Score: the pipe's right edge has passed the bird's centre
i = 0
while i < flSlots() {
if A[32 + i] == 0 && A[i] + A[79] * flFP() < A[70] {
A[32 + i] = 1
A[71] = A[71] + 1
flSfxScore()
}
i = i + 1
}
// 碰撞:鸟的包围盒 × 管子缺口
// Collision: the bird's box against the pipe gap
var bx = A[70] / flFP()
var by = A[68] / flFP()
var r = flBirdR()
// 落地
// Ground
if by + r >= A[77] { flDie(2); return }
// 天花板
// Ceiling
if by - r < 0 { A[68] = r * flFP(); A[69] = 0 }
i = 0
while i < flSlots() {
var px = A[i] / flFP()
if bx + r > px && bx - r < px + A[79] {
var gy = A[16 + i]
if by - r < gy { flDie(1); return }
if by + r > gy + A[78] { flDie(1); return }
}
i = i + 1
}
}
// ── 画一帧 ────────────────────────────────────────────────────
// ── Draw one frame ────────────────────────────────────────────
func flDraw() {
var w = A[65]
var h = A[66]
ui_2d_clear(flSky())
// 上半层稍微亮一点(天空的两段色带,省掉渐变刷子)
// The upper half is a touch lighter (a two-band sky, avoiding a gradient brush)
ui_2d_rect(0, 0, w, A[77] / 3, flSky2(), 1, 0, 0)
// 云
// Clouds
var i = 0
while i < 3 {
var cx = A[48 + i]
var cy = 70 + i * 90
ui_2d_rect(cx, cy, 54, 16, flCloud(), 1, 0, 8)
ui_2d_rect(cx + 14, cy - 10, 34, 14, flCloud(), 1, 0, 7)
i = i + 1
}
// 管子(成对:上管 + 下管 + 两个管口)
// Pipes (in pairs: upper + lower + two caps)
i = 0
while i < flSlots() {
var px = A[i] / flFP()
var gy = A[16 + i]
ui_2d_rect(px, 0, A[79], gy, flPipe(), 1, 0, 0)
ui_2d_rect(px - 5, gy - 18, A[79] + 10, 18, flPipeCap(), 1, 0, 3)
ui_2d_rect(px, gy + A[78], A[79], A[77] - gy - A[78], flPipe(), 1, 0, 0)
ui_2d_rect(px - 5, gy + A[78], A[79] + 10, 18, flPipeCap(), 1, 0, 3)
i = i + 1
}
// 地面
// Ground
ui_2d_rect(0, A[77], w, h - A[77], flGround(), 1, 0, 0)
ui_2d_rect(0, A[77], w, 6, flGrass(), 1, 0, 0)
i = 0
while i < 8 {
ui_2d_rect(i * 40 - A[84], A[77] + 14, 22, 5, flGrass(), 1, 0, 2)
i = i + 1
}
// 鸟:身体 + 翅膀(扇动时抬起)+ 嘴 + 眼
// Bird: body + wing (raised while flapping) + beak + eye
var bx = A[70] / flFP()
var by = A[68] / flFP()
var br = flBirdR()
ui_2d_circle(bx, by, br, flBird(), 1, 0)
if A[69] < 0 { ui_2d_rect(bx - br, by - br / 2, br, br, flBeak(), 1, 0, 5) }
else { ui_2d_rect(bx - br, by + br / 4, br, br, flBeak(), 1, 0, 5) }
ui_2d_rect(bx + br - 5, by - 6, br, br / 2, flBeak(), 1, 0, 2)
ui_2d_circle(bx + br / 3, by - br / 3, br / 5, flBlack(), 1, 0)
// 撞了那一下闪白(12 拍内)
// White flash on impact (within 12 ticks)
// ⚠ 用 **`ui_2d_set_alpha` + 实心矩形**,不要用"粗线框"(`fill=0, lw=6`)——
// ⚠ Use **`ui_2d_set_alpha` plus a filled rect**, not a thick outline (`fill=0, lw=6`), which draws a **white
// 那画出来是一圈**白边框**,不是"整屏闪一下"。
// border** rather than a full-screen flash.
if A[76] > 0 {
A[76] = A[76] - 1
ui_2d_set_alpha(70)
ui_2d_rect(0, 0, w, h, flWhite(), 1, 0, 0)
ui_2d_set_alpha(255)
}
// HUD:**一句拼出来的文案**(字符串 `+` 修好之后才写得出这一行)
// HUD: **one concatenated line** (only writable now that string `+` is fixed)
ui_2d_text(w / 2, 26, flHudText(), flWhite(), 18, VML_ANCHOR_CENTER)
if A[64] == 0 {
ui_2d_text(w / 2, A[77] / 2 + 90, flT("轻点起飞", "Tap to fly"), flText(), 15, VML_ANCHOR_CENTER)
ui_2d_text(w / 2, A[77] / 2 + 116, flT("点一下扇一次翅膀", "each tap is one flap"), flText(), 12, VML_ANCHOR_CENTER)
}
if A[64] == 3 {
ui_2d_text(w / 2, A[77] / 2 - 40, flT("这局结束", "Game over"), flWhite(), 24, VML_ANCHOR_CENTER)
// 新纪录:把 best / score 两次比较分开写(`?:` 里再嵌 `?:` 容易看错)
// New record: the two comparisons are kept separate (a `?:` nested in a `?:` is easy to misread)
var line = flHudText()
if A[71] >= A[72] && A[71] > 0 { line = flT("新纪录!", "New record! ") + int_to_str(A[71]) }
ui_2d_text(w / 2, A[77] / 2, line, flPipe(), 18, VML_ANCHOR_CENTER)
ui_2d_text(w / 2, A[77] / 2 + 40, flT("轻点重来(或按 R)", "tap to retry (or press R)"), flText(), 14, VML_ANCHOR_CENTER)
}
ui_2d_present()
}
func main() {
// 界面语言:开局问一次宿主(0=中文 1=英文),存进 A[67](见 flT)
// UI language: ask the host once at the start (0=Chinese 1=English) and store it in A[67] (see flT)
A[67] = ui_scr_get_language()
var lang = A[67]
// 竖屏 + 不要屏幕手柄区 —— 两条都在**开窗之前**声明
// Portrait + no on-screen gamepad — both declared **before** the window opens
// 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)
// Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)
ui_win_open_ex(lang == 0 ? "过管子" : "Flappy", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD)
// 尺寸:开窗**之后**再问一次 —— 这时 `ui_scr_get_w/h` 给的是实测画布(真值)
// Size: re-read **after** the open — `ui_scr_get_w/h` now report the measured canvas (the real value)
A[65] = ui_scr_get_w()
A[66] = ui_scr_get_h()
if A[65] <= 0 { A[65] = 411 }
if A[66] <= 0 { A[66] = 726 }
flLayout()
ui_scr_set_keep_on(1)
flLoadBest()
flReset()
flDraw()
A[82] = ui_timer_set(30, 0)
var acc = 0 // 运动墙钟欠账(毫秒)—— 按墙钟补步,用户 2026-10-04 口径
// Motion wall-clock debt in ms — step by wall clock (user rule, 2026-10-04)
var lastMs = ui_time_get_ms() // 上次补步取的墙钟时刻(墙钟起点,进主循环时取)
// Wall-clock time of the last step (taken when entering the main loop)
while ui_win_is_closed() == 0 {
var t = ui_msg_fetch_wait(30)
if t == flMsgClose() { break }
// 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。
// 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.
var nowMs = ui_time_get_ms()
acc = acc + (nowMs - lastMs)
lastMs = nowMs
if acc > 30 * 8 { acc = 0 } // 卡太久别一次补一大串(防瞬移)
// Do not catch up in one huge burst after a long stall (anti-teleport)
while acc >= 30 {
acc = acc - 30
flTick()
}
if t == flMsgTimer() {
// 定时器**只负责唤醒**(推进在上面按墙钟欠账走)
// The timer **only wakes us up** — the advance happens above, by wall-clock debt
flDraw()
}
else if t == flMsgKeydown() {
var k = ui_msg_get_a()
if k == flKeyEsc() { break }
else if k == flKeySpace() || k == flKeyUp() || k == flKeyEnter() {
if A[64] == 3 { flReset(); flFlap() } else { flFlap() }
}
else if k == flKeyR() { flReset() }
flDraw()
}
else if t == flMsgTouchdown() {
if A[64] == 3 { flReset(); flFlap() } else { flFlap() }
flDraw()
}
}
ui_timer_kill(A[82])
ui_scr_set_keep_on(0)
ui_win_close()
}